fix teacher, parent and admin pages
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Link, useSearchParams } from 'react-router-dom'
|
||||
import { ApiHttpError } from '../../api/http'
|
||||
import { applyDiscountVoucher, fetchDiscountApplyContext } from '../../api/session'
|
||||
import type { DiscountApplyParentRow, DiscountVoucherRow } from '../../api/types'
|
||||
import { AcademicFilterBar } from './AcademicFilterBar'
|
||||
|
||||
export function DiscountApplyVoucherPage() {
|
||||
const [searchParams] = useSearchParams()
|
||||
const schoolYear = searchParams.get('school_year') ?? undefined
|
||||
const semester = searchParams.get('semester') ?? undefined
|
||||
|
||||
const [vouchers, setVouchers] = useState<DiscountVoucherRow[]>([])
|
||||
const [voucherId, setVoucherId] = useState('')
|
||||
const [allowAdditional, setAllowAdditional] = useState(true)
|
||||
const [parents, setParents] = useState<DiscountApplyParentRow[]>([])
|
||||
const [selected, setSelected] = useState<Record<string, boolean>>({})
|
||||
const [checkAll, setCheckAll] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [msg, setMsg] = useState<string | null>(null)
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
setLoading(true)
|
||||
fetchDiscountApplyContext({ school_year: schoolYear, semester })
|
||||
.then((d) => {
|
||||
if (cancelled) return
|
||||
setParents(d.parents ?? [])
|
||||
setVouchers(d.vouchers ?? [])
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
if (!cancelled)
|
||||
setError(e instanceof ApiHttpError ? e.message : 'Could not load parents or vouchers.')
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false)
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [schoolYear, semester])
|
||||
|
||||
const selectableParents = useMemo(
|
||||
() =>
|
||||
parents.filter((p) => {
|
||||
if (!p.has_discount) return true
|
||||
return allowAdditional
|
||||
}),
|
||||
[parents, allowAdditional],
|
||||
)
|
||||
|
||||
function toggleParent(id: string, enabled: boolean) {
|
||||
if (!enabled) return
|
||||
setSelected((s) => ({ ...s, [id]: !s[id] }))
|
||||
}
|
||||
|
||||
function onCheckAll(checked: boolean) {
|
||||
setCheckAll(checked)
|
||||
const next: Record<string, boolean> = {}
|
||||
if (checked) {
|
||||
for (const p of selectableParents) {
|
||||
next[String(p.id)] = true
|
||||
}
|
||||
}
|
||||
setSelected(next)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setSelected({})
|
||||
setCheckAll(false)
|
||||
}, [allowAdditional, parents])
|
||||
|
||||
async function onSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
if (!voucherId) return
|
||||
const ids = Object.entries(selected)
|
||||
.filter(([, on]) => on)
|
||||
.map(([id]) => id)
|
||||
if (ids.length === 0) {
|
||||
setMsg('Select at least one parent.')
|
||||
return
|
||||
}
|
||||
setBusy(true)
|
||||
setMsg(null)
|
||||
try {
|
||||
await applyDiscountVoucher({
|
||||
voucher_id: voucherId,
|
||||
parent_ids: ids,
|
||||
allow_additional: allowAdditional,
|
||||
school_year: schoolYear,
|
||||
semester,
|
||||
})
|
||||
setMsg('Voucher applied.')
|
||||
} catch (err: unknown) {
|
||||
setMsg(err instanceof ApiHttpError ? err.message : 'Apply failed.')
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<h1>Apply Discount Voucher</h1>
|
||||
<AcademicFilterBar />
|
||||
|
||||
{loading ? <p className="text-muted">Loading…</p> : null}
|
||||
{error ? <div className="alert alert-danger">{error}</div> : null}
|
||||
{msg ? (
|
||||
<div className={`alert ${msg.includes('fail') || msg.includes('Select') ? 'alert-warning' : 'alert-success'}`}>
|
||||
{msg}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="d-flex gap-2 mb-3 justify-content-end">
|
||||
<button type="submit" className="btn btn-success" disabled={busy || loading}>
|
||||
Apply Voucher
|
||||
</button>
|
||||
<Link to="/app/administrator/discounts" className="btn btn-info">
|
||||
Cancel
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="form-label" htmlFor="voucher_id">
|
||||
Select Voucher Code
|
||||
</label>
|
||||
<select
|
||||
name="voucher_id"
|
||||
id="voucher_id"
|
||||
className="form-select"
|
||||
required
|
||||
value={voucherId}
|
||||
onChange={(ev) => setVoucherId(ev.target.value)}
|
||||
>
|
||||
<option value="">-- Select Voucher --</option>
|
||||
{vouchers.map((v) => (
|
||||
<option key={String(v.id)} value={String(v.id)}>
|
||||
{v.code} ({String(v.discount_type)} —{' '}
|
||||
{v.discount_type === 'percent' ? `${v.discount_value}%` : `$${v.discount_value}`})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-check mb-3">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
id="allowAdditional"
|
||||
checked={allowAdditional}
|
||||
onChange={(ev) => setAllowAdditional(ev.target.checked)}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="allowAdditional">
|
||||
Allow additional discounts for parents who already have discounts
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Select Parents</label>
|
||||
<div className="table-responsive">
|
||||
<table className="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checkAll}
|
||||
onChange={(ev) => onCheckAll(ev.target.checked)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
</th>
|
||||
<th>Parent ID</th>
|
||||
<th>Parent Name</th>
|
||||
<th>Email</th>
|
||||
<th>Has Discount?</th>
|
||||
<th>Total Discount Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{parents.map((parent) => {
|
||||
const id = String(parent.id)
|
||||
const hasDisc = Boolean(parent.has_discount)
|
||||
const enabled = !hasDisc || allowAdditional
|
||||
return (
|
||||
<tr key={id}>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
name={`parent_${id}`}
|
||||
className="form-check-input"
|
||||
checked={Boolean(selected[id])}
|
||||
disabled={!enabled}
|
||||
onChange={() => toggleParent(id, enabled)}
|
||||
/>
|
||||
</td>
|
||||
<td>{parent.school_id ?? 'N/A'}</td>
|
||||
<td>
|
||||
{[parent.firstname, parent.lastname].filter(Boolean).join(' ') || '—'}
|
||||
</td>
|
||||
<td>{parent.email ?? '—'}</td>
|
||||
<td>{hasDisc ? 'Yes' : 'No'}</td>
|
||||
<td>${Number(parent.total_discount ?? 0).toFixed(2)}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user