fix teacher, parent and admin pages

This commit is contained in:
root
2026-04-25 00:00:10 -04:00
parent 7fe34dde0d
commit 3e77fc92c7
275 changed files with 46412 additions and 3325 deletions
+148
View File
@@ -0,0 +1,148 @@
/**
* Incidents / flags API (parity with CI `flags/*` + JSON routes in Laravel).
* Expected base: `/api/v1/administrator/flags/...` with JWT.
*/
import { apiFetch } from './http'
export type IncidentFlagRow = {
id?: number
flag_state?: string
student_name?: string
grade?: string
flag?: string
open_description?: string
close_description?: string
cancel_description?: string
flag_datetime?: string | null
action_taken?: string | null
updated_by_open_name?: string | null
updated_by_open?: string | number | null
updated_by_closed_name?: string | null
updated_by_closed?: string | number | null
updated_by_canceled_name?: string | null
updated_by_canceled?: string | number | null
}
export type IncidentLogRow = {
flag?: string
flag_state?: string
flag_datetime?: string | null
open_description?: string
close_description?: string
cancel_description?: string
action_taken?: string | null
}
export type IncidentAnalysisStudent = {
student_name?: string
grade?: string
total?: number
open?: number
closed?: number
canceled?: number
last_incident?: string | null
logs?: IncidentLogRow[]
}
export type GradeOption = { id: number; name: string }
function qs(searchParams: URLSearchParams): string {
const sy = searchParams.get('school_year')
const sem = searchParams.get('semester')
const q = new URLSearchParams()
if (sy) q.set('school_year', sy)
if (sem) q.set('semester', sem)
const s = q.toString()
return s ? `?${s}` : ''
}
function unwrapFlags(body: unknown): unknown[] {
if (Array.isArray(body)) return body
if (body && typeof body === 'object') {
const o = body as { flags?: unknown; data?: { flags?: unknown } }
const raw = o.flags ?? o.data?.flags
if (Array.isArray(raw)) return raw
}
return []
}
function unwrapStudents(body: unknown): unknown[] {
if (Array.isArray(body)) return body
if (body && typeof body === 'object') {
const o = body as { students?: unknown; data?: { students?: unknown } }
const raw = o.students ?? o.data?.students
if (Array.isArray(raw)) return raw
}
return []
}
export async function fetchFlagsProcessed(searchParams: URLSearchParams): Promise<IncidentFlagRow[]> {
const body = await apiFetch<{ flags?: IncidentFlagRow[]; data?: { flags?: IncidentFlagRow[] } }>(
`/api/v1/administrator/flags/processed${qs(searchParams)}`,
)
return unwrapFlags(body) as IncidentFlagRow[]
}
export async function fetchFlagsPending(searchParams: URLSearchParams): Promise<IncidentFlagRow[]> {
const body = await apiFetch<{ flags?: IncidentFlagRow[]; data?: { flags?: IncidentFlagRow[] } }>(
`/api/v1/administrator/flags/pending${qs(searchParams)}`,
)
return unwrapFlags(body) as IncidentFlagRow[]
}
export async function fetchIncidentAnalysis(
searchParams: URLSearchParams,
): Promise<IncidentAnalysisStudent[]> {
const body = await apiFetch<{
students?: IncidentAnalysisStudent[]
data?: { students?: IncidentAnalysisStudent[] }
}>(`/api/v1/administrator/flags/incident-analysis${qs(searchParams)}`)
return unwrapStudents(body) as IncidentAnalysisStudent[]
}
export async function fetchFlagsFormMeta(): Promise<{ grades: GradeOption[] }> {
return apiFetch<{ grades: GradeOption[] }>('/api/v1/administrator/flags/form-meta')
}
export async function fetchStudentsByGrade(gradeId: number): Promise<Array<{ id: number; name: string }>> {
return apiFetch<Array<{ id: number; name: string }>>(
`/api/v1/administrator/flags/grades/${gradeId}/students`,
)
}
export async function createIncidentFlag(payload: {
grade: number | string
student: number | string
flag: string
description: string
school_year?: string
semester?: string
}): Promise<{ message?: string } | unknown> {
return apiFetch(`/api/v1/administrator/flags`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export async function closeIncidentFlag(
id: number,
body: { state_description: string; action_taken: string },
): Promise<unknown> {
return apiFetch(`/api/v1/administrator/flags/${id}/close`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export async function cancelIncidentFlag(
id: number,
body: { state_description: string; action_taken: string },
): Promise<unknown> {
return apiFetch(`/api/v1/administrator/flags/${id}/cancel`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
+154
View File
@@ -0,0 +1,154 @@
/**
* Administrator grading API (parity with CI `Views/grading/*.php`).
* Base: `/api/v1/administrator/grading/...` with JWT.
*/
import { apiFetch } from './http'
const BASE = '/api/v1/administrator/grading'
export type GradingScoreRow = {
id?: number
score?: number | string | null
comment?: string | null
}
export type GradingScoreFormPayload = {
scoresLocked?: boolean
student?: { id?: number; firstname?: string; lastname?: string }
classSectionId?: number
scores?: GradingScoreRow[]
}
function q(sp: URLSearchParams): string {
const s = sp.toString()
return s ? `?${s}` : ''
}
export async function fetchGradingMain(searchParams: URLSearchParams): Promise<unknown> {
return apiFetch(`${BASE}/main${q(searchParams)}`)
}
export async function fetchGradingScoreForm(
scoreType: string,
studentId: string,
classSectionId: string,
): Promise<GradingScoreFormPayload> {
const qs = new URLSearchParams({
student_id: studentId,
class_section_id: classSectionId,
})
return apiFetch(`${BASE}/scores/${encodeURIComponent(scoreType)}?${qs}`)
}
export async function postGradingScoreUpdate(
scoreType: string,
body: Record<string, unknown>,
): Promise<unknown> {
return apiFetch(`${BASE}/scores/${encodeURIComponent(scoreType)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export async function fetchBelowSixty(searchParams: URLSearchParams): Promise<unknown> {
return apiFetch(`${BASE}/below-sixty${q(searchParams)}`)
}
export async function fetchBelowSixtyEmailEditor(searchParams: URLSearchParams): Promise<{
studentId?: number
studentName?: string
semester?: string
schoolYear?: string
subject?: string
html?: string
}> {
return apiFetch(`${BASE}/below-sixty/email-editor${q(searchParams)}`)
}
export async function postBelowSixtyEmail(body: Record<string, unknown>): Promise<unknown> {
return apiFetch(`${BASE}/below-sixty/email`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export async function postBelowSixtyStatus(body: Record<string, unknown>): Promise<unknown> {
return apiFetch(`${BASE}/below-sixty/status`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export async function fetchHomeworkTracking(
searchParams: URLSearchParams,
): Promise<unknown> {
return apiFetch(`${BASE}/homework-tracking${q(searchParams)}`)
}
export async function fetchParticipation(searchParams: URLSearchParams): Promise<unknown> {
return apiFetch(`${BASE}/participation${q(searchParams)}`)
}
export async function postParticipation(body: Record<string, unknown>): Promise<unknown> {
return apiFetch(`${BASE}/participation`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export async function fetchPlacementIndex(searchParams: URLSearchParams): Promise<unknown> {
return apiFetch(`${BASE}/placement-index${q(searchParams)}`)
}
export async function postPlacementAll(body: Record<string, unknown>): Promise<unknown> {
return apiFetch(`${BASE}/placement-all`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export async function fetchPlacementSection(searchParams: URLSearchParams): Promise<unknown> {
return apiFetch(`${BASE}/placement${q(searchParams)}`)
}
export async function postPlacementSection(body: Record<string, unknown>): Promise<unknown> {
return apiFetch(`${BASE}/placement`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export async function fetchPlacementBatch(batchId: string | number): Promise<unknown> {
return apiFetch(`${BASE}/placement-batch/${batchId}`)
}
export async function postPlacementBatch(
batchId: string | number,
body: Record<string, unknown>,
): Promise<unknown> {
return apiFetch(`${BASE}/placement-batch/${batchId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export async function fetchScheduleMeetingForm(
searchParams: URLSearchParams,
): Promise<unknown> {
return apiFetch(`${BASE}/schedule-meeting${q(searchParams)}`)
}
export async function postScheduleMeeting(body: Record<string, unknown>): Promise<unknown> {
return apiFetch(`${BASE}/schedule-meeting`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
+11 -1
View File
@@ -37,7 +37,17 @@ export async function apiFetch<T>(
headers.set('Authorization', `Bearer ${token}`)
}
const res = await fetch(apiUrl(path), { ...init, headers })
const url = apiUrl(path)
let res: Response
try {
res = await fetch(url, { ...init, headers })
} catch (e) {
const hint = import.meta.env.DEV
? ' Start the API and ensure Vite can reach it (see vite.config.ts; default proxy is http://192.168.3.100:8000).'
: ''
const reason = e instanceof Error ? e.message : 'Failed to fetch'
throw new ApiHttpError(`Network error: ${reason}.${hint}`, 0, null)
}
const body: unknown = await res.json().catch(() => null)
if (!res.ok) {
+158
View File
@@ -0,0 +1,158 @@
/**
* Inventory (parity with CI `Views/inventory/*.php`).
* Base: `/api/v1/administrator/inventory` with JWT unless noted.
*/
import { apiFetch } from './http'
const BASE = '/api/v1/administrator/inventory'
function q(searchParams: URLSearchParams): string {
const s = searchParams.toString()
return s ? `?${s}` : ''
}
export type InventoryKind = 'book' | 'classroom' | 'kitchen' | 'office'
/** Index list: expect `{ items, categories, userNames }` (names optional). */
export async function fetchInventoryIndex(
kind: InventoryKind,
searchParams: URLSearchParams,
): Promise<unknown> {
return apiFetch(`${BASE}/${kind}${q(searchParams)}`)
}
export async function postInventoryCategory(body: Record<string, unknown>): Promise<unknown> {
return apiFetch(`${BASE}/category`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export async function fetchInventoryItem(itemId: string | number): Promise<{
item?: Record<string, unknown>
categories?: Record<string, unknown>[]
conditionOptions?: Record<string, string>
[key: string]: unknown
}> {
return apiFetch(`${BASE}/items/${itemId}`)
}
/** Create form payload (categories, defaults). */
export async function fetchInventoryCreateForm(
kind: InventoryKind,
searchParams: URLSearchParams,
): Promise<unknown> {
return apiFetch(`${BASE}/${kind}/create${q(searchParams)}`)
}
export async function postInventoryItem(body: Record<string, unknown>): Promise<unknown> {
return apiFetch(`${BASE}/items`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export async function putInventoryItem(
itemId: string | number,
body: Record<string, unknown>,
): Promise<unknown> {
return apiFetch(`${BASE}/items/${itemId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export async function deleteInventoryItem(itemId: string | number): Promise<unknown> {
return apiFetch(`${BASE}/items/${itemId}`, { method: 'DELETE' })
}
export async function postInventoryAdjust(
itemId: string | number,
body: Record<string, unknown>,
): Promise<unknown> {
return apiFetch(`${BASE}/items/${itemId}/adjust`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export async function postClassroomAudit(
itemId: string | number,
body: Record<string, unknown>,
): Promise<unknown> {
return apiFetch(`${BASE}/items/${itemId}/classroom-audit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export async function fetchInventorySummary(searchParams: URLSearchParams): Promise<unknown> {
return apiFetch(`${BASE}/summary${q(searchParams)}`)
}
export async function fetchMovements(searchParams: URLSearchParams): Promise<unknown> {
return apiFetch(`${BASE}/movements${q(searchParams)}`)
}
export async function fetchMovementForm(
movementId: string | number | undefined,
searchParams: URLSearchParams,
): Promise<unknown> {
if (movementId === undefined || movementId === '') {
return apiFetch(`${BASE}/movements/create${q(searchParams)}`)
}
return apiFetch(`${BASE}/movements/${movementId}/edit${q(searchParams)}`)
}
export async function postMovement(body: Record<string, unknown>): Promise<unknown> {
return apiFetch(`${BASE}/movements`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export async function putMovement(
movementId: string | number,
body: Record<string, unknown>,
): Promise<unknown> {
return apiFetch(`${BASE}/movements/${movementId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export async function deleteMovement(movementId: string | number): Promise<unknown> {
return apiFetch(`${BASE}/movements/${movementId}`, { method: 'DELETE' })
}
export async function postMovementsBulkDelete(ids: number[]): Promise<unknown> {
return apiFetch(`${BASE}/movements/bulk-delete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids }),
})
}
/** Teacher book distribution — JWT; base matches CI `inventory/books/distribute`. */
const TEACHER_DIST = '/api/v1/teacher/inventory/books/distribute'
export async function fetchTeacherBookDistribute(
searchParams: URLSearchParams,
): Promise<unknown> {
return apiFetch(`${TEACHER_DIST}${q(searchParams)}`)
}
export async function postTeacherBookDistribute(body: Record<string, unknown>): Promise<unknown> {
return apiFetch(TEACHER_DIST, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
+70
View File
@@ -0,0 +1,70 @@
/**
* Administrator invoice management & PDF (parity with CI `invoice_payment/*`).
*/
import { apiUrl } from '../lib/apiOrigin'
import { apiFetch, getStoredToken } from './http'
const BASE = '/api/v1/administrator/invoices'
function q(searchParams: URLSearchParams): string {
const s = searchParams.toString()
return s ? `?${s}` : ''
}
export type InvoiceManagementRow = {
parent_id?: number
parent_name?: string
enrolledKids?: { name?: string; grade?: string | number }[]
invoice_id?: number | null
invoice_amount?: number
refund_amount?: number
invoice_date?: string
}
export type InvoiceManagementResponse = {
ok?: boolean
schoolYears?: string[]
schoolYear?: string
invoices?: InvoiceManagementRow[]
}
export async function fetchInvoiceManagement(
searchParams: URLSearchParams,
): Promise<InvoiceManagementResponse> {
return apiFetch(`${BASE}/management${q(searchParams)}`)
}
export async function postInvoiceGenerate(parentId: number): Promise<unknown> {
return apiFetch(`${BASE}/generate-for-parent`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ parent_id: parentId }),
})
}
/** HTML/JSON preview for browser print view (mirrors FPDF layout). */
export async function fetchInvoicePreview(invoiceId: string | number): Promise<unknown> {
return apiFetch(`${BASE}/${invoiceId}/preview`)
}
/** Binary PDF with JWT (for tabs that cannot send Authorization). */
export async function fetchInvoicePdfBlob(invoiceId: string | number): Promise<Blob> {
const token = getStoredToken()
const res = await fetch(apiUrl(`${BASE}/${invoiceId}/pdf`), {
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {}),
Accept: 'application/pdf',
},
})
if (!res.ok) {
const text = await res.text().catch(() => '')
throw new Error(text || `PDF HTTP ${res.status}`)
}
return res.blob()
}
export function openPdfBlobInNewTab(blob: Blob): void {
const url = URL.createObjectURL(blob)
window.open(url, '_blank', 'noopener,noreferrer')
setTimeout(() => URL.revokeObjectURL(url), 120_000)
}
+49
View File
@@ -0,0 +1,49 @@
/**
* Password reset / initial set-password flows (legacy CI `user/*` auth views).
* Backend expected under `/api/v1/auth/...`.
*/
import { apiFetch } from './http'
export async function requestForgotPassword(email: string): Promise<{ ok?: boolean; message?: string }> {
return apiFetch('/api/v1/auth/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email.trim() }),
})
}
export async function submitPasswordReset(payload: {
token: string
password: string
pass_confirm: string
}): Promise<{ ok?: boolean; message?: string }> {
return apiFetch('/api/v1/auth/reset-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export async function submitSetPassword(payload: {
user_id: number | string
token: string
password: string
password_confirm: string
}): Promise<{ ok?: boolean; message?: string }> {
return apiFetch('/api/v1/auth/set-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export async function submitAuthorizedUserPassword(
userId: number | string,
payload: { token: string; password: string; password_confirm: string },
): Promise<{ ok?: boolean; message?: string }> {
return apiFetch(`/api/v1/auth/set-authorized-user-password/${encodeURIComponent(String(userId))}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
+347
View File
@@ -0,0 +1,347 @@
/**
* Administrator payment / notifications / financial report API.
* Paths mirror Laravel `/api/v1/administrator/...` conventions expected by the SPA.
*/
import { apiUrl } from '../lib/apiOrigin'
import { ApiHttpError, apiFetch, getStoredToken } from './http'
export type AdminNotificationRow = {
id?: number
title?: string
message?: string
priority?: string | number
scheduled_at?: string
expires_at?: string
deleted_at?: string
target_group?: string
created_at?: string
isExpired?: boolean
}
export async function fetchNotificationsDeleted(params?: {
school_year?: string
semester?: string
}): Promise<{ notifications: AdminNotificationRow[] }> {
const qs = new URLSearchParams()
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/notifications/deleted${suffix}`)
}
export async function fetchNotificationsActive(params?: {
target_group?: string
school_year?: string
semester?: string
}): Promise<{ notifications: AdminNotificationRow[] }> {
const qs = new URLSearchParams()
if (params?.target_group) qs.set('target_group', params.target_group)
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/notifications/active${suffix}`)
}
export async function restoreDeletedNotification(
id: number | string,
): Promise<{ ok?: boolean; message?: string }> {
return apiFetch(`/api/v1/notifications/${encodeURIComponent(String(id))}/restore`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
})
}
async function postMultipart<T>(path: string, formData: FormData): 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', headers, body: formData })
const body: unknown = await res.json().catch(() => null)
if (!res.ok) {
const msg =
typeof body === 'object' && body !== null && 'message' in body
? String((body as { message?: unknown }).message ?? '')
: ''
throw new ApiHttpError(msg || `HTTP ${res.status}`, res.status, body)
}
return body as T
}
export type ManualPayPageResponse = {
ok?: boolean
parent?: Record<string, unknown> | null
students?: Array<Record<string, unknown>>
payments?: Array<Record<string, unknown>>
invoices?: Array<Record<string, unknown>>
searchTermUsedInSearch?: string
installmentEndYmd?: string
}
export async function fetchManualPayPage(params: {
search_term: string
page?: number | string
}): Promise<ManualPayPageResponse> {
const qs = new URLSearchParams()
qs.set('search_term', params.search_term)
if (params.page != null) qs.set('page', String(params.page))
return apiFetch(`/api/v1/administrator/payments/manual-pay?${qs}`)
}
type ManualPaySuggestRow = { id?: number | string; label?: string; email?: string; phone?: string }
export async function fetchManualPaySuggest(q: string): Promise<ManualPaySuggestRow[]> {
const qs = new URLSearchParams({ q })
const body = await apiFetch<{ suggestions?: unknown[]; data?: unknown[] }>(
`/api/v1/administrator/payments/manual-pay/suggest?${qs}`,
)
const raw = body.suggestions ?? body.data ?? []
return Array.isArray(raw) ? (raw as ManualPaySuggestRow[]) : []
}
export async function submitManualPayUpdate(formData: FormData): Promise<{ ok?: boolean; message?: string }> {
return postMultipart('/api/v1/administrator/payments/manual-pay/update', formData)
}
export async function submitManualPayEdit(formData: FormData): Promise<{ ok?: boolean; message?: string }> {
return postMultipart('/api/v1/administrator/payments/manual-pay/edit', formData)
}
/** Authenticated URL for embedding or downloading check/card files (legacy CI: serveCheckFile). */
export function manualPayCheckFileUrl(tokenOrKey: string, mode: 'inline' | 'download'): string {
const enc = encodeURIComponent(tokenOrKey)
return `/api/v1/administrator/payments/check-file/${enc}/${mode}`
}
export async function fetchAuthenticatedBlobUrl(apiPath: string): Promise<string> {
const headers = new Headers({ Accept: '*/*' })
const token = getStoredToken()
if (token) headers.set('Authorization', `Bearer ${token}`)
const res = await fetch(apiUrl(apiPath), { headers })
if (!res.ok) throw new ApiHttpError(`HTTP ${res.status}`, res.status, null)
const blob = await res.blob()
return URL.createObjectURL(blob)
}
export type PaymentNotificationLogRow = Record<string, unknown>
export async function fetchPaymentNotificationLogs(params?: {
from?: string
to?: string
type?: string
school_year?: string
semester?: string
}): Promise<{
logs: PaymentNotificationLogRow[]
parentsById?: Record<string, string>
filters?: { from?: string; to?: string; type?: string }
}> {
const qs = new URLSearchParams()
if (params?.from) qs.set('from', params.from)
if (params?.to) qs.set('to', params.to)
if (params?.type) qs.set('type', params.type)
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/notification-logs${suffix}`)
}
export type UnpaidParentRow = Record<string, unknown>
export async function fetchUnpaidParents(params?: {
school_year?: string
semester?: string
}): Promise<{ rows: UnpaidParentRow[]; school_year?: string; schoolYears?: string[] }> {
const qs = new URLSearchParams()
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}`)
}
export async function sendPaymentReminders(payload: {
school_year: string
parent_id?: number | string
return_to?: string
}): Promise<{ ok?: boolean; message?: string }> {
return apiFetch('/api/v1/administrator/payments/notifications/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export type ParentPaymentsModalRow = Record<string, unknown>
export async function fetchParentPaymentsForModal(
parentId: number | string,
): Promise<{ payments: ParentPaymentsModalRow[]; parentName?: string }> {
return apiFetch(
`/api/v1/administrator/payments/parent/${encodeURIComponent(String(parentId))}/payments-list`,
)
}
export async function applyDiscountByCodeForStudent(payload: {
student_id: number | string
voucher_code: string
}): Promise<{ ok?: boolean; message?: string }> {
return apiFetch('/api/v1/discounts/apply-by-code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export type ExtraChargeRow = Record<string, unknown>
export async function fetchExtraChargesPage(params?: {
school_year?: string
semester?: string
page?: number | string
parent_id?: number | string
}): Promise<{
rows: ExtraChargeRow[]
schoolYear?: string
schoolYears?: string[]
semester?: string
parents?: Array<{ id: number; firstname?: string; lastname?: string }>
pager?: { current_page?: number; last_page?: number }
}> {
const qs = new URLSearchParams()
if (params?.school_year) qs.set('school_year', params.school_year)
if (params?.semester) qs.set('semester', params.semester)
if (params?.page != null) qs.set('page', String(params.page))
if (params?.parent_id != null) qs.set('parent_id', String(params.parent_id))
const suffix = qs.toString() ? `?${qs}` : ''
return apiFetch(`/api/v1/administrator/charges${suffix}`)
}
export async function fetchChargeInvoicesForParent(parentId: number | string): Promise<
Array<{ id?: number; invoice_number?: string }>
> {
const body = await apiFetch<{ invoices?: unknown[] }>(
`/api/v1/administrator/charges/invoices-for-parent?parent_id=${encodeURIComponent(String(parentId))}`,
)
const raw = body.invoices
return Array.isArray(raw) ? (raw as Array<{ id?: number; invoice_number?: string }>) : []
}
export async function submitExtraCharge(formData: FormData): Promise<{ ok?: boolean; message?: string }> {
return postMultipart('/api/v1/administrator/charges', formData)
}
export type FinancialDetailedJson = {
ok?: boolean
invoices?: Array<Record<string, unknown>>
payments?: Array<Record<string, unknown>>
refunds?: Array<Record<string, unknown>>
discounts?: Array<Record<string, unknown>>
paymentBreakdown?: Record<string, { cash?: number; credit?: number; check?: number }>
paymentTotals?: {
total_cash?: number
total_credit?: number
total_check?: number
total_all?: number
}
expenses?: Array<{ category?: string; total_amount?: number }>
reimbursements?: Array<{ status?: string; total_amount?: number }>
eventFeesTotal?: number
}
export async function fetchFinancialReportDetailed(params: {
school_year?: string
date_from?: string
date_to?: string
}): Promise<FinancialDetailedJson> {
const qs = new URLSearchParams()
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)
qs.set('format', 'json')
return apiFetch(`/api/v1/administrator/reports/financial-detailed?${qs}`)
}
export async function downloadFinancialReportCsv(params: {
school_year?: string
date_from?: string
date_to?: string
}): Promise<void> {
const qs = new URLSearchParams()
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 headers = new Headers()
const token = getStoredToken()
if (token) headers.set('Authorization', `Bearer ${token}`)
const res = await fetch(apiUrl(path), { headers })
if (!res.ok) throw new ApiHttpError(`HTTP ${res.status}`, res.status, null)
const blob = await res.blob()
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'financial-report.csv'
a.click()
URL.revokeObjectURL(url)
}
export type FinancialSummaryJson = {
ok?: boolean
schoolYear?: string
totalCharges?: number
totalEventFees?: number
totalExtraCharges?: number
totalDiscounts?: number
totalRefunds?: number
totalExpenses?: number
totalReimbursements?: number
donationToSchool?: number
netAmount?: number
amountCollected?: number
totalUnpaid?: number
totalPaid?: number
}
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}`)
}
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 headers = new Headers()
const token = getStoredToken()
if (token) headers.set('Authorization', `Bearer ${token}`)
const res = await fetch(apiUrl(path), { headers })
if (!res.ok) throw new ApiHttpError(`HTTP ${res.status}`, res.status, null)
const blob = await res.blob()
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'financial-summary.pdf'
a.click()
URL.revokeObjectURL(url)
}
export type PaypalRedirectContext = {
parentName?: string
totalAmount?: string | number
schoolId?: string | number
currency?: string
invoiceId?: string | number
}
export async function fetchPaypalPaymentContext(params: {
invoice_id?: string
}): Promise<PaypalRedirectContext> {
const qs = new URLSearchParams()
if (params.invoice_id) qs.set('invoice_id', params.invoice_id)
const suffix = qs.toString() ? `?${qs}` : ''
return apiFetch(`/api/v1/parent/payment/paypal-context${suffix}`)
}
+91
View File
@@ -0,0 +1,91 @@
/**
* Teacher / admin print copy requests (legacy CI: print-requests/*).
*/
import { apiUrl } from '../lib/apiOrigin'
import { ApiHttpError, apiFetch, getStoredToken } from './http'
export type PrintRequestRow = Record<string, unknown>
export type TeacherPrintRequestsContext = {
sundays?: string[]
times?: string[]
class_id?: number | null
class_section_id?: number | null
class_section_name?: string | null
print_requests?: PrintRequestRow[]
}
export async function fetchTeacherPrintRequestsContext(): Promise<TeacherPrintRequestsContext> {
return apiFetch('/api/v1/teacher/print-requests/context')
}
async function multipartRequest<T>(path: string, formData: FormData, method = 'POST'): 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, headers, body: formData })
const body: unknown = await res.json().catch(() => null)
if (!res.ok) {
const msg =
typeof body === 'object' && body !== null && 'message' in body
? String((body as { message?: unknown }).message ?? '')
: ''
throw new ApiHttpError(msg || `HTTP ${res.status}`, res.status, body)
}
return body as T
}
export async function createPrintRequest(formData: FormData): Promise<{ ok?: boolean; message?: string }> {
return multipartRequest('/api/v1/print-requests', formData)
}
export async function createCopyRequest(
formData: FormData,
): Promise<{ ok?: boolean; message?: string }> {
return multipartRequest('/api/v1/print-requests/hand-copy', formData)
}
export async function updatePrintRequest(
requestId: number | string,
formData: FormData,
): Promise<{ ok?: boolean; message?: string }> {
formData.set('_method', 'PATCH')
return multipartRequest(`/api/v1/print-requests/${encodeURIComponent(String(requestId))}`, formData)
}
export async function deletePrintRequest(requestId: number | string): Promise<{ ok?: boolean }> {
return apiFetch(`/api/v1/print-requests/${encodeURIComponent(String(requestId))}`, {
method: 'DELETE',
})
}
export async function fetchAdminPrintRequests(): Promise<{ print_requests: PrintRequestRow[] }> {
return apiFetch('/api/v1/administrator/print-requests')
}
export async function adminUpdatePrintRequestStatus(
requestId: number | string,
payload: { status: string; admin_id?: number | string },
): Promise<{ ok?: boolean; message?: string }> {
return apiFetch(`/api/v1/administrator/print-requests/${encodeURIComponent(String(requestId))}/status`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
/** Authenticated download URL for uploaded job files (opened in new tab). */
export function printRequestFileUrl(requestId: number | string): string {
return `/api/v1/print-requests/${encodeURIComponent(String(requestId))}/file`
}
export async function openPrintRequestFile(requestId: number | string): Promise<void> {
const token = getStoredToken()
const headers = new Headers({ Accept: '*/*' })
if (token) headers.set('Authorization', `Bearer ${token}`)
const res = await fetch(apiUrl(printRequestFileUrl(requestId)), { headers })
if (!res.ok) throw new ApiHttpError(`HTTP ${res.status}`, res.status, null)
const blob = await res.blob()
const url = URL.createObjectURL(blob)
window.open(url, '_blank', 'noopener')
}
+250
View File
@@ -0,0 +1,250 @@
/**
* Stickers, badges, report cards (legacy CI printables_reports/*).
*/
import { apiUrl } from '../lib/apiOrigin'
import { ApiHttpError, apiFetch, getStoredToken } from './http'
export type ClassOption = { class_section_id?: number; id?: number; class_section_name?: string }
export type StudentOption = {
id: number | string
firstname?: string
lastname?: string
registration_grade?: string
gender?: string
}
export async function fetchStickerFormOptions(params?: {
school_year?: string
semester?: string
}): Promise<{
classes?: ClassOption[]
students?: StudentOption[]
}> {
const qs = new URLSearchParams()
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/printables/stickers/form-options${suffix}`)
}
export async function fetchStudentsByClass(classId: number | string): Promise<StudentOption[]> {
const body = await apiFetch<{ students?: StudentOption[] } | StudentOption[]>(
`/api/v1/administrator/students/by-class/${encodeURIComponent(String(classId))}`,
)
if (Array.isArray(body)) return body
return body.students ?? []
}
export async function postStickerGenerate(formData: FormData): Promise<Blob> {
const headers = new Headers({ Accept: 'application/pdf,*/*' })
const token = getStoredToken()
if (token) headers.set('Authorization', `Bearer ${token}`)
const res = await fetch(apiUrl('/api/v1/administrator/printables/stickers/generate'), {
method: 'POST',
headers,
body: formData,
})
if (!res.ok) {
const errBody: unknown = await res.json().catch(() => null)
const msg =
typeof errBody === 'object' && errBody !== null && 'message' in errBody
? String((errBody as { message?: unknown }).message ?? '')
: ''
throw new ApiHttpError(msg || `HTTP ${res.status}`, res.status, errBody)
}
return res.blob()
}
export async function postBadgeGenerate(formData: FormData): Promise<Blob> {
const headers = new Headers({ Accept: 'application/pdf,*/*' })
const token = getStoredToken()
if (token) headers.set('Authorization', `Bearer ${token}`)
const res = await fetch(apiUrl('/api/v1/administrator/printables/badges/generate'), {
method: 'POST',
headers,
body: formData,
})
if (!res.ok) {
const errBody: unknown = await res.json().catch(() => null)
const msg =
typeof errBody === 'object' && errBody !== null && 'message' in errBody
? String((errBody as { message?: unknown }).message ?? '')
: ''
throw new ApiHttpError(msg || `HTTP ${res.status}`, res.status, errBody)
}
return res.blob()
}
export type StickerPreviewCounts = {
students?: Array<{ student_id: number | string; primary_count?: number }>
totals?: { stickers?: number; students?: number }
}
export async function fetchStickerPreviewCounts(classId?: number | string | null): Promise<StickerPreviewCounts> {
const qs = new URLSearchParams()
if (classId) qs.set('class_id', String(classId))
const suffix = qs.toString() ? `?${qs}` : ''
const body = await apiFetch<
StickerPreviewCounts & { status?: string; data?: StickerPreviewCounts }
>(`/api/v1/administrator/printables/stickers/preview${suffix}`)
if (body.data) return body.data
return body
}
export type BadgeUserRow = Record<string, unknown>
export async function fetchBadgeFormOptions(params?: {
school_year?: string
semester?: string
}): Promise<{
selectedYear?: string
users?: BadgeUserRow[]
}> {
const qs = new URLSearchParams()
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/printables/badges/form-options${suffix}`)
}
export async function fetchBadgePrintStatus(params: {
user_ids: (number | string)[]
school_year?: string
}): Promise<{
ok?: boolean
data?: Record<string, { count?: number }>
csrf_token?: string
csrf_hash?: string
}> {
const qs = new URLSearchParams()
for (const id of params.user_ids) qs.append('user_ids[]', String(id))
if (params.school_year) qs.set('school_year', params.school_year)
return apiFetch(`/api/v1/administrator/printables/badges/status?${qs}`)
}
export type ReportCardMeta = {
ok?: boolean
schoolYears?: string[]
semesters?: string[]
selectedYear?: string
selectedSemester?: string
students?: Array<{ id: number | string; firstname?: string; lastname?: string; class_section_name?: string }>
classSections?: Array<{ class_section_id?: number; id?: number; class_section_name?: string }>
}
export async function fetchReportCardMeta(params?: {
school_year?: string
semester?: string
}): Promise<ReportCardMeta> {
const qs = new URLSearchParams()
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/printables/report-card/meta${suffix}`)
}
export async function fetchReportCardAck(params: {
student_id: number | string
school_year?: string
semester?: string
}): Promise<{ ok?: boolean; viewed_at?: string; signed_at?: string; signed_name?: string }> {
const qs = new URLSearchParams()
qs.set('student_id', String(params.student_id))
if (params.school_year) qs.set('school_year', params.school_year)
if (params.semester) qs.set('semester', params.semester ?? '')
return apiFetch(`/api/v1/administrator/printables/report-card/ack?${qs}`)
}
export async function fetchReportCardCompleteness(params: {
class_section_id: number | string
school_year?: string
semester?: string
}): Promise<{
ok?: boolean
students?: Array<Record<string, unknown>>
summary?: Record<string, unknown>
used_fallback?: boolean
}> {
const qs = new URLSearchParams()
qs.set('class_section_id', String(params.class_section_id))
if (params.school_year) qs.set('school_year', params.school_year ?? '')
if (params.semester) qs.set('semester', params.semester ?? '')
return apiFetch(`/api/v1/administrator/printables/report-card/completeness?${qs}`)
}
/** Build absolute API URL for iframe / window.open (report PDF HTML). */
export function reportCardStudentUrl(
studentId: number | string,
opts: { school_year: string; semester?: string; download?: boolean; report_date?: string },
): string {
const qs = new URLSearchParams()
qs.set('school_year', opts.school_year)
if (opts.semester) qs.set('semester', opts.semester)
if (opts.download) qs.set('download', '1')
if (opts.report_date) qs.set('report_date', opts.report_date)
qs.set('cb', String(Date.now()))
return apiUrl(`/api/v1/reports/report-cards/students/${encodeURIComponent(String(studentId))}?${qs}`)
}
export function reportCardClassUrl(
classSectionId: number | string,
opts: { school_year: string; semester?: string; download?: boolean; report_date?: string },
): string {
const qs = new URLSearchParams()
qs.set('school_year', opts.school_year)
if (opts.semester) qs.set('semester', opts.semester)
if (opts.download) qs.set('download', '1')
if (opts.report_date) qs.set('report_date', opts.report_date)
qs.set('cb', String(Date.now()))
return apiUrl(`/api/v1/reports/report-cards/classes/${encodeURIComponent(String(classSectionId))}?${qs}`)
}
/** HTML document for iframe preview (Bearer auth; unlike raw iframe src). */
export async function fetchReportCardStudentHtml(
studentId: number | string,
opts: { school_year: string; semester?: string; report_date?: string },
): Promise<string> {
const qs = new URLSearchParams()
qs.set('school_year', opts.school_year)
if (opts.semester) qs.set('semester', opts.semester)
if (opts.report_date) qs.set('report_date', opts.report_date)
qs.set('cb', String(Date.now()))
const headers = new Headers({ Accept: 'text/html' })
const token = getStoredToken()
if (token) headers.set('Authorization', `Bearer ${token}`)
const path = `/api/v1/reports/report-cards/students/${encodeURIComponent(String(studentId))}?${qs}`
const res = await fetch(apiUrl(path), { headers })
if (!res.ok) throw new ApiHttpError(`HTTP ${res.status}`, res.status, null)
return res.text()
}
export async function fetchReportCardClassHtml(
classSectionId: number | string,
opts: { school_year: string; semester?: string; report_date?: string },
): Promise<string> {
const qs = new URLSearchParams()
qs.set('school_year', opts.school_year)
if (opts.semester) qs.set('semester', opts.semester)
if (opts.report_date) qs.set('report_date', opts.report_date)
qs.set('cb', String(Date.now()))
const headers = new Headers({ Accept: 'text/html' })
const token = getStoredToken()
if (token) headers.set('Authorization', `Bearer ${token}`)
const path = `/api/v1/reports/report-cards/classes/${encodeURIComponent(String(classSectionId))}?${qs}`
const res = await fetch(apiUrl(path), { headers })
if (!res.ok) throw new ApiHttpError(`HTTP ${res.status}`, res.status, null)
return res.text()
}
/** Opens PDF download in new tab (authenticated GET). */
export async function openReportCardDownload(url: string): Promise<void> {
const headers = new Headers({ Accept: 'application/pdf,*/*' })
const token = getStoredToken()
if (token) headers.set('Authorization', `Bearer ${token}`)
const res = await fetch(url, { headers })
if (!res.ok) throw new ApiHttpError(`HTTP ${res.status}`, res.status, null)
const blob = await res.blob()
const r = URL.createObjectURL(blob)
window.open(r, '_blank', 'noopener')
}
+47
View File
@@ -0,0 +1,47 @@
/**
* Admin refunds list (legacy CI `refunds/list.php`).
* Laravel: `GET /api/v1/administrator/refunds/list` with JWT.
*/
import { apiFetch } from './http'
const BASE = '/api/v1/administrator/refunds'
export type RefundListRow = Record<string, unknown>
export type RefundsListResponse = {
refunds: RefundListRow[]
school_year?: string | null
semester?: string | null
school_years?: string[]
}
function normalizeListPayload(raw: unknown): RefundsListResponse {
const body = raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {}
const inner =
'data' in body && body.data && typeof body.data === 'object'
? (body.data as Record<string, unknown>)
: body
const list = inner.refunds ?? inner.rows ?? []
return {
refunds: Array.isArray(list) ? (list as RefundListRow[]) : [],
school_year: inner.school_year != null ? String(inner.school_year) : undefined,
semester: inner.semester != null ? String(inner.semester) : undefined,
school_years: Array.isArray(inner.school_years)
? (inner.school_years as string[])
: undefined,
}
}
export async function fetchRefundsList(params?: {
school_year?: string
semester?: string
page?: number | string
}): Promise<RefundsListResponse> {
const qs = new URLSearchParams()
if (params?.school_year) qs.set('school_year', params.school_year)
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}`)
return normalizeListPayload(body)
}
+194
View File
@@ -0,0 +1,194 @@
/**
* Admin reimbursements (legacy CI reimbursements/*).
* Backend is expected under `/api/v1/administrator/reimbursements`.
*/
import { apiUrl } from '../lib/apiOrigin'
import { ApiHttpError, apiFetch, getStoredToken } from './http'
const BASE = '/api/v1/administrator/reimbursements'
export type ReimbursementUserOption = { id?: number | string; firstname?: string; lastname?: string }
export type BatchSummaryRow = Record<string, unknown>
export type BatchDetailPayload = {
items?: Array<Record<string, unknown>>
checks?: Array<Record<string, unknown>>
}
export type ReimbursementsIndexResponse = {
schoolYears?: string[]
users?: ReimbursementUserOption[]
batchSummaries?: BatchSummaryRow[]
batchDetails?: Record<string, BatchDetailPayload>
donationBatch?: BatchSummaryRow | null
donationDetails?: BatchDetailPayload
batchAttachments?: Record<string, { receipts?: unknown[]; checks?: unknown[] }>
}
export async function fetchReimbursementsIndex(params?: {
semester?: string
status?: string
school_year?: string
user_id?: string
}): Promise<ReimbursementsIndexResponse> {
const qs = new URLSearchParams()
if (params?.semester) qs.set('semester', params.semester)
if (params?.status) qs.set('status', params.status)
if (params?.school_year) qs.set('school_year', params.school_year)
if (params?.user_id) qs.set('user_id', params.user_id)
const suffix = qs.toString() ? `?${qs}` : ''
return apiFetch(`${BASE}${suffix}`)
}
/** Opens CSV download in a new tab (authenticated GET → blob URL). */
export async function downloadReimbursementsExport(params?: {
semester?: string
status?: string
school_year?: string
user_id?: string
type?: string
}): Promise<void> {
const qs = new URLSearchParams()
if (params?.semester) qs.set('semester', params.semester)
if (params?.status) qs.set('status', params.status)
if (params?.school_year) qs.set('school_year', params.school_year)
if (params?.user_id) qs.set('user_id', params.user_id)
if (params?.type) qs.set('type', params.type)
const url = apiUrl(`${BASE}/export?${qs}`)
const headers = new Headers({ Accept: 'text/csv,*/*' })
const token = getStoredToken()
if (token) headers.set('Authorization', `Bearer ${token}`)
const res = await fetch(url, { headers })
if (!res.ok) throw new ApiHttpError(`HTTP ${res.status}`, res.status, null)
const blob = await res.blob()
window.open(URL.createObjectURL(blob), '_blank', 'noopener')
}
export async function downloadBatchCsv(batchId: number | string): Promise<void> {
const qs = new URLSearchParams({ batch_id: String(batchId) })
const url = apiUrl(`${BASE}/batch/export?${qs}`)
const headers = new Headers({ Accept: 'text/csv,*/*' })
const token = getStoredToken()
if (token) headers.set('Authorization', `Bearer ${token}`)
const res = await fetch(url, { headers })
if (!res.ok) throw new ApiHttpError(`HTTP ${res.status}`, res.status, null)
const blob = await res.blob()
window.open(URL.createObjectURL(blob), '_blank', 'noopener')
}
export async function postBatchEmail(formData: FormData): Promise<{ success?: boolean; message?: string }> {
const headers = new Headers({ Accept: 'application/json' })
const token = getStoredToken()
if (token) headers.set('Authorization', `Bearer ${token}`)
const res = await fetch(apiUrl(`${BASE}/batch/send`), { method: 'POST', headers, body: formData })
const body: unknown = await res.json().catch(() => null)
if (!res.ok) {
const msg =
typeof body === 'object' && body !== null && 'error' in body
? String((body as { error?: unknown }).error ?? '')
: ''
throw new ApiHttpError(msg || `HTTP ${res.status}`, res.status, body)
}
return body as { success?: boolean; message?: string }
}
export type UnderProcessingWorkspace = {
pendingItems?: Array<Record<string, unknown>>
itemsPayload?: Array<Record<string, unknown>>
existingBatches?: Array<Record<string, unknown>>
adminUsers?: Array<{ id?: number; firstname?: string; lastname?: string }>
}
export async function fetchUnderProcessingWorkspace(): Promise<UnderProcessingWorkspace> {
return apiFetch(`${BASE}/under-processing`)
}
export async function createReimbursementBatch(): Promise<{
success?: boolean
batch_id?: number
sequence?: number
yearly_number?: number
csrf_hash?: string
}> {
const fd = new FormData()
return postFormExpectJson(`${BASE}/batch/create`, fd)
}
async function postFormExpectJson(path: string, form: FormData): Promise<{ success?: boolean; error?: string; csrf_hash?: string; reimbursement_id?: number } & Record<string, unknown>> {
const headers = new Headers({ Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' })
const token = getStoredToken()
if (token) headers.set('Authorization', `Bearer ${token}`)
const res = await fetch(apiUrl(path), { method: 'POST', headers, body: form })
const body: unknown = await res.json().catch(() => ({}))
const data = body as Record<string, unknown>
if (!res.ok) {
throw new ApiHttpError(String(data.error ?? `HTTP ${res.status}`), res.status, body)
}
if (data && typeof data === 'object' && 'success' in data && data.success === false) {
throw new ApiHttpError(String(data.error ?? 'Request failed'), res.status, body)
}
return data as { success?: boolean; error?: string; csrf_hash?: string; reimbursement_id?: number }
}
export function postBatchUpdate(form: FormData) {
return postFormExpectJson(`${BASE}/batch/update`, form)
}
export function postBatchLock(form: FormData) {
return postFormExpectJson(`${BASE}/batch/lock`, form)
}
export function postAdminCheckUpload(form: FormData) {
return postFormExpectJson(`${BASE}/batch/admin-file/upload`, form)
}
export function postMarkDonation(form: FormData) {
return postFormExpectJson(`${BASE}/mark-donation`, form)
}
export type ReimbursementFormContext = {
users?: ReimbursementUserOption[]
expense_id?: number | string
prefill_amount?: number | string
}
export async function fetchReimbursementCreateContext(search: {
expense_id?: string
}): Promise<ReimbursementFormContext> {
const qs = new URLSearchParams()
if (search.expense_id) qs.set('expense_id', search.expense_id)
const suffix = qs.toString() ? `?${qs}` : ''
return apiFetch(`${BASE}/create-context${suffix}`)
}
async function postMultipart(path: string, formData: FormData): Promise<{ ok?: boolean; message?: string }> {
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', headers, body: formData })
const body: unknown = await res.json().catch(() => null)
if (!res.ok) {
const msg =
typeof body === 'object' && body !== null && 'message' in body
? String((body as { message?: unknown }).message ?? '')
: ''
throw new ApiHttpError(msg || `HTTP ${res.status}`, res.status, body)
}
return (body ?? {}) as { ok?: boolean; message?: string }
}
export async function createReimbursement(formData: FormData) {
return postMultipart(`${BASE}`, formData)
}
export async function fetchReimbursementForEdit(id: number | string): Promise<{
reimbursement?: Record<string, unknown>
users?: ReimbursementUserOption[]
receipt_url?: string | null
}> {
return apiFetch(`${BASE}/${encodeURIComponent(String(id))}/edit-data`)
}
export async function updateReimbursement(id: number | string, formData: FormData) {
return postMultipart(`${BASE}/${encodeURIComponent(String(id))}`, formData)
}
+50
View File
@@ -0,0 +1,50 @@
import { apiFetch } from './http'
import type { ApiEnvelope } from './types'
/**
* Laravel `GET /api/v1/reports/combined` with JWT Bearer auth.
* If your `routes/api.php` registers this elsewhere, change the path here.
*/
export type CombinedReportPayload = {
title?: string
subtitle?: string
html?: string
message?: string
columns?: string[]
rows?: Array<Record<string, unknown>>
sections?: Array<{
title?: string
heading?: string
rows?: Array<Record<string, unknown>>
columns?: string[]
}>
[key: string]: unknown
}
export function unwrapCombinedReport(raw: unknown): CombinedReportPayload | null {
if (raw == null || typeof raw !== 'object') return null
const o = raw as ApiEnvelope<CombinedReportPayload> & CombinedReportPayload
if (o.data != null && typeof o.data === 'object') {
return o.data as CombinedReportPayload
}
return raw as CombinedReportPayload
}
export async function fetchCombinedReport(params?: {
school_year?: string | null
semester?: string | null
student_id?: string | number | null
class_section_id?: string | number | null
}): Promise<unknown> {
const q = new URLSearchParams()
if (params?.school_year) q.set('school_year', String(params.school_year))
if (params?.semester) q.set('semester', String(params.semester))
if (params?.student_id != null && String(params.student_id).trim() !== '') {
q.set('student_id', String(params.student_id))
}
if (params?.class_section_id != null && String(params.class_section_id).trim() !== '') {
q.set('class_section_id', String(params.class_section_id))
}
const qs = q.size > 0 ? `?${q.toString()}` : ''
return apiFetch<unknown>(`/api/v1/reports/combined${qs}`)
}
+17
View File
@@ -0,0 +1,17 @@
import { apiFetch } from './http'
/**
* Laravel `GET /api/v1/rfid/coming-soon` with JWT Bearer auth.
* Adjust the path if your `routes/api.php` uses a different segment (e.g. under `administrator`).
*/
export type RfidComingSoonPayload = {
title?: string
subtitle?: string
message?: string
/** Extra keys from the API are ignored by the UI unless added explicitly. */
[key: string]: unknown
}
export async function fetchRfidComingSoon(): Promise<RfidComingSoonPayload> {
return apiFetch<RfidComingSoonPayload>('/api/v1/rfid/coming-soon')
}
+261
View File
@@ -0,0 +1,261 @@
/**
* Roles & permissions admin (legacy CI rolepermission/*).
* Expected under `/api/v1/role-permission`.
*/
import { apiFetch } from './http'
const BASE = '/api/v1/role-permission'
function unwrapData<T>(body: T): T {
if (!body || typeof body !== 'object') return body
const maybeEnvelope = body as { data?: unknown }
return (maybeEnvelope.data as T | undefined) ?? body
}
export type RoleRow = {
id?: number | string
name?: string
slug?: string
description?: string
dashboard_route?: string
priority?: number
is_active?: number | string | boolean
user_count?: number
}
export type PermissionRow = {
id?: number | string
name?: string
description?: string
created_at?: string
updated_at?: string
}
export async function fetchRolesList(): Promise<{ roles?: RoleRow[] }> {
const body = await apiFetch<{ roles?: RoleRow[] } | { data?: { roles?: RoleRow[] } }>(`${BASE}/roles`)
return unwrapData(body)
}
export async function fetchPermissionsList(): Promise<{ permissions?: PermissionRow[] }> {
const body = await apiFetch<
{ permissions?: PermissionRow[] } | { data?: { permissions?: PermissionRow[] } }
>(`${BASE}/permissions`)
return unwrapData(body)
}
export type AssignUserRow = {
id?: number | string
account_id?: string
firstname?: string
lastname?: string
email?: string
roles?: string[]
}
export async function fetchUsersForAssign(params?: {
school_year?: string
semester?: string
page?: number
per_page?: number
}): Promise<{ users?: AssignUserRow[]; meta?: { current_page?: number; last_page?: number; total?: number; per_page?: number } }> {
const qs = new URLSearchParams()
if (params?.school_year) qs.set('school_year', params.school_year)
if (params?.semester) qs.set('semester', params.semester)
qs.set('per_page', String(params?.per_page ?? 200))
if (params?.page) qs.set('page', String(params.page))
qs.set('sort_by', 'lastname')
qs.set('sort_dir', 'asc')
const suffix = qs.toString() ? `?${qs}` : ''
const body = await apiFetch<
| {
users?: AssignUserRow[]
meta?: { current_page?: number; last_page?: number; total?: number; per_page?: number }
}
| {
data?: {
users?: AssignUserRow[]
meta?: { current_page?: number; last_page?: number; total?: number; per_page?: number }
}
}
>(
`${BASE}/users${suffix}`,
)
return unwrapData(body)
}
export async function fetchAllUsersForAssign(params?: {
school_year?: string
semester?: string
}): Promise<{ users: AssignUserRow[] }> {
const usersById = new Map<string, AssignUserRow>()
let page = 1
let lastPage = 1
do {
const response = await fetchUsersForAssign({
school_year: params?.school_year,
semester: params?.semester,
page,
per_page: 200,
})
for (const user of response.users ?? []) {
const key = String(user.id ?? '').trim()
if (key === '') {
continue
}
if (!usersById.has(key)) {
usersById.set(key, user)
}
}
lastPage = Number(response.meta?.last_page ?? page)
page += 1
} while (page <= lastPage)
return { users: [...usersById.values()] }
}
export async function saveUserRoles(payload: {
user_id: number | string
role_ids: (number | string)[]
}): Promise<{ ok?: boolean; message?: string }> {
const userId = encodeURIComponent(String(payload.user_id))
const body = await apiFetch<{ ok?: boolean; message?: string } | { data?: { ok?: boolean; message?: string } }>(
`${BASE}/users/${userId}/roles`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role_ids: payload.role_ids }),
},
)
return unwrapData(body)
}
export async function createRole(payload: Record<string, unknown>): Promise<{ ok?: boolean; message?: string }> {
const body = await apiFetch<{ ok?: boolean; message?: string } | { data?: { ok?: boolean; message?: string } }>(
`${BASE}/roles`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
)
return unwrapData(body)
}
export async function updateRole(
roleId: number | string,
payload: Record<string, unknown>,
): Promise<{ ok?: boolean; message?: string }> {
const body = await apiFetch<{ ok?: boolean; message?: string } | { data?: { ok?: boolean; message?: string } }>(
`${BASE}/roles/${encodeURIComponent(String(roleId))}`,
{
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
)
return unwrapData(body)
}
export async function deleteRole(roleId: number | string): Promise<{ ok?: boolean }> {
const body = await apiFetch<{ ok?: boolean } | { data?: { ok?: boolean } }>(
`${BASE}/roles/${encodeURIComponent(String(roleId))}`,
{ method: 'DELETE' },
)
return unwrapData(body)
}
export async function fetchRole(roleId: number | string): Promise<{ role?: RoleRow }> {
const body = await apiFetch<{ role?: RoleRow } | { data?: { role?: RoleRow } }>(
`${BASE}/roles/${encodeURIComponent(String(roleId))}`,
)
return unwrapData(body)
}
export async function createPermission(payload: {
name: string
description?: string
}): Promise<{ ok?: boolean; message?: string }> {
const body = await apiFetch<{ ok?: boolean; message?: string } | { data?: { ok?: boolean; message?: string } }>(
`${BASE}/permissions`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
)
return unwrapData(body)
}
export async function fetchPermission(
permissionId: number | string,
): Promise<{ permission?: PermissionRow }> {
const body = await apiFetch<{ permission?: PermissionRow } | { data?: { permission?: PermissionRow } }>(
`${BASE}/permissions/${encodeURIComponent(String(permissionId))}`,
)
return unwrapData(body)
}
export async function updatePermission(
permissionId: number | string,
payload: { name: string; description?: string },
): Promise<{ ok?: boolean; message?: string }> {
const body = await apiFetch<{ ok?: boolean; message?: string } | { data?: { ok?: boolean; message?: string } }>(
`${BASE}/permissions/${encodeURIComponent(String(permissionId))}`,
{
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
)
return unwrapData(body)
}
export type PermissionMatrixRow = {
id?: number | string
name?: string
}
export type ExistingPermissionFlags = {
can_create?: boolean
can_read?: boolean
can_update?: boolean
can_delete?: boolean
}
export async function fetchRolePermissionMatrix(roleId: number | string): Promise<{
roleId?: number | string
permissions?: PermissionMatrixRow[]
existing?: Record<string, ExistingPermissionFlags>
}> {
const body = await apiFetch<
| {
roleId?: number | string
permissions?: PermissionMatrixRow[]
existing?: Record<string, ExistingPermissionFlags>
}
| {
data?: {
roleId?: number | string
permissions?: PermissionMatrixRow[]
existing?: Record<string, ExistingPermissionFlags>
}
}
>(`${BASE}/roles/${encodeURIComponent(String(roleId))}/permissions`)
return unwrapData(body)
}
export async function saveRolePermissionMatrix(
roleId: number | string,
payload: Record<string, { create?: boolean; read?: boolean; update?: boolean; delete?: boolean }>,
): Promise<{ ok?: boolean; message?: string }> {
const body = await apiFetch<{ ok?: boolean; message?: string } | { data?: { ok?: boolean; message?: string } }>(
`${BASE}/roles/${encodeURIComponent(String(roleId))}/permissions`,
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ permissions: payload }),
},
)
return unwrapData(body)
}
+24
View File
@@ -0,0 +1,24 @@
/**
* Score analysis / prediction (legacy CI score_analysis/score_prediction).
*/
import { apiFetch } from './http'
export type ScorePredictionStudent = Record<string, unknown>
export type ScorePredictionResponse = {
school_year?: string
selectedClassSectionId?: string | number | null
classSections?: Array<{ class_section_id?: number | string; class_section_name?: string }>
students?: ScorePredictionStudent[]
}
export async function fetchScorePrediction(params?: {
school_year?: string
class_section_id?: string
}): Promise<ScorePredictionResponse> {
const qs = new URLSearchParams()
if (params?.school_year) qs.set('school_year', params.school_year)
if (params?.class_section_id) qs.set('class_section_id', params.class_section_id)
const suffix = qs.toString() ? `?${qs}` : ''
return apiFetch(`/api/v1/administrator/score-analysis/score-prediction${suffix}`)
}
+899 -3
View File
@@ -1,9 +1,15 @@
import { apiUrl } from '../lib/apiOrigin'
import { apiFetch, getStoredToken } from './http'
import { ApiHttpError, apiFetch, getStoredToken } from './http'
import type {
AdministratorAbsenceFormResponse,
AdministratorAbsenceSubmitResponse,
AdministratorAdminAttendanceResponse,
AttendanceCommentTemplatesResponse,
AttendanceViolationsResponse,
EarlyDismissalsResponse,
ParentAttendanceAdminReportRow,
StaffMonthlyOverviewPayload,
TeacherStaffAttendanceResponse,
AdministratorDailyAttendanceResponse,
AdministratorDashboardMetricsResponse,
AdministratorDashboardSearchResponse,
@@ -67,7 +73,25 @@ import type {
TeacherSubmissionsResponse,
EmergencyContactGroupsResponse,
EmergencyContactShowResponse,
UserListRow,
UsersResponse,
ClassPrepIndexResponse,
ClassPrepPrintResponse,
CommunicationsComposeResponse,
CommunicationsPreviewResponse,
ConfigurationListResponse,
DiscountApplyContextResponse,
DiscountsListResponse,
FamilyListItem,
GuardianListItem,
RegisteredNewStudentsResponse,
EnrollmentWithdrawalPageResponse,
ExpensesListResponse,
ExpenseCreateFormResponse,
ExpenseEditFormResponse,
FamilyLegacyImportMetaResponse,
FamilyLegacyImportResult,
FamilySearchResponse,
} from './types'
export async function loginRequest(
@@ -200,6 +224,251 @@ export async function saveAdministratorAdminAttendance(payload: {
})
}
export async function fetchTeacherStaffAttendance(params?: {
date?: string | null
semester?: string | null
schoolYear?: string | null
classSectionId?: number | null
}): Promise<TeacherStaffAttendanceResponse> {
const query = new URLSearchParams()
if (params?.date) query.set('date', params.date)
if (params?.semester) query.set('semester', params.semester)
if (params?.schoolYear) query.set('school_year', params.schoolYear)
if (params?.classSectionId != null && params.classSectionId > 0) {
query.set('class_section_id', String(params.classSectionId))
}
const qs = query.size > 0 ? `?${query.toString()}` : ''
return apiFetch<TeacherStaffAttendanceResponse>(`/api/v1/attendance/staff/teachers${qs}`)
}
export async function saveTeacherStaffAttendance(payload: {
date: string
semester: string
school_year: string
class_section_id: number
teachers: Record<
string,
{
status?: string | null
reason?: string | null
}
>
}): Promise<{ message?: string }> {
return apiFetch<{ message?: string }>('/api/v1/attendance/staff/teachers/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export async function fetchStaffMonthlyAttendanceOverview(params?: {
semester?: string | null
schoolYear?: string | null
}): Promise<StaffMonthlyOverviewPayload> {
const query = new URLSearchParams()
if (params?.semester) query.set('semester', params.semester)
if (params?.schoolYear) query.set('school_year', params.schoolYear)
const qs = query.size > 0 ? `?${query.toString()}` : ''
return apiFetch<StaffMonthlyOverviewPayload>(`/api/v1/attendance/staff/month-overview${qs}`)
}
/** Matches CI `teacher_attendance_month` save cell (form body). */
export async function saveStaffMonthlyAttendanceCell(payload: Record<string, string>): Promise<{
ok?: boolean
csrf_token?: string
csrf_hash?: string
}> {
const body = new URLSearchParams()
for (const [k, v] of Object.entries(payload)) body.set(k, v)
return apiFetch('/api/v1/attendance/staff/month-save', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
})
}
export async function fetchAttendanceViolationsPending(params?: {
schoolYear?: string | null
semester?: string | null
}): Promise<AttendanceViolationsResponse> {
const query = new URLSearchParams()
if (params?.schoolYear) query.set('school_year', params.schoolYear)
if (params?.semester) query.set('semester', params.semester)
const qs = query.size > 0 ? `?${query.toString()}` : ''
return apiFetch<AttendanceViolationsResponse>(`/api/v1/attendance/violations/pending${qs}`)
}
export async function fetchAttendanceViolationsNotified(params?: {
schoolYear?: string | null
semester?: string | null
}): Promise<AttendanceViolationsResponse> {
const query = new URLSearchParams()
if (params?.schoolYear) query.set('school_year', params.schoolYear)
if (params?.semester) query.set('semester', params.semester)
const qs = query.size > 0 ? `?${query.toString()}` : ''
return apiFetch<AttendanceViolationsResponse>(`/api/v1/attendance/violations/notified${qs}`)
}
export async function fetchParentAttendanceReportsAdmin(params?: {
schoolYear?: string | null
semester?: string | null
start?: string | null
end?: string | null
}): Promise<{ rows?: ParentAttendanceAdminReportRow[] }> {
const query = new URLSearchParams()
if (params?.schoolYear) query.set('school_year', params.schoolYear)
if (params?.semester) query.set('semester', params.semester)
if (params?.start) query.set('start', params.start)
if (params?.end) query.set('end', params.end)
const qs = query.size > 0 ? `?${query.toString()}` : ''
return apiFetch<{ rows?: ParentAttendanceAdminReportRow[] }>(
`/api/v1/attendance/parent-reports${qs}`,
)
}
export async function fetchEarlyDismissals(params?: {
schoolYear?: string | null
semester?: string | null
}): Promise<EarlyDismissalsResponse> {
const query = new URLSearchParams()
if (params?.schoolYear) query.set('school_year', params.schoolYear)
if (params?.semester) query.set('semester', params.semester)
const qs = query.size > 0 ? `?${query.toString()}` : ''
return apiFetch<EarlyDismissalsResponse>(`/api/v1/attendance/early-dismissals${qs}`)
}
export async function fetchEarlyDismissalStudentOptions(): Promise<{
students?: Array<{
id?: number
firstname?: string
lastname?: string
class_section_name?: string | null
}>
}> {
return apiFetch('/api/v1/attendance/early-dismissals/student-options')
}
export async function createEarlyDismissal(payload: {
student_id: number
date: string
dismiss_time: string
reason?: string | null
}): Promise<{ message?: string }> {
return apiFetch('/api/v1/attendance/early-dismissals', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
/** Multipart signature upload per report date — matches CI `attendance/early-dismissals/signature`. */
export async function uploadEarlyDismissalSignature(formData: FormData): Promise<{ message?: string }> {
const token = getStoredToken()
const headers = new Headers({ Accept: 'application/json' })
if (token) headers.set('Authorization', `Bearer ${token}`)
const res = await fetch(apiUrl('/api/v1/attendance/early-dismissals/signature'), {
method: 'POST',
body: formData,
headers,
})
const body: unknown = await res.json().catch(() => null)
if (!res.ok) {
const msg =
typeof body === 'object' && body !== null && 'message' in body
? String((body as { message?: unknown }).message ?? '')
: ''
throw new Error(msg || `HTTP ${res.status}`)
}
return body as { message?: string }
}
export async function fetchAttendanceCommentTemplates(): Promise<AttendanceCommentTemplatesResponse> {
return apiFetch<AttendanceCommentTemplatesResponse>('/api/v1/attendance-templates')
}
export async function saveAttendanceCommentTemplate(payload: Record<string, string>): Promise<{
status?: string
}> {
const body = new URLSearchParams()
for (const [k, v] of Object.entries(payload)) body.set(k, v)
return apiFetch('/api/v1/attendance-templates/save', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
})
}
export async function deleteAttendanceCommentTemplate(id: number): Promise<{ status?: string }> {
const body = new URLSearchParams()
body.set('id', String(id))
return apiFetch('/api/v1/attendance-templates/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
})
}
export async function saveAttendanceViolationNote(payload: Record<string, string>): Promise<{ message?: string }> {
const body = new URLSearchParams()
for (const [k, v] of Object.entries(payload)) body.set(k, v)
return apiFetch('/api/v1/attendance/violations/save-note', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
})
}
export async function sendAttendanceViolationEmail(payload: Record<string, string>): Promise<{ message?: string }> {
const body = new URLSearchParams()
for (const [k, v] of Object.entries(payload)) body.set(k, v)
return apiFetch('/api/v1/attendance/violations/send-email-manual', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
})
}
export async function fetchAttendanceComposeEmailContext(params: {
studentId: string | number
code: string
variant?: string | null
incidentDate?: string | null
}): Promise<Record<string, unknown>> {
const q = new URLSearchParams()
q.set('student_id', String(params.studentId))
q.set('code', params.code)
if (params.variant) q.set('variant', params.variant)
if (params.incidentDate) q.set('incident_date', params.incidentDate)
return apiFetch(`/api/v1/attendance/violations/compose?${q.toString()}`)
}
export async function fetchAttendanceStudentViolationsView(
studentId: string | number,
query?: { code?: string | null; incident_date?: string | null; include_notified?: string | null },
): Promise<Record<string, unknown>> {
const q = new URLSearchParams()
if (query?.code) q.set('code', query.code)
if (query?.incident_date) q.set('incident_date', query.incident_date)
if (query?.include_notified) q.set('include_notified', query.include_notified)
const qs = q.size > 0 ? `?${q.toString()}` : ''
return apiFetch(`/api/v1/attendance/violations/student/${studentId}${qs}`)
}
export async function fetchAttendanceTrackingStudents(): Promise<{
students?: Array<Record<string, unknown>>
title?: string
}> {
return apiFetch('/api/v1/attendance/tracking')
}
export async function recordAttendanceTrackingEntry(payload: Record<string, unknown>): Promise<{ message?: string }> {
return apiFetch('/api/v1/attendance/tracking/record', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export async function fetchBroadcastEmailOptions(): Promise<BroadcastEmailOptionsResponse> {
return apiFetch<BroadcastEmailOptionsResponse>('/api/v1/broadcast-email/options')
}
@@ -429,8 +698,13 @@ export async function deleteTeacherClassAssignment(payload: {
)
}
export async function fetchTeacherSubmissions(): Promise<TeacherSubmissionsResponse> {
return apiFetch<TeacherSubmissionsResponse>('/api/v1/administrator/teacher-submissions')
export async function fetchTeacherSubmissions(
searchParams?: URLSearchParams,
): Promise<TeacherSubmissionsResponse> {
const q = searchParams?.toString()
return apiFetch<TeacherSubmissionsResponse>(
`/api/v1/administrator/teacher-submissions${q ? `?${q}` : ''}`,
)
}
export async function fetchPublishedCompetitions(): Promise<PublicCompetitionIndexResponse> {
@@ -719,12 +993,194 @@ export async function fetchUsers(params?: {
return apiFetch<UsersResponse>(`/api/v1/users${qs}`)
}
function unwrapEnvelopeData(body: unknown): unknown {
if (!body || typeof body !== 'object') return body
const o = body as Record<string, unknown>
if ('data' in o && o.data != null && typeof o.data === 'object') return o.data
return body
}
function normalizeUserDetailResponse(body: unknown): UserListRow {
const raw = unwrapEnvelopeData(body)
if (!raw || typeof raw !== 'object') throw new Error('Invalid user response')
const o = raw as Record<string, unknown>
if (o.user != null && typeof o.user === 'object') {
return {
user: o.user as UserListRow['user'],
roles: Array.isArray(o.roles) ? (o.roles as UserListRow['roles']) : undefined,
role_ids: Array.isArray(o.role_ids) ? (o.role_ids as number[]) : undefined,
parent_type: typeof o.parent_type === 'string' ? o.parent_type : undefined,
second_from_parents:
typeof o.second_from_parents === 'string' ? o.second_from_parents : undefined,
}
}
if (o.id != null || o.email != null || o.firstname != null) {
return { user: o as UserListRow['user'] }
}
throw new Error('Invalid user response')
}
/** GET `/api/v1/users/:id` (falls back to user list row if show route is missing). */
export async function fetchUser(userId: number): Promise<UserListRow> {
try {
const body = await apiFetch<unknown>(`/api/v1/users/${userId}`)
return normalizeUserDetailResponse(body)
} catch (e) {
if (e instanceof ApiHttpError && (e.status === 404 || e.status === 405)) {
const data = await fetchUsers({ sort: 'lastname', order: 'asc' })
const row = data.users?.find((r) => Number(r.user?.id) === userId)
if (row) return row
}
throw e
}
}
export async function deleteUser(userId: number): Promise<{ ok: boolean; message?: string }> {
return apiFetch<{ ok: boolean; message?: string }>(`/api/v1/users/${userId}`, {
method: 'DELETE',
})
}
export async function saveUser(
userId: number,
payload: Record<string, unknown>,
): Promise<{ ok?: boolean; message?: string }> {
try {
return await apiFetch(`/api/v1/users/${userId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
} catch (error) {
if (error instanceof ApiHttpError && error.status === 422) {
const body = error.body
if (body && typeof body === 'object') {
const errors = (body as { errors?: unknown }).errors
if (errors && typeof errors === 'object') {
for (const value of Object.values(errors as Record<string, unknown>)) {
if (Array.isArray(value) && value.length > 0) {
throw new Error(String(value[0]))
}
if (typeof value === 'string' && value.trim() !== '') {
throw new Error(value)
}
}
}
}
}
throw error
}
}
/**
* Returns whether `email` can be used (not taken by another user).
* Expected Laravel route: `GET /api/v1/users/email-availability?email=...&except_user_id=...`
* 200 body: `{ "available": true }` or `{ "available": false, "message": "..." }` (or under `data`).
*/
export async function checkUserEmailAvailable(
exceptUserId: number,
email: string,
): Promise<{ available: boolean; message?: string }> {
const q = new URLSearchParams()
q.set('email', email.trim())
q.set('except_user_id', String(exceptUserId))
const body = await apiFetch<unknown>(`/api/v1/users/email-availability?${q.toString()}`)
const o = (body && typeof body === 'object' ? body : {}) as Record<string, unknown>
const d =
o.data && typeof o.data === 'object' && o.data !== null
? (o.data as Record<string, unknown>)
: o
const av = d.available
if (av === false) {
return {
available: false,
message: typeof d.message === 'string' ? d.message : undefined,
}
}
if (av === true) {
return { available: true, message: typeof d.message === 'string' ? d.message : undefined }
}
return { available: true }
}
/**
* Account settings: ask the API to email a confirmation link to `email`.
* The stored login email must not change until the user completes that flow (`confirmUserEmailChange`).
*
* Expected Laravel route: `POST /api/v1/users/{user}/email-change-request` with JSON `{ "email": "..." }`.
*/
export async function requestUserEmailChange(
userId: number,
email: string,
): Promise<{ ok?: boolean; message?: string }> {
return apiFetch(`/api/v1/users/${userId}/email-change-request`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email.trim() }),
})
}
/**
* Completes a pending email change (token from the confirmation email).
* Expected Laravel route: `POST /api/v1/users/email-change/confirm` with JSON `{ "token": "..." }`.
*/
export async function confirmUserEmailChange(
token: string,
): Promise<{ ok?: boolean; message?: string }> {
return apiFetch('/api/v1/users/email-change/confirm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: token.trim() }),
})
}
export async function createUserRecord(
payload: Record<string, unknown>,
): Promise<{ ok?: boolean; id?: number; message?: string }> {
return apiFetch('/api/v1/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export type LoginActivityRow = {
user_id?: number | string
email?: string | null
login_time?: string | null
logout_time?: string | null
ip_address?: string | null
user_agent?: string | null
}
export type LoginActivityResponse = {
activities?: LoginActivityRow[]
pagination?: {
currentPage?: number
pageCount?: number
perPage?: number
total?: number
}
}
export async function fetchLoginActivity(params?: {
page?: number
per_page?: number
school_year?: string
semester?: string
}): Promise<LoginActivityResponse> {
const query = new URLSearchParams()
if (params?.page) query.set('page', String(params.page))
if (params?.per_page) query.set('per_page', String(params.per_page))
if (params?.school_year) query.set('school_year', params.school_year)
if (params?.semester) query.set('semester', params.semester)
const qs = query.size > 0 ? `?${query.toString()}` : ''
return apiFetch<LoginActivityResponse>(`/api/v1/administrator/login-activity${qs}`)
}
export async function fetchNotificationAlerts(): Promise<NotificationAlertsResponse> {
return apiFetch<NotificationAlertsResponse>('/api/v1/administrator/notifications/alerts')
}
@@ -769,6 +1225,62 @@ export async function fetchFamilyAdminIndex(params?: {
return apiFetch<FamilyAdminIndexResponse>(`/api/v1/family-admin${qs}`)
}
/** Optional JSON search (`family/search` parity). Falls back to client-side lists when unavailable. */
export async function fetchFamilySearchSuggestions(q: string): Promise<FamilySearchResponse> {
const query = q.trim()
if (!query) return { items: [] }
try {
return await apiFetch<FamilySearchResponse>(
`/api/v1/family-admin/search?q=${encodeURIComponent(query)}`,
)
} catch {
return { items: [] }
}
}
export async function sendFamilyComposeEmail(payload: {
to: string
subject: string
html: string
return_url?: string
}): Promise<{ ok?: boolean; message?: string }> {
return apiFetch(`/api/v1/family-admin/compose-email/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
/** GET metadata for legacy family import (`routes/api.php` + JWT Bearer). */
export async function fetchFamilyLegacyImportMeta(): Promise<FamilyLegacyImportMetaResponse> {
const raw = await apiFetch<
FamilyLegacyImportMetaResponse | ApiEnvelope<FamilyLegacyImportMetaResponse>
>('/api/v1/families/import-legacy')
if (raw && typeof raw === 'object' && 'data' in raw && (raw as ApiEnvelope<FamilyLegacyImportMetaResponse>).data) {
return (raw as ApiEnvelope<FamilyLegacyImportMetaResponse>).data as FamilyLegacyImportMetaResponse
}
return raw as FamilyLegacyImportMetaResponse
}
/**
* POST multipart legacy family import. Typical Laravel field: `file` (UploadedFile).
* Append optional `dry_run` = `1` when supported.
*/
export async function submitFamilyLegacyImport(formData: FormData): Promise<FamilyLegacyImportResult> {
const raw = await fetchMultipartJson<
FamilyLegacyImportResult | ApiEnvelope<FamilyLegacyImportResult>
>('/api/v1/families/import-legacy', formData)
if (
raw &&
typeof raw === 'object' &&
'data' in raw &&
(raw as ApiEnvelope<FamilyLegacyImportResult>).data != null
) {
return (raw as ApiEnvelope<FamilyLegacyImportResult>).data as FamilyLegacyImportResult
}
return raw as FamilyLegacyImportResult
}
export async function fetchPaypalTransactions(params?: {
q?: string
perPage?: number
@@ -983,6 +1495,35 @@ export async function fetchStudentScoreCard(studentId: number): Promise<StudentS
return apiFetch<StudentScoreCardResponse>(`/api/v1/students/${studentId}/score-card`)
}
/** Students the current user may open on the score-card list (CI `StudentController::scoreCardList`). */
export type ScoreCardSelectableStudent = {
id: number
school_id?: string | null
firstname?: string | null
lastname?: string | null
/** When present, client may filter to the teachers active section (see `StudentScoreCardListPage`). */
class_section_ids?: number[] | null
}
/**
* Teachers should pass `class_section_id` + `school_year` so the list matches CI `scoreCardList`
* (students in that section for the year, not the whole school).
*/
export async function fetchScoreCardSelectableStudents(params?: {
classSectionId?: number | null
schoolYear?: string | null
}): Promise<{ students: ScoreCardSelectableStudent[] }> {
const q = new URLSearchParams()
if (params?.classSectionId != null && params.classSectionId > 0) {
q.set('class_section_id', String(params.classSectionId))
}
if (params?.schoolYear != null && String(params.schoolYear).trim() !== '') {
q.set('school_year', String(params.schoolYear).trim())
}
const qs = q.size > 0 ? `?${q.toString()}` : ''
return apiFetch<{ students: ScoreCardSelectableStudent[] }>(`/api/v1/students/score-card/selectable${qs}`)
}
export async function fetchAdministratorEmergencyContacts(params?: {
parentId?: number | null
parentIds?: number[]
@@ -1115,3 +1656,358 @@ export async function fetchReportCardPdf(studentId: number): Promise<Blob> {
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.blob()
}
function unwrapData<T extends object>(body: T & { data?: unknown }): T {
if (body && typeof body === 'object' && 'data' in body && body.data && typeof body.data === 'object') {
return body.data as T
}
return body
}
export async function fetchClassPrepIndex(params?: {
school_year?: string
semester?: string
}): Promise<ClassPrepIndexResponse> {
const qs = new URLSearchParams()
if (params?.school_year) qs.set('school_year', params.school_year)
if (params?.semester) qs.set('semester', params.semester)
const suffix = qs.toString() ? `?${qs}` : ''
const body = await apiFetch<ClassPrepIndexResponse & { data?: ClassPrepIndexResponse }>(
`/api/v1/class-prep${suffix}`,
)
return unwrapData(body)
}
export async function saveClassPrepAdjustment(payload: {
class_section_id: number | string
school_year: string
semester?: string
adjustments: Record<string, number>
}): Promise<{ ok?: boolean; message?: string }> {
return apiFetch(`/api/v1/class-prep/adjustments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export async function fetchClassPrepPrintPayload(params: {
sectionId: string
schoolYear: string
semester?: string
}): Promise<ClassPrepPrintResponse> {
const qs = new URLSearchParams()
if (params.semester) qs.set('semester', params.semester)
const suffix = qs.toString() ? `?${qs}` : ''
const body = await apiFetch<ClassPrepPrintResponse & { data?: ClassPrepPrintResponse }>(
`/api/v1/class-prep/sections/${encodeURIComponent(params.sectionId)}/print/${encodeURIComponent(params.schoolYear)}${suffix}`,
)
return unwrapData(body)
}
export async function fetchCommunicationsCompose(): Promise<CommunicationsComposeResponse> {
const body = await apiFetch<CommunicationsComposeResponse & { data?: CommunicationsComposeResponse }>(
'/api/v1/communications/compose',
)
return unwrapData(body)
}
export async function fetchStudentFamilies(studentId: number): Promise<{ data: FamilyListItem[] }> {
return apiFetch(`/api/v1/students/${studentId}/families`)
}
export async function fetchFamilyGuardians(familyId: number): Promise<{ data: GuardianListItem[] }> {
return apiFetch(`/api/v1/families/${familyId}/guardians`)
}
export async function previewCommunicationsEmail(payload: {
student_id: number
family_id: number
template_key: string
vars?: Record<string, string>
}): Promise<CommunicationsPreviewResponse> {
return apiFetch(`/api/v1/communications/preview`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export async function sendCommunicationsEmail(payload: {
student_id: number
family_id: number
template_key: string
subject: string
body: string
recipients: string[]
cc?: string[]
bcc?: string[]
}): Promise<{ ok?: boolean; message?: string }> {
return apiFetch(`/api/v1/communications/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export async function fetchConfigurations(): Promise<ConfigurationListResponse> {
const body = await apiFetch<ConfigurationListResponse & { data?: ConfigurationListResponse }>(
'/api/v1/configuration',
)
return unwrapData(body)
}
export async function createConfigurationEntry(payload: {
config_key: string
config_value: string
}): Promise<{ ok?: boolean }> {
return apiFetch(`/api/v1/configuration`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export async function updateConfigurationEntry(
id: number | string,
payload: { config_key: string; config_value: string },
): Promise<{ ok?: boolean }> {
return apiFetch(`/api/v1/configuration/${encodeURIComponent(String(id))}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export async function deleteConfigurationEntry(id: number | string): Promise<{ ok?: boolean }> {
return apiFetch(`/api/v1/configuration/${encodeURIComponent(String(id))}`, {
method: 'DELETE',
})
}
export async function fetchDiscountsList(params?: {
school_year?: string
semester?: string
}): Promise<DiscountsListResponse> {
const qs = new URLSearchParams()
if (params?.school_year) qs.set('school_year', params.school_year)
if (params?.semester) qs.set('semester', params.semester)
const suffix = qs.toString() ? `?${qs}` : ''
const body = await apiFetch<DiscountsListResponse & { data?: DiscountsListResponse }>(
`/api/v1/discounts${suffix}`,
)
return unwrapData(body)
}
export async function fetchDiscountApplyContext(params?: {
school_year?: string
semester?: string
}): Promise<DiscountApplyContextResponse> {
const qs = new URLSearchParams()
if (params?.school_year) qs.set('school_year', params.school_year)
if (params?.semester) qs.set('semester', params.semester)
const suffix = qs.toString() ? `?${qs}` : ''
const body = await apiFetch<DiscountApplyContextResponse & { data?: DiscountApplyContextResponse }>(
`/api/v1/discounts/apply-context${suffix}`,
)
return unwrapData(body)
}
export async function createDiscountVoucher(payload: Record<string, unknown>): Promise<{ ok?: boolean; message?: string }> {
return apiFetch(`/api/v1/discounts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export async function applyDiscountVoucher(payload: {
voucher_id: number | string
parent_ids: (number | string)[]
allow_additional?: boolean
school_year?: string
semester?: string
}): Promise<{ ok?: boolean; message?: string }> {
return apiFetch(`/api/v1/discounts/apply`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export async function reverseDiscountApplication(payload: {
voucher_id?: number | string
parent_ids: (number | string)[]
reason?: string
school_year?: string
semester?: string
}): Promise<{ ok?: boolean; message?: string }> {
return apiFetch(`/api/v1/discounts/reverse`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
function normalizeEnrollmentWithdrawalResponse(
d: EnrollmentWithdrawalPageResponse,
): {
students: EnrollmentWithdrawalPageResponse['students']
classes: EnrollmentWithdrawalPageResponse['classes']
schoolYears: string[]
selectedYear: string
currentYear: string
semester: string
missingYear: boolean
isCurrentYear: boolean
} {
const selectedYear = String(d.selectedYear ?? d.selected_year ?? '')
const currentYear = String(d.currentYear ?? d.current_year ?? '')
const semester = String(d.semester ?? '')
const missingYear = Boolean(d.missingYear ?? d.missing_year)
let isCurrentYear = d.isCurrentYear ?? d.is_current_year
if (isCurrentYear === undefined)
isCurrentYear = selectedYear !== '' && currentYear !== '' && selectedYear === currentYear
const schoolYearsRaw = d.schoolYears ?? d.school_years
const schoolYears = Array.isArray(schoolYearsRaw) ? schoolYearsRaw.map((y) => String(y)) : []
return {
students: d.students ?? [],
classes: d.classes ?? [],
schoolYears,
selectedYear,
currentYear,
semester,
missingYear,
isCurrentYear: Boolean(isCurrentYear),
}
}
/** Admin — registered students (legacy new-students). */
export async function fetchRegisteredNewStudents(params?: {
school_year?: string
semester?: string
}): Promise<{
new_students: RegisteredNewStudentsResponse['new_students']
total_new?: number
school_years?: string[]
}> {
const qs = new URLSearchParams()
if (params?.school_year) qs.set('school_year', params.school_year)
if (params?.semester) qs.set('semester', params.semester)
const suffix = qs.toString() ? `?${qs}` : ''
const body = await apiFetch<
RegisteredNewStudentsResponse & { data?: RegisteredNewStudentsResponse }
>(`/api/v1/administrator/enroll-withdrawal/registered-new-students${suffix}`)
const d = unwrapData(body)
return {
new_students: d.new_students ?? [],
total_new: d.total_new,
school_years: d.school_years,
}
}
/** Admin — enrollment & withdrawal grid (legacy enrollment_withdrawal). */
export async function fetchEnrollmentWithdrawalPage(params?: {
schoolYear?: string
semester?: string
}): Promise<ReturnType<typeof normalizeEnrollmentWithdrawalResponse>> {
const qs = new URLSearchParams()
if (params?.schoolYear) qs.set('schoolYear', params.schoolYear)
if (params?.semester) qs.set('semester', params.semester)
const suffix = qs.toString() ? `?${qs}` : ''
const body = await apiFetch<
EnrollmentWithdrawalPageResponse & { data?: EnrollmentWithdrawalPageResponse }
>(`/api/v1/administrator/enroll-withdrawal${suffix}`)
const d = unwrapData(body)
return normalizeEnrollmentWithdrawalResponse(d)
}
export async function postEnrollmentWithdrawalAssignClass(payload: {
student_id: number
parent_id: number
class_section_id: number | string
}): Promise<{ ok?: boolean; message?: string }> {
return apiFetch(`/api/v1/administrator/enroll-withdrawal/assign-class`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export async function postEnrollmentWithdrawalStatusBatch(payload: {
updates: { student_id: number; enrollment_status: string }[]
school_year?: string
semester?: string
}): Promise<{ ok?: boolean; message?: string }> {
return apiFetch(`/api/v1/administrator/enroll-withdrawal/status-batch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export async function postGenerateInvoiceForParent(parentId: number): Promise<unknown> {
return apiFetch(`/api/v1/administrator/invoices/generate-for-parent`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ parent_id: parentId }),
})
}
export async function fetchExpensesList(params?: {
school_year?: string
semester?: string
}): Promise<ExpensesListResponse> {
const qs = new URLSearchParams()
if (params?.school_year) qs.set('school_year', params.school_year)
if (params?.semester) qs.set('semester', params.semester)
const suffix = qs.toString() ? `?${qs}` : ''
const body = await apiFetch<ExpensesListResponse & { data?: ExpensesListResponse }>(
`/api/v1/administrator/expenses${suffix}`,
)
const d = unwrapData(body as ExpensesListResponse & { data?: ExpensesListResponse })
return {
expenses: d.expenses ?? [],
school_year: d.school_year,
semester: d.semester,
school_years: d.school_years,
}
}
export async function fetchExpenseCreateForm(): Promise<ExpenseCreateFormResponse> {
const body = await apiFetch<ExpenseCreateFormResponse & { data?: ExpenseCreateFormResponse }>(
'/api/v1/administrator/expenses/create-options',
)
return unwrapData(body as ExpenseCreateFormResponse & { data?: ExpenseCreateFormResponse })
}
export async function createExpense(
formData: FormData,
): Promise<{ ok?: boolean; message?: string }> {
return fetchMultipartJson('/api/v1/administrator/expenses', formData)
}
export async function fetchExpenseEdit(expenseId: number): Promise<ExpenseEditFormResponse> {
const body = await apiFetch<ExpenseEditFormResponse & { data?: ExpenseEditFormResponse }>(
`/api/v1/administrator/expenses/${expenseId}/edit`,
)
return unwrapData(body as ExpenseEditFormResponse & { data?: ExpenseEditFormResponse })
}
export async function updateExpense(
expenseId: number,
formData: FormData,
): Promise<{ ok?: boolean; message?: string }> {
return fetchMultipartJson(`/api/v1/administrator/expenses/${expenseId}`, formData)
}
export async function updateExpenseStatus(payload: {
id: number
status: 'approved' | 'denied'
}): Promise<{ success?: boolean; error?: string }> {
return apiFetch(`/api/v1/administrator/expenses/update-status`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
+17
View File
@@ -0,0 +1,17 @@
/**
* Late slips preview (legacy CI slips/preview_list).
*/
import { apiFetch } from './http'
export type SlipPreviewRow = Record<string, unknown>
export async function fetchSlipPreviewList(params?: {
school_year?: string
semester?: string
}): Promise<{ rows?: SlipPreviewRow[]; school_year?: string; semester?: string }> {
const qs = new URLSearchParams()
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/slips/preview${suffix}`)
}
+94
View File
@@ -0,0 +1,94 @@
/**
* Staff directory (legacy CI `staff/*`).
* Expected Laravel routes under `/api/v1/administrator/staff`.
*/
import { apiFetch } from './http'
const BASE = '/api/v1/administrator/staff'
export type StaffListRow = {
id: number
user_id?: number
school_id?: string | null
firstname?: string | null
lastname?: string | null
email?: string | null
phone?: string | null
active_role?: string | null
class_section?: string | null
verification_issue?: boolean
}
export type StaffIndexResponse = {
staff?: StaffListRow[]
issues_count?: number
school_year?: string | null
semester?: string | null
schoolYears?: string[]
}
export type RoleOption = { id: number; name: string }
export type StaffFormOptionsResponse = {
roles?: RoleOption[]
}
export type StaffMemberResponse = {
user?: Record<string, unknown> & {
id?: number
firstname?: string | null
lastname?: string | null
email?: string | null
phone?: string | null
}
staff?: Record<string, unknown> & {
firstname?: string | null
lastname?: string | null
email?: string | null
phone?: string | null
}
}
export async function fetchStaffIndex(params?: {
school_year?: string
semester?: string
}): Promise<StaffIndexResponse> {
const qs = new URLSearchParams()
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(`${BASE}${suffix}`)
}
export async function fetchStaffFormOptions(): Promise<StaffFormOptionsResponse> {
return apiFetch(`${BASE}/form-options`)
}
export async function fetchStaffMember(id: number): Promise<StaffMemberResponse> {
return apiFetch(`${BASE}/${id}`)
}
export async function createStaff(payload: {
firstname: string
lastname: string
email: string
password: string
role_id: number
}): Promise<{ ok?: boolean; message?: string }> {
return apiFetch(`${BASE}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export async function updateStaff(
id: number,
payload: { firstname: string; lastname: string; email: string; phone?: string },
): Promise<{ ok?: boolean; message?: string }> {
return apiFetch(`${BASE}/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
+82
View File
@@ -0,0 +1,82 @@
/**
* Teacher portal API (`Views/teacher/*` parity).
* Laravel should expose matching routes under `/api/v1/teacher/...`.
*/
import { apiFetch, getStoredToken } from './http'
import { apiUrl } from '../lib/apiOrigin'
/** Response shape for `GET /api/v1/teacher/dashboard` (extend as backend grows). */
export type TeacherDashboardPayload = {
welcome?: string | null
school_year?: string | null
semester?: string | null
counts?: Record<string, number | string | null>
announcements?: Array<{ title?: string | null; body?: string | null; html?: string | null }>
[key: string]: unknown
}
export async function fetchTeacherJson<T>(path: string): Promise<T> {
const p = path.startsWith('/') ? path : `/${path}`
return apiFetch<T>(`/api/v1/teacher${p}`)
}
/**
* Opt-in: set `VITE_TEACHER_DASHBOARD_API=1` in `.env` when Laravel exposes
* `GET /api/v1/teacher/dashboard`. Otherwise the SPA skips the request (avoids 404 noise).
*/
export function isTeacherDashboardApiEnabled(): boolean {
return import.meta.env.VITE_TEACHER_DASHBOARD_API === '1'
}
export async function fetchTeacherDashboard(): Promise<TeacherDashboardPayload> {
return fetchTeacherJson<TeacherDashboardPayload>('/dashboard')
}
export async function postTeacherJson<T>(path: string, body: unknown): Promise<T> {
const p = path.startsWith('/') ? path : `/${path}`
return apiFetch<T>(`/api/v1/teacher${p}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export async function postTeacherMultipart<T>(path: string, formData: FormData): Promise<T> {
const isAbsoluteApiPath = path.startsWith('/api/')
const p = isAbsoluteApiPath ? path : path.startsWith('/') ? path : `/${path}`
const headers = new Headers({ Accept: 'application/json' })
const token = getStoredToken()
if (token) headers.set('Authorization', `Bearer ${token}`)
const url = isAbsoluteApiPath ? apiUrl(p) : apiUrl(`/api/v1/teacher${p}`)
const res = await fetch(url, {
method: 'POST',
headers,
body: formData,
})
const body: unknown = await res.json().catch(() => null)
if (!res.ok) {
let msg =
typeof body === 'object' && body !== null && 'message' in body
? String((body as { message?: unknown }).message ?? '')
: ''
if (typeof body === 'object' && body !== null && 'errors' in body) {
const errors = (body as { errors?: unknown }).errors
if (errors && typeof errors === 'object') {
const firstFieldErrors = Object.values(errors as Record<string, unknown>).find(
(value) => Array.isArray(value) && value.length > 0,
)
if (Array.isArray(firstFieldErrors) && firstFieldErrors[0]) {
msg = String(firstFieldErrors[0])
}
}
}
throw new Error(msg || `HTTP ${res.status}`)
}
return body as T
}
+377 -2
View File
@@ -2,6 +2,8 @@ export type AuthUser = {
id: number
name: string
roles: Record<string, boolean>
class_section_id?: number | null
class_section_name?: string | null
}
export type LoginSuccess = {
@@ -129,8 +131,13 @@ export type AdministratorAbsenceFormResponse = {
admin_name?: string | null
semester?: string | null
schoolYear?: string | null
/** Laravel may return snake_case */
school_year?: string | null
existing?: AdministratorAbsenceRow[]
availableDates?: string[]
available_dates?: string[]
/** Optional labels for POST `reason_type` (from API or empty to use client defaults). */
reason_types?: Record<string, string>
}
export type AdministratorAbsenceSubmitResponse = {
@@ -201,6 +208,95 @@ export type AdministratorAdminAttendanceResponse = {
admins_grid?: AdministratorAdminAttendanceRow[]
}
/** Staff teacher daily grid — `GET /api/v1/attendance/staff/teachers` */
export type TeacherStaffAttendanceRow = {
teacher_id?: number
firstname?: string | null
lastname?: string | null
position?: string | null
status?: string | null
reason?: string | null
}
export type TeacherStaffAttendanceResponse = {
semester?: string | null
school_year?: string | null
date?: string | null
class_section_id?: number | null
locked?: boolean
/** Section id → label */
sections?: Record<string, string>
grid?: TeacherStaffAttendanceRow[]
}
/** Monthly admin/teacher grid payload — `GET /api/v1/attendance/staff/month-overview` */
export type StaffMonthlyCell = { status?: string | null }
export type StaffMonthlyTeacherRow = {
user_id?: number
teacher_id?: number
name?: string | null
position?: string | null
cells?: Record<string, StaffMonthlyCell>
totals?: { p?: number; a?: number; l?: number }
}
export type StaffMonthlySection = {
section_code?: string
label?: string
teachers?: StaffMonthlyTeacherRow[]
}
export type StaffMonthlyAdminRow = {
user_id?: number
name?: string | null
role?: string | null
cells?: Record<string, StaffMonthlyCell>
totals?: { p?: number; a?: number; l?: number }
}
export type StaffMonthlyOverviewPayload = {
filters?: {
semester?: string
school_year?: string
range_label?: string
month_label?: string
}
sundays?: string[]
noSchoolDays?: string[]
sections?: StaffMonthlySection[]
admins?: StaffMonthlyAdminRow[]
missingYear?: boolean
schoolYears?: Array<{ school_year?: string } | string>
isCurrentYear?: boolean
}
export type AttendanceViolationStudentRow = Record<string, unknown>
export type AttendanceViolationsResponse = {
students?: AttendanceViolationStudentRow[]
school_year?: string
semester?: string
debug?: Record<string, unknown>
}
export type ParentAttendanceAdminReportRow = Record<string, unknown>
export type EarlyDismissalGroup = Record<string, Array<Record<string, unknown>>>
export type EarlyDismissalsResponse = {
groups?: EarlyDismissalGroup
schoolYear?: string
semester?: string
signatureByDate?: Record<string, { filename?: string; original_name?: string }>
}
export type AttendanceCommentTemplate = {
id?: number
min_score?: number
max_score?: number
template_text?: string
is_active?: boolean
}
export type AttendanceCommentTemplatesResponse = {
templates?: AttendanceCommentTemplate[]
}
export type ParentAttendanceRow = {
firstname: string
lastname: string
@@ -539,7 +635,7 @@ export type UserListRow = {
semester?: string | null
[key: string]: unknown
} | null
roles?: Array<{ id?: number; name?: string | null; slug?: string | null }>
roles?: Array<string | { id?: number; name?: string | null; slug?: string | null }>
role_ids?: number[]
parent_type?: string
second_from_parents?: string
@@ -580,6 +676,10 @@ export type FamilyGuardianRow = {
is_primary?: boolean
receive_emails?: boolean
receive_sms?: boolean
address_street?: string | null
city?: string | null
state?: string | null
zip?: string | null
}
export type FamilyStudentRow = {
@@ -621,6 +721,50 @@ export type FamilyAdminIndexResponse = ApiEnvelope<{
resolved_student_id?: number | null
}>
export type FamilySearchSuggestionItem = {
type: 'student' | 'guardian'
id: number
label: string
sub?: string
}
export type FamilySearchResponse = {
items: FamilySearchSuggestionItem[]
}
/** GET `/api/v1/families/import-legacy` — metadata for the legacy import UI (optional; SPA uses defaults if unavailable). */
export type FamilyLegacyImportMetaResponse = {
instructions?: string | null
instructions_html?: string | null
accepted_extensions?: string[]
accepted_mime_types?: string[]
max_file_bytes?: number | null
template_url?: string | null
template_download_url?: string | null
columns?: string[]
dry_run_supported?: boolean
}
export type FamilyLegacyImportRowError = {
row?: number
line?: number
message?: string
error?: string
}
/** POST `/api/v1/families/import-legacy` (multipart) response */
export type FamilyLegacyImportResult = {
ok?: boolean
message?: string
imported?: number
imported_count?: number
skipped?: number
skipped_count?: number
failed?: number
errors?: FamilyLegacyImportRowError[]
validation_errors?: FamilyLegacyImportRowError[]
}
export type PaypalTransactionRow = {
id: number
transaction_id?: string | null
@@ -977,10 +1121,15 @@ export type SchoolCalendarEventRow = {
title: string
start: string
description?: string
backgroundColor?: string | null
extendedProps?: {
event_type?: string
backgroundColor?: string
color?: string
no_school?: number | boolean
notify_parent?: number | boolean
notify_teacher?: number | boolean
notify_admin?: number | boolean
[key: string]: unknown
}
}
@@ -1080,13 +1229,20 @@ export type ClassProgressReportRow = {
status_label?: string | null
covered?: string | null
homework?: string | null
materials?: string | null
support_needed?: string | null
flags?: string[]
week_start?: string | null
week_end?: string | null
created_at?: string | null
updated_at?: string | null
attachments?: { id?: number; name?: string | null; file_path?: string | null; download_url?: string | null }[]
attachments?: {
id?: number
name?: string | null
file_path?: string | null
download_url?: string | null
legacy?: boolean | number | null
}[]
}
export type ClassProgressGroupRow = {
@@ -1183,3 +1339,222 @@ export type ParentEventsOverviewResponse = {
charges?: Record<string, unknown>[]
yourStudents?: ParentEnrollmentStudent[]
}
/** Class preparation (inventory vs sections) — Laravel `GET /api/v1/class-prep` */
export type ClassPrepSectionRow = {
class_section_id: number | string
class_section: string
needs_print?: boolean
last_printed_at?: string | null
student_count: number
prep_items: Record<string, number>
adjustments: Record<string, number>
}
export type ClassPrepIndexResponse = {
school_year: string
semester?: string | null
prep_results: ClassPrepSectionRow[]
total_needed: Record<string, number>
available: Record<string, number>
shortages?: Record<string, number>
}
export type ClassPrepPrintResponse = {
class_section?: string | null
prep_items: Record<string, number>
}
export type CommunicationsStudentOption = {
id: number
firstname?: string | null
lastname?: string | null
}
export type CommunicationsTemplateOption = {
template_key: string
name?: string | null
}
export type CommunicationsComposeResponse = {
students: CommunicationsStudentOption[]
templates: CommunicationsTemplateOption[]
display_name?: string | null
}
export type CommunicationsPreviewResponse = {
subject?: string | null
html?: string | null
}
export type FamilyListItem = {
id: number
household_name?: string | null
is_primary_home?: boolean
}
export type GuardianListItem = {
email?: string | null
receive_emails?: boolean
}
export type ConfigurationRow = {
id: number | string
config_key: string
config_value: string
}
export type ConfigurationListResponse = {
configs: ConfigurationRow[]
}
export type DiscountVoucherRow = {
id: number | string
code: string
discount_type: 'percent' | 'fixed' | string
discount_value: number | string
max_uses?: number | string | null
times_used?: number | string | null
valid_from?: string | null
valid_until?: string | null
is_active?: boolean
}
export type DiscountsListResponse = {
vouchers: DiscountVoucherRow[]
school_year?: string | null
semester?: string | null
school_years?: string[]
}
export type DiscountApplyParentRow = {
id: number | string
school_id?: string | null
firstname?: string | null
lastname?: string | null
email?: string | null
has_discount?: boolean
total_discount?: number | string | null
}
export type DiscountApplyContextResponse = {
vouchers: DiscountVoucherRow[]
parents: DiscountApplyParentRow[]
}
/** Admin: enroll_withdraw / new-students list */
export type RegisteredNewStudentRow = {
id: number
firstname?: string | null
lastname?: string | null
new_student?: string | null
class_section?: string | null
enrollment_status?: string | null
age?: string | number | null
registration_date?: string | null
parent_firstname?: string | null
parent_lastname?: string | null
parent_phone?: string | null
parent_email?: string | null
emergency_name?: string | null
emergency_relationship?: string | null
emergency_phone?: string | null
}
export type RegisteredNewStudentsResponse = {
new_students: RegisteredNewStudentRow[]
total_new?: number
school_year?: string | null
semester?: string | null
school_years?: string[]
}
export type EnrollmentWithdrawalClassOption = {
id?: number | string
class_section_id?: number | string
name?: string | null
class_section_name?: string | null
}
export type EnrollmentWithdrawalStudentRow = {
student_id?: number
id?: number
parent_id?: number | null
guardian_id?: number | null
firstname?: string | null
lastname?: string | null
parent_label?: string | null
new_student?: string | null
removed_previous_year?: string | null
class_section?: string | null
enrollment_status?: string | null
registration_date?: string | null
}
export type EnrollmentWithdrawalPageResponse = {
students: EnrollmentWithdrawalStudentRow[]
classes: EnrollmentWithdrawalClassOption[]
school_years?: string[]
schoolYears?: string[]
selected_year?: string | null
selectedYear?: string | null
current_year?: string | null
currentYear?: string | null
semester?: string | null
missing_year?: boolean
missingYear?: boolean
is_current_year?: boolean
isCurrentYear?: boolean
}
/** Admin expenses (`Views/expenses/*`) */
export type ExpenseUserOption = {
id: number
firstname?: string | null
lastname?: string | null
}
export type ExpenseRow = {
id: number
category: string
amount: number | string
retailor?: string | null
retailer?: string | null
description?: string | null
status: string
receipt_path?: string | null
/** Full URL if API provides it */
receipt_url?: string | null
purchaser_firstname?: string | null
purchaser_lastname?: string | null
approver_firstname?: string | null
approver_lastname?: string | null
created_at?: string | null
date_of_purchase?: string | null
purchase_date?: string | null
purchased_at?: string | null
date_purchased?: string | null
}
export type ExpensesListResponse = {
expenses: ExpenseRow[]
school_year?: string | null
semester?: string | null
school_years?: string[]
}
export type ExpenseCreateFormResponse = {
retailors: string[]
users: ExpenseUserOption[]
}
export type ExpenseDetail = ExpenseRow & {
purchased_by?: number | string | null
}
export type ExpenseEditFormResponse = {
expense: ExpenseDetail
retailors: string[]
users: ExpenseUserOption[]
receipt_url?: string | null
}
+140
View File
@@ -0,0 +1,140 @@
/**
* WhatsApp admin tools (legacy CI `whatsapp/*`).
* Expected Laravel routes under `/api/v1/administrator/whatsapp`.
*/
import { apiFetch } from './http'
const BASE = '/api/v1/administrator/whatsapp'
export type WhatsappSectionRow = {
class_section_id?: number
id?: number
class_section_name?: string
name?: string
school_year?: string
semester?: string
}
export type WhatsappLinkRow = {
invite_link?: string | null
active?: number | boolean
}
export type WhatsappManageLinksResponse = {
schoolYear?: string | null
semester?: string | null
sections?: WhatsappSectionRow[]
linksBySection?: Record<string, WhatsappLinkRow>
parents?: Array<{
id: number
firstname?: string | null
lastname?: string | null
email?: string | null
source?: string | null
}>
}
export type ParentContactRow = {
firstname?: string | null
lastname?: string | null
type?: string | null
phone?: string | null
email?: string | null
}
export type WhatsappParentContactsResponse = {
schoolYear?: string | null
semester?: string | null
contacts?: ParentContactRow[]
}
export type ParentContactsByClassParentRow = {
class_section_id?: number
primary_id?: number
primary_name?: string
primary_phone?: string | null
primary_member?: string | null
second_id?: number
second_name?: string
second_phone?: string | null
second_member?: string | null
}
export type ParentContactsByClassSection = {
class_section_id?: number
class_section_name?: string
school_year?: string
semester?: string
parents?: ParentContactsByClassParentRow[]
}
export type WhatsappParentContactsByClassResponse = {
classSections?: ParentContactsByClassSection[]
}
export async function fetchWhatsappManageLinks(params?: {
school_year?: string
semester?: string
}): Promise<WhatsappManageLinksResponse> {
const qs = new URLSearchParams()
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(`${BASE}/manage-links${suffix}`)
}
export async function saveWhatsappLink(payload: {
class_section_id: number
class_section_name?: string
invite_link: string
active?: boolean
}): Promise<{ ok?: boolean; message?: string }> {
return apiFetch(`${BASE}/save-link`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export async function sendWhatsappInvites(payload: {
mode: 'all' | 'class' | 'parents'
class_section_id?: number | null
parent_ids?: number[]
}): Promise<{ ok?: boolean; message?: string }> {
return apiFetch(`${BASE}/send-invites`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export async function fetchWhatsappParentContacts(params?: {
school_year?: string
semester?: string
}): Promise<WhatsappParentContactsResponse> {
const qs = new URLSearchParams()
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(`${BASE}/parent-contacts${suffix}`)
}
export async function fetchWhatsappParentContactsByClass(): Promise<WhatsappParentContactsByClassResponse> {
return apiFetch(`${BASE}/parent-contacts-by-class`)
}
export async function updateWhatsappMembership(payload: {
class_section_id: number
school_year?: string
semester?: string
primary_id: number
second_id?: number
primary_member?: string
second_member?: string
}): Promise<{ status?: string; message?: string }> {
return apiFetch(`${BASE}/update-membership`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}