fix teacher, parent and admin pages
This commit is contained in:
@@ -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),
|
||||
})
|
||||
}
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
@@ -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}`)
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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}`)
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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 teacher’s 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),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user