fix apply the plan docs/alrahma_api_fix_plan_school_year_only_v7
Web Client CI/CD / Lint (ESLint + TypeScript) (push) Successful in 29s
Web Client CI/CD / Build (tsc + Vite) (push) Successful in 30s
Web Client CI/CD / Deploy to shared hosting (push) Successful in 33s

This commit is contained in:
root
2026-07-07 01:52:37 -04:00
parent b510f534fd
commit 4b78aafa65
18 changed files with 664 additions and 256 deletions
+7 -2
View File
@@ -1,5 +1,5 @@
import { apiUrl } from '../lib/apiOrigin'
import { selectedSchoolYearHeaders } from '../lib/schoolYearSelection'
import { selectedSchoolYearHeaders, withSelectedSchoolYearUrl } from '../lib/schoolYearSelection'
const TOKEN_KEY = 'alrahma_api_token'
@@ -39,6 +39,10 @@ export function applyStoredSchoolYearHeaders(headers: Headers): Headers {
return headers
}
export function withStoredSchoolYearUrl(url: string): string {
return withSelectedSchoolYearUrl(url)
}
export class ApiHttpError extends Error {
readonly status: number
readonly body: unknown
@@ -73,7 +77,8 @@ export async function apiFetch<T>(
applyStoredSchoolYearHeaders(headers)
}
const url = apiUrl(path)
const method = String(init.method ?? 'GET').toUpperCase()
const url = apiUrl(method === 'GET' ? withStoredSchoolYearUrl(path) : path)
let res: Response
+70
View File
@@ -0,0 +1,70 @@
import { apiFetch } from './http'
export type ParentProfileSummary = {
id: number
firstname?: string | null
lastname?: string | null
email?: string | null
cellphone?: string | null
is_active?: boolean
families_count?: number
selected_year_students_count?: number
balance?: number
positive_unpaid_balance?: number
credit_balance?: number
primary_family?: { id?: number; household_name?: string | null } | null
}
export type ParentProfileDetail = {
school_year: string
read_only: boolean
parent: Record<string, unknown> | null
families: Record<string, unknown>[]
students: Record<string, unknown>[]
emergency_contacts: Record<string, unknown>[]
invoices: Record<string, unknown>[]
payments: Record<string, unknown>[]
finance_summary: Record<string, unknown>
}
type Envelope<T> = { data?: T }
function unwrap<T>(response: T | Envelope<T>): T {
if (response && typeof response === 'object' && 'data' in response) {
return (response as Envelope<T>).data as T
}
return response as T
}
export async function fetchParentProfiles(params: {
schoolYear: string
q?: string
page?: number
perPage?: number
}): Promise<{
school_year: string
read_only: boolean
parents: ParentProfileSummary[]
pagination: { page: number; per_page: number; total: number }
}> {
const query = new URLSearchParams()
query.set('school_year', params.schoolYear)
if (params.q?.trim()) query.set('q', params.q.trim())
if (params.page) query.set('page', String(params.page))
if (params.perPage) query.set('per_page', String(params.perPage))
return unwrap(await apiFetch(`/api/v1/parent-profiles?${query.toString()}`))
}
export async function fetchParentProfile(parentId: number, schoolYear: string): Promise<ParentProfileDetail> {
const query = new URLSearchParams({ school_year: schoolYear })
return unwrap(await apiFetch(`/api/v1/parent-profiles/${parentId}?${query.toString()}`))
}
export async function updateParentProfile(parentId: number, schoolYear: string, payload: Record<string, unknown>): Promise<ParentProfileDetail> {
const query = new URLSearchParams({ school_year: schoolYear })
return unwrap(await apiFetch(`/api/v1/parent-profiles/${parentId}?${query.toString()}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...payload, school_year: schoolYear }),
}))
}
+64 -31
View File
@@ -20,7 +20,11 @@ export type SchoolYearSummary = {
students_considered: number
students_blocked: number
parents_with_balances: number
parents_with_positive_balance?: number
net_balance_to_transfer: number
net_balance_impact?: number
total_positive_unpaid_balance?: number
total_credit_balance?: number
}
}
@@ -57,19 +61,29 @@ export type SchoolYearParentBalanceRow = {
parent_name?: string | null
student_names?: string[]
old_unpaid_balance: number
positive_unpaid_balance?: number
credit_balance: number
net_balance?: number
amount_to_transfer: number
positive_invoice_ids?: number[]
credit_invoice_ids?: number[]
source_invoice_ids?: number[]
existing_transfer_conflict?: boolean
}
export type SchoolYearParentBalancePreview = {
from_school_year?: string
to_school_year?: string
rows: SchoolYearParentBalanceRow[]
summary: {
parents_with_non_zero_balance: number
parents_with_positive_balance?: number
parents_with_credit_balance: number
total_old_unpaid_balance: number
total_positive_unpaid_balance?: number
total_credit_balance: number
net_balance_to_transfer: number
net_balance_impact?: number
existing_transfer_conflicts: number
}
}
@@ -111,7 +125,9 @@ export type SchoolYearClosingReport = {
parents_with_unpaid_balances?: number
total_unpaid_balance_transferred?: number
parents_with_credit_balances?: number
total_credit_carried_or_reported?: number
total_credit_transferred?: number
net_balance_impact?: number
}
warnings?: string[]
}
@@ -251,14 +267,17 @@ export async function createSchoolYear(payload: SaveSchoolYearPayload): Promise<
}
export async function updateSchoolYear(
schoolYearId: number,
schoolYear: string,
payload: SaveSchoolYearPayload,
): Promise<SchoolYearRecord> {
const response = await apiFetch<ApiRecordResponse>(`/api/v1/school-years/${schoolYearId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
const response = await apiFetch<ApiRecordResponse>(
`/api/v1/school-years/selected${selectedYearQuery(schoolYear)}`,
{
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: selectedYearBody(payload, schoolYear),
},
)
if (!response.data) {
throw new Error('School year update returned no record.')
@@ -267,8 +286,23 @@ export async function updateSchoolYear(
return response.data
}
export async function fetchSchoolYearSummary(schoolYearId: number): Promise<SchoolYearSummary> {
const response = await apiFetch<ApiSummaryResponse>(`/api/v1/school-years/${schoolYearId}/summary`)
function selectedYearQuery(schoolYear: string, extras?: Record<string, string | undefined>): string {
const query = new URLSearchParams()
query.set('school_year', schoolYear)
for (const [key, value] of Object.entries(extras ?? {})) {
if (value) query.set(key, value)
}
return `?${query.toString()}`
}
function selectedYearBody(payload: object, schoolYear: string): string {
return JSON.stringify({ ...payload, school_year: schoolYear })
}
export async function fetchSchoolYearSummary(schoolYear: string): Promise<SchoolYearSummary> {
const response = await apiFetch<ApiSummaryResponse>(
`/api/v1/school-years/summary${selectedYearQuery(schoolYear)}`,
)
if (!response.data) {
throw new Error('School year summary returned no data.')
}
@@ -276,15 +310,15 @@ export async function fetchSchoolYearSummary(schoolYearId: number): Promise<Scho
}
export async function validateSchoolYearClose(
schoolYearId: number,
schoolYear: string,
payload: CloseSchoolYearPayload,
): Promise<SchoolYearCloseValidation> {
const response = await apiFetch<ApiValidationResponse>(
`/api/v1/school-years/${schoolYearId}/validate-close`,
`/api/v1/school-years/validate-close${selectedYearQuery(schoolYear)}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
body: selectedYearBody(payload, schoolYear),
},
)
@@ -296,15 +330,15 @@ export async function validateSchoolYearClose(
}
export async function previewSchoolYearClose(
schoolYearId: number,
schoolYear: string,
payload: CloseSchoolYearPayload,
): Promise<SchoolYearClosePreview> {
const response = await apiFetch<ApiPreviewResponse>(
`/api/v1/school-years/${schoolYearId}/preview-close`,
`/api/v1/school-years/preview-close${selectedYearQuery(schoolYear)}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
body: selectedYearBody(payload, schoolYear),
},
)
@@ -316,13 +350,13 @@ export async function previewSchoolYearClose(
}
export async function closeSchoolYear(
schoolYearId: number,
schoolYear: string,
payload: CloseSchoolYearPayload,
): Promise<SchoolYearCloseResult> {
const response = await apiFetch<ApiCloseResponse>(`/api/v1/school-years/${schoolYearId}/close`, {
const response = await apiFetch<ApiCloseResponse>(`/api/v1/school-years/close${selectedYearQuery(schoolYear)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
body: selectedYearBody(payload, schoolYear),
})
if (!response.data) {
@@ -332,10 +366,11 @@ export async function closeSchoolYear(
return response.data
}
export async function reopenSchoolYear(schoolYearId: number): Promise<SchoolYearRecord> {
const response = await apiFetch<ApiRecordResponse>(`/api/v1/school-years/${schoolYearId}/reopen`, {
export async function reopenSchoolYear(schoolYear: string): Promise<SchoolYearRecord> {
const response = await apiFetch<ApiRecordResponse>(`/api/v1/school-years/reopen${selectedYearQuery(schoolYear)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ school_year: schoolYear }),
})
if (!response.data) {
@@ -346,12 +381,11 @@ export async function reopenSchoolYear(schoolYearId: number): Promise<SchoolYear
}
export async function fetchSchoolYearParentBalances(
schoolYearId: number,
schoolYear: string,
toSchoolYear?: string,
): Promise<SchoolYearParentBalancePreview> {
const query = toSchoolYear ? `?to_school_year=${encodeURIComponent(toSchoolYear)}` : ''
const response = await apiFetch<ApiParentBalancesResponse>(
`/api/v1/school-years/${schoolYearId}/parent-balances${query}`,
`/api/v1/school-years/parent-balances${selectedYearQuery(schoolYear, { to_school_year: toSchoolYear })}`,
)
if (!response.data) {
@@ -362,12 +396,11 @@ export async function fetchSchoolYearParentBalances(
}
export async function fetchSchoolYearPromotionPreview(
schoolYearId: number,
schoolYear: string,
toSchoolYear?: string,
): Promise<SchoolYearPromotionPreview> {
const query = toSchoolYear ? `?to_school_year=${encodeURIComponent(toSchoolYear)}` : ''
const response = await apiFetch<ApiPromotionPreviewResponse>(
`/api/v1/school-years/${schoolYearId}/promotion-preview${query}`,
`/api/v1/school-years/promotion-preview${selectedYearQuery(schoolYear, { to_school_year: toSchoolYear })}`,
)
if (!response.data) {
@@ -379,10 +412,10 @@ export async function fetchSchoolYearPromotionPreview(
export async function fetchSchoolYearClosingReport(
schoolYearId: number,
schoolYear: string,
): Promise<SchoolYearClosingReport> {
const response = await apiFetch<ApiClosingReportResponse>(
`/api/v1/school-years/${schoolYearId}/reports/closing`,
`/api/v1/school-years/reports/closing${selectedYearQuery(schoolYear)}`,
)
if (!response.data) {
@@ -393,10 +426,10 @@ export async function fetchSchoolYearClosingReport(
}
export async function fetchSchoolYearBalanceTransferReport(
schoolYearId: number,
schoolYear: string,
): Promise<SchoolYearBalanceTransferReport> {
const response = await apiFetch<ApiBalanceTransferReportResponse>(
`/api/v1/school-years/${schoolYearId}/parent-balances`,
`/api/v1/school-years/parent-balances${selectedYearQuery(schoolYear)}`,
)
if (!response.data) {
@@ -407,11 +440,11 @@ export async function fetchSchoolYearBalanceTransferReport(
}
export async function fetchParentStatement(params: {
schoolYearId?: number | null
schoolYear?: string | null
parentId?: number | null
}): Promise<ParentStatement> {
const query = new URLSearchParams()
if (params.schoolYearId != null) query.set('school_year_id', String(params.schoolYearId))
if (params.schoolYear) query.set('school_year', params.schoolYear)
if (params.parentId != null) query.set('parent_id', String(params.parentId))
const response = await apiFetch<ApiParentStatementResponse>(
+7 -2
View File
@@ -1722,10 +1722,15 @@ export async function sendFamilyComposeEmail(payload: {
html: string
return_url?: string
}): Promise<{ ok?: boolean; message?: string }> {
return apiFetch(`/api/v1/family-admin/compose-email/send`, {
return apiFetch(`/api/v1/family-admin/compose-email`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
body: JSON.stringify({
recipient: payload.to.trim(),
subject: payload.subject.trim(),
html_message: payload.html,
return_url: payload.return_url,
}),
})
}
+31 -10
View File
@@ -1,10 +1,9 @@
/**
* Staff directory (legacy CI `staff/*`).
* Expected Laravel routes under `/api/v1/administrator/staff`.
* Staff directory.
*/
import { apiFetch } from './http'
const BASE = '/api/v1/administrator/staff'
const BASE = '/api/v1/staff'
function unwrapData<T>(body: T | { data?: T }): T {
if (body && typeof body === 'object' && 'data' in body && (body as { data?: T }).data != null) {
@@ -24,6 +23,7 @@ export type StaffListRow = {
active_role?: string | null
class_section?: string | null
verification_issue?: boolean
status?: string | null
}
export type StaffIndexResponse = {
@@ -32,12 +32,16 @@ export type StaffIndexResponse = {
school_year?: string | null
semester?: string | null
schoolYears?: string[]
meta?: unknown
}
export type RoleOption = { id: number; name: string }
export type RoleOption = { id: number; name: string; label?: string }
export type StaffFormOptionsResponse = {
roles?: RoleOption[]
statuses?: Array<{ value: string; label: string }>
school_year?: string | null
is_read_only_year?: boolean
}
export type StaffMemberResponse = {
@@ -59,28 +63,45 @@ export type StaffMemberResponse = {
export async function fetchStaffIndex(params?: {
school_year?: string
semester?: string
role?: string
status?: string
search?: string
page?: number
per_page?: number
}): 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)
if (params?.role) qs.set('role', params.role)
if (params?.status) qs.set('status', params.status)
if (params?.search) qs.set('search', params.search)
if (params?.page) qs.set('page', String(params.page))
if (params?.per_page) qs.set('per_page', String(params.per_page))
const suffix = qs.toString() ? `?${qs}` : ''
return unwrapData(await apiFetch(`${BASE}${suffix}`))
}
export async function fetchStaffFormOptions(): Promise<StaffFormOptionsResponse> {
return unwrapData(await apiFetch(`${BASE}/form-options`))
export async function fetchStaffFormOptions(params?: { school_year?: string }): Promise<StaffFormOptionsResponse> {
const qs = new URLSearchParams()
if (params?.school_year) qs.set('school_year', params.school_year)
return unwrapData(await apiFetch(`${BASE}/form-options${qs.toString() ? `?${qs}` : ''}`))
}
export async function fetchStaffMember(id: number): Promise<StaffMemberResponse> {
return unwrapData(await apiFetch(`${BASE}/${id}`))
export async function fetchStaffMember(id: number, params?: { school_year?: string }): Promise<StaffMemberResponse> {
const qs = new URLSearchParams()
if (params?.school_year) qs.set('school_year', params.school_year)
return unwrapData(await apiFetch(`${BASE}/${id}${qs.toString() ? `?${qs}` : ''}`))
}
export async function createStaff(payload: {
firstname: string
lastname: string
email: string
phone?: string
password: string
role_id: number
status?: string
school_year: string
}): Promise<{ ok?: boolean; message?: string }> {
return apiFetch(`${BASE}`, {
method: 'POST',
@@ -91,10 +112,10 @@ export async function createStaff(payload: {
export async function updateStaff(
id: number,
payload: { firstname: string; lastname: string; email: string; phone?: string },
payload: { firstname: string; lastname: string; email: string; phone?: string; role_id?: number; status?: string; school_year: string },
): Promise<{ ok?: boolean; message?: string }> {
return apiFetch(`${BASE}/${id}`, {
method: 'PUT',
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})