init project
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { apiUrl } from '../lib/apiOrigin'
|
||||
|
||||
const TOKEN_KEY = 'alrahma_api_token'
|
||||
|
||||
export function getStoredToken(): string | null {
|
||||
return localStorage.getItem(TOKEN_KEY)
|
||||
}
|
||||
|
||||
export function setStoredToken(token: string | null): void {
|
||||
if (token) localStorage.setItem(TOKEN_KEY, token)
|
||||
else localStorage.removeItem(TOKEN_KEY)
|
||||
}
|
||||
|
||||
export class ApiHttpError extends Error {
|
||||
readonly status: number
|
||||
readonly body: unknown
|
||||
|
||||
constructor(message: string, status: number, body: unknown) {
|
||||
super(message)
|
||||
this.name = 'ApiHttpError'
|
||||
this.status = status
|
||||
this.body = body
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiFetch<T>(
|
||||
path: string,
|
||||
init: RequestInit = {},
|
||||
opts: { attachAuth?: boolean } = {},
|
||||
): Promise<T> {
|
||||
const attachAuth = opts.attachAuth !== false
|
||||
const headers = new Headers(init.headers)
|
||||
if (!headers.has('Accept')) headers.set('Accept', 'application/json')
|
||||
|
||||
const token = getStoredToken()
|
||||
if (attachAuth && token && !headers.has('Authorization')) {
|
||||
headers.set('Authorization', `Bearer ${token}`)
|
||||
}
|
||||
|
||||
const res = await fetch(apiUrl(path), { ...init, 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 ApiHttpError(msg || `HTTP ${res.status}`, res.status, body)
|
||||
}
|
||||
|
||||
return body as T
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { apiUrl } from '../lib/apiOrigin'
|
||||
|
||||
export type FullRegisterPayload = {
|
||||
firstname: string
|
||||
lastname: string
|
||||
gender: string
|
||||
cellphone: string
|
||||
email: string
|
||||
confirm_email: string
|
||||
address_street: string
|
||||
apt?: string
|
||||
city: string
|
||||
state: string
|
||||
zip: string
|
||||
is_parent: boolean
|
||||
no_second_parent_info: boolean
|
||||
second_firstname?: string
|
||||
second_lastname?: string
|
||||
second_gender?: string
|
||||
second_email?: string
|
||||
second_cellphone?: string
|
||||
accept_school_policy: boolean
|
||||
captcha: string
|
||||
}
|
||||
|
||||
export async function fetchRegisterCaptcha(): Promise<{ challenge: string }> {
|
||||
const res = await fetch(apiUrl('/api/v1/auth/register/captcha'), {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
})
|
||||
const body: unknown = await res.json().catch(() => null)
|
||||
if (!res.ok || !body || typeof body !== 'object') {
|
||||
throw new Error('Could not load CAPTCHA.')
|
||||
}
|
||||
const o = body as { ok?: boolean; captcha?: string }
|
||||
if (!o.ok || typeof o.captcha !== 'string') {
|
||||
throw new Error('Invalid CAPTCHA response.')
|
||||
}
|
||||
return { challenge: o.captcha }
|
||||
}
|
||||
|
||||
export type FullRegisterSuccess = {
|
||||
ok: true
|
||||
message?: string
|
||||
registration?: unknown
|
||||
}
|
||||
|
||||
export type FullRegisterFailure = {
|
||||
ok: false
|
||||
message: string
|
||||
errors?: Record<string, string[]>
|
||||
}
|
||||
|
||||
export async function submitFullRegistration(
|
||||
payload: FullRegisterPayload,
|
||||
): Promise<FullRegisterSuccess | FullRegisterFailure> {
|
||||
const body: Record<string, unknown> = {
|
||||
firstname: payload.firstname.trim(),
|
||||
lastname: payload.lastname.trim(),
|
||||
gender: payload.gender,
|
||||
cellphone: payload.cellphone.trim(),
|
||||
email: payload.email.trim(),
|
||||
confirm_email: payload.confirm_email.trim(),
|
||||
address_street: payload.address_street.trim(),
|
||||
apt: payload.apt?.trim() || '',
|
||||
city: payload.city.trim(),
|
||||
state: payload.state,
|
||||
zip: payload.zip.trim(),
|
||||
is_parent: payload.is_parent,
|
||||
no_second_parent_info: payload.no_second_parent_info,
|
||||
accept_school_policy: payload.accept_school_policy,
|
||||
captcha: payload.captcha.trim(),
|
||||
}
|
||||
|
||||
if (payload.is_parent && !payload.no_second_parent_info) {
|
||||
body.second_firstname = payload.second_firstname?.trim() ?? ''
|
||||
body.second_lastname = payload.second_lastname?.trim() ?? ''
|
||||
body.second_gender = payload.second_gender ?? ''
|
||||
body.second_email = payload.second_email?.trim() ?? ''
|
||||
body.second_cellphone = payload.second_cellphone?.trim() ?? ''
|
||||
}
|
||||
|
||||
const res = await fetch(apiUrl('/api/v1/auth/register'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
const data: unknown = await res.json().catch(() => null)
|
||||
|
||||
if (res.ok && data && typeof data === 'object' && 'ok' in data && (data as { ok?: boolean }).ok === true) {
|
||||
return { ok: true, message: (data as { message?: string }).message, registration: (data as { registration?: unknown }).registration }
|
||||
}
|
||||
|
||||
let message = 'Registration failed.'
|
||||
let errors: Record<string, string[]> | undefined
|
||||
if (data && typeof data === 'object') {
|
||||
const o = data as { message?: string; errors?: Record<string, string[] | string> }
|
||||
if (typeof o.message === 'string') message = o.message
|
||||
if (o.errors && typeof o.errors === 'object') {
|
||||
errors = {}
|
||||
for (const [k, v] of Object.entries(o.errors)) {
|
||||
if (Array.isArray(v)) errors[k] = v.map(String)
|
||||
else errors[k] = [String(v)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: false, message, errors }
|
||||
}
|
||||
|
||||
export async function fetchSchoolPolicyHtml(): Promise<string> {
|
||||
const res = await fetch(apiUrl('/api/v1/policies/school'), {
|
||||
headers: { Accept: 'application/json' },
|
||||
})
|
||||
const json = (await res.json().catch(() => null)) as {
|
||||
data?: { policy?: { content?: string } }
|
||||
}
|
||||
const html = json?.data?.policy?.content
|
||||
if (!res.ok || !html) {
|
||||
return '<p>Unable to load policy. Please contact the school.</p>'
|
||||
}
|
||||
return html
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { apiFetch } from './http'
|
||||
import type {
|
||||
ApiEnvelope,
|
||||
DashboardPayload,
|
||||
LoginFailure,
|
||||
LoginResponse,
|
||||
LoginSuccess,
|
||||
NavMenuResponse,
|
||||
ParentAttendanceResponse,
|
||||
ParentAttendanceReportFormResponse,
|
||||
ParentEnrollmentsResponse,
|
||||
ParentEventsResponse,
|
||||
ParentInvoicesResponse,
|
||||
ParentProfileResponse,
|
||||
ParentRegistrationResponse,
|
||||
ParentEmergencyContactsResponse,
|
||||
ParentAttendanceReportsListResponse,
|
||||
ParentAuthorizedUsersApiResponse,
|
||||
ContactSubmitResponse,
|
||||
} from './types'
|
||||
|
||||
export async function loginRequest(
|
||||
email: string,
|
||||
password: string,
|
||||
): Promise<LoginResponse> {
|
||||
const body = await apiFetch<LoginResponse>(
|
||||
'/api/v1/auth/login',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
},
|
||||
{ attachAuth: false },
|
||||
)
|
||||
|
||||
if (body.status === false) {
|
||||
const fail = body as LoginFailure
|
||||
return { status: false, message: fail.message }
|
||||
}
|
||||
|
||||
return body as LoginSuccess
|
||||
}
|
||||
|
||||
export async function fetchDashboardRoute(): Promise<ApiEnvelope<DashboardPayload>> {
|
||||
return apiFetch<ApiEnvelope<DashboardPayload>>('/api/v1/dashboard/route')
|
||||
}
|
||||
|
||||
export async function fetchNavMenu(): Promise<NavMenuResponse> {
|
||||
return apiFetch<NavMenuResponse>('/api/v1/nav-builder/menu')
|
||||
}
|
||||
|
||||
export async function fetchParentAttendance(
|
||||
schoolYear: string | null,
|
||||
): Promise<ParentAttendanceResponse> {
|
||||
const q = schoolYear ? `?school_year=${encodeURIComponent(schoolYear)}` : ''
|
||||
return apiFetch<ParentAttendanceResponse>(`/api/v1/parents/attendance${q}`)
|
||||
}
|
||||
|
||||
export async function fetchParentProfile(): Promise<ParentProfileResponse> {
|
||||
return apiFetch<ParentProfileResponse>('/api/v1/parents/profile')
|
||||
}
|
||||
|
||||
export async function fetchParentEnrollments(
|
||||
schoolYear: string | null,
|
||||
): Promise<ParentEnrollmentsResponse> {
|
||||
const q = schoolYear ? `?school_year=${encodeURIComponent(schoolYear)}` : ''
|
||||
return apiFetch<ParentEnrollmentsResponse>(`/api/v1/parents/enrollments${q}`)
|
||||
}
|
||||
|
||||
export async function fetchParentInvoices(
|
||||
schoolYear: string | null,
|
||||
): Promise<ParentInvoicesResponse> {
|
||||
const q = schoolYear ? `?school_year=${encodeURIComponent(schoolYear)}` : ''
|
||||
return apiFetch<ParentInvoicesResponse>(`/api/v1/parents/invoices${q}`)
|
||||
}
|
||||
|
||||
export async function fetchParentEventsOverview(): Promise<ParentEventsResponse> {
|
||||
return apiFetch<ParentEventsResponse>('/api/v1/parents/events')
|
||||
}
|
||||
|
||||
export async function fetchParentRegistrationOverview(): Promise<ParentRegistrationResponse> {
|
||||
return apiFetch<ParentRegistrationResponse>('/api/v1/parents/registration')
|
||||
}
|
||||
|
||||
export async function fetchAttendanceReportForm(): Promise<ParentAttendanceReportFormResponse> {
|
||||
return apiFetch<ParentAttendanceReportFormResponse>(
|
||||
'/api/v1/parents/attendance-reports/form',
|
||||
)
|
||||
}
|
||||
|
||||
export async function submitContactMessage(payload: {
|
||||
name: string
|
||||
email: string
|
||||
subject: string
|
||||
message: string
|
||||
}): Promise<ContactSubmitResponse> {
|
||||
return apiFetch<ContactSubmitResponse>('/api/v1/contact', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export async function fetchParentEmergencyContacts(): Promise<ParentEmergencyContactsResponse> {
|
||||
return apiFetch<ParentEmergencyContactsResponse>('/api/v1/parents/emergency-contacts')
|
||||
}
|
||||
|
||||
export async function fetchParentAttendanceReportsList(
|
||||
query?: Record<string, string>,
|
||||
): Promise<ParentAttendanceReportsListResponse> {
|
||||
const q =
|
||||
query && Object.keys(query).length > 0
|
||||
? `?${new URLSearchParams(query).toString()}`
|
||||
: ''
|
||||
return apiFetch<ParentAttendanceReportsListResponse>(
|
||||
`/api/v1/parents/attendance-reports${q}`,
|
||||
)
|
||||
}
|
||||
|
||||
export async function fetchParentAuthorizedUsers(): Promise<ParentAuthorizedUsersApiResponse> {
|
||||
return apiFetch<ParentAuthorizedUsersApiResponse>('/api/v1/parents/authorized-users')
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
export type AuthUser = {
|
||||
id: number
|
||||
name: string
|
||||
roles: Record<string, boolean>
|
||||
}
|
||||
|
||||
export type LoginSuccess = {
|
||||
status: true
|
||||
token: string
|
||||
user: AuthUser
|
||||
}
|
||||
|
||||
export type LoginFailure = {
|
||||
status: false
|
||||
message?: string
|
||||
}
|
||||
|
||||
export type LoginResponse = LoginSuccess | LoginFailure
|
||||
|
||||
export type ApiEnvelope<T> = {
|
||||
status: boolean
|
||||
message?: string
|
||||
data?: T
|
||||
}
|
||||
|
||||
export type DashboardPayload = {
|
||||
dashboard: {
|
||||
route: string | null
|
||||
role: string | null
|
||||
role_slug: string | null
|
||||
roles: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export type NavItem = {
|
||||
id: number
|
||||
menu_parent_id: number | null
|
||||
label: string
|
||||
url: string | null
|
||||
icon_class: string | null
|
||||
target: string | null
|
||||
sort_order: number
|
||||
is_enabled: number
|
||||
children: NavItem[]
|
||||
}
|
||||
|
||||
export type NavMenuResponse = ApiEnvelope<{ items: NavItem[] }>
|
||||
|
||||
export type ParentAttendanceRow = {
|
||||
firstname: string
|
||||
lastname: string
|
||||
date: string
|
||||
status: string
|
||||
reason: string | null
|
||||
}
|
||||
|
||||
export type ParentAttendanceResponse = {
|
||||
ok: true
|
||||
attendance: ParentAttendanceRow[]
|
||||
schoolYears: { school_year: string }[]
|
||||
selectedYear: string | null
|
||||
}
|
||||
|
||||
/** Laravel `User::toArray()` for the logged-in parent (password hidden server-side). */
|
||||
export type ParentProfileRecord = {
|
||||
id?: number
|
||||
firstname?: string | null
|
||||
lastname?: string | null
|
||||
email?: string | null
|
||||
cellphone?: string | null
|
||||
city?: string | null
|
||||
state?: string | null
|
||||
zip?: string | null
|
||||
address_street?: string | null
|
||||
}
|
||||
|
||||
export type ParentProfileResponse = {
|
||||
ok: true
|
||||
profile: ParentProfileRecord | null
|
||||
}
|
||||
|
||||
export type ParentInvoiceRow = {
|
||||
id: number
|
||||
invoice_number?: string | null
|
||||
balance?: number
|
||||
status?: string | null
|
||||
due_date?: string | null
|
||||
}
|
||||
|
||||
export type ParentInvoicesResponse = {
|
||||
ok: true
|
||||
invoices: ParentInvoiceRow[]
|
||||
}
|
||||
|
||||
export type ParentEnrollmentStudent = {
|
||||
id: number
|
||||
firstname: string
|
||||
lastname: string
|
||||
class_section?: string | null
|
||||
}
|
||||
|
||||
export type ParentEnrollmentsResponse = {
|
||||
ok: true
|
||||
students: ParentEnrollmentStudent[]
|
||||
schoolYears: { school_year: string }[]
|
||||
selectedYear: string | null
|
||||
}
|
||||
|
||||
export type ContactSubmitResponse = {
|
||||
status: boolean
|
||||
message?: string
|
||||
}
|
||||
|
||||
/** Loose JSON payloads for complex parent dashboards (events, registration, attendance report form). */
|
||||
export type ParentEventsResponse = Record<string, unknown>
|
||||
export type ParentRegistrationResponse = Record<string, unknown>
|
||||
export type ParentAttendanceReportFormResponse = Record<string, unknown>
|
||||
|
||||
export type ParentEmergencyContactRow = {
|
||||
id: number
|
||||
name?: string
|
||||
cellphone?: string
|
||||
email?: string | null
|
||||
relation?: string | null
|
||||
}
|
||||
|
||||
export type ParentEmergencyContactsResponse = {
|
||||
ok: true
|
||||
contacts: ParentEmergencyContactRow[]
|
||||
}
|
||||
|
||||
export type ParentAttendanceReportsListResponse = {
|
||||
ok: true
|
||||
reports: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
/** `GET /api/v1/parents/authorized-users` — envelope from BaseApiController. */
|
||||
export type ParentAuthorizedUsersApiResponse = ApiEnvelope<unknown[]>
|
||||
Reference in New Issue
Block a user