init project

This commit is contained in:
root
2026-04-23 00:21:02 -04:00
parent 23826c5528
commit 9191fd32f0
64 changed files with 10229 additions and 0 deletions
+52
View File
@@ -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
}
+127
View File
@@ -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
}
+122
View File
@@ -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')
}
+138
View File
@@ -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[]>