228 lines
9.2 KiB
TypeScript
228 lines
9.2 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { Link } from 'react-router-dom'
|
|
import {
|
|
fetchWhatsappParentContactsByClass,
|
|
updateWhatsappMembership,
|
|
type ParentContactsByClassParentRow,
|
|
type ParentContactsByClassSection,
|
|
} from '../../api/whatsapp'
|
|
|
|
function digitsOnly(phone: string): string {
|
|
return phone.replace(/\D+/g, '')
|
|
}
|
|
|
|
function memClass(v: string): string {
|
|
if (v === '1') return 'mem-yes'
|
|
if (v === '0') return 'mem-no'
|
|
return 'mem-none'
|
|
}
|
|
|
|
export function WhatsappParentContactsByClassPage() {
|
|
const [sections, setSections] = useState<ParentContactsByClassSection[]>([])
|
|
const [openIndex, setOpenIndex] = useState(0)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [statusByKey, setStatusByKey] = useState<Record<string, string>>({})
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
;(async () => {
|
|
try {
|
|
const data = await fetchWhatsappParentContactsByClass()
|
|
if (cancelled) return
|
|
setSections(data.classSections ?? [])
|
|
setError(null)
|
|
} catch (e) {
|
|
if (!cancelled) setError(e instanceof Error ? e.message : 'Unable to load data.')
|
|
}
|
|
})()
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [])
|
|
|
|
async function saveRow(
|
|
section: ParentContactsByClassSection,
|
|
p: ParentContactsByClassParentRow,
|
|
primaryVal: string,
|
|
secondVal: string,
|
|
rowKey: string,
|
|
) {
|
|
const class_section_id = Number(p.class_section_id ?? section.class_section_id ?? 0)
|
|
try {
|
|
const res = await updateWhatsappMembership({
|
|
class_section_id,
|
|
school_year: section.school_year,
|
|
semester: section.semester,
|
|
primary_id: Number(p.primary_id ?? 0),
|
|
second_id: Number(p.second_id ?? 0),
|
|
primary_member: primaryVal,
|
|
second_member: secondVal,
|
|
})
|
|
const ok = res.status === 'ok'
|
|
setStatusByKey((prev) => ({
|
|
...prev,
|
|
[rowKey]: ok ? 'Saved' : res.message ?? 'Nothing to save',
|
|
}))
|
|
} catch {
|
|
setStatusByKey((prev) => ({ ...prev, [rowKey]: 'Save failed' }))
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="container-fluid">
|
|
<style>{`
|
|
.mem-select.form-select.form-select-sm {
|
|
display: inline-block;
|
|
width: auto;
|
|
min-width: 52px;
|
|
font-size: 0.75rem;
|
|
padding-left: 4px;
|
|
padding-right: 12px;
|
|
line-height: 1;
|
|
border: 1px solid #adb5bd;
|
|
}
|
|
.mem-select.mem-yes {
|
|
background: #157347;
|
|
color: #fff;
|
|
border-color: #0f5132;
|
|
}
|
|
.mem-select.mem-no {
|
|
background: #bb2d3b;
|
|
color: #fff;
|
|
border-color: #842029;
|
|
}
|
|
.mem-select.mem-none {
|
|
background: #fff;
|
|
color: #212529;
|
|
border-color: #adb5bd;
|
|
}
|
|
`}</style>
|
|
<div className="wrapper py-3">
|
|
<h2 className="text-center mt-4 mb-3">Parent Contacts by Class</h2>
|
|
<Link className="btn btn-outline-secondary btn-sm mb-3" to="/app/whatsapp/manage-links">
|
|
<i className="bi bi-people" /> WhatsApp Group Links
|
|
</Link>
|
|
|
|
{error ? <div className="alert alert-danger">{error}</div> : null}
|
|
|
|
{sections.length === 0 && !error ? <p>No class sections available.</p> : null}
|
|
|
|
<div id="classAccordion">
|
|
{sections.map((classSection, index) => (
|
|
<div key={`${classSection.class_section_name}-${index}`} className="card mb-2">
|
|
<div className="card-header" id={`heading-${index}`}>
|
|
<h2 className="mb-0">
|
|
<button
|
|
className="btn btn-link w-100 text-start"
|
|
type="button"
|
|
aria-expanded={openIndex === index}
|
|
onClick={() => setOpenIndex(openIndex === index ? -1 : index)}
|
|
>
|
|
Grade : {classSection.class_section_name}
|
|
</button>
|
|
</h2>
|
|
</div>
|
|
|
|
{openIndex === index ? (
|
|
<div className="card-body">
|
|
{(classSection.parents ?? []).length === 0 ? (
|
|
<p>No parents found for this class section.</p>
|
|
) : (
|
|
<div className="table-responsive">
|
|
<table className="table table-striped table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Primary Parent</th>
|
|
<th>Phone</th>
|
|
<th>In Group (Primary)</th>
|
|
<th>Second Parent</th>
|
|
<th>Phone</th>
|
|
<th>In Group (Second)</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{(classSection.parents ?? []).map((p, rIdx) => {
|
|
const rowKey = `${index}-${rIdx}`
|
|
const pm = (p.primary_member ?? '') as string
|
|
const sm = (p.second_member ?? '') as string
|
|
return (
|
|
<tr key={rowKey}>
|
|
<td>{p.primary_name}</td>
|
|
<td>
|
|
{p.primary_phone ? (
|
|
<a href={`tel:${digitsOnly(p.primary_phone)}`}>{p.primary_phone}</a>
|
|
) : (
|
|
<span className="text-muted">—</span>
|
|
)}
|
|
</td>
|
|
<td className="text-center">
|
|
<select
|
|
className={`form-select form-select-sm mem-select ${memClass(pm)}`}
|
|
aria-label="Primary in group"
|
|
defaultValue={pm}
|
|
onChange={(e) => {
|
|
const tr = e.target.closest('tr')
|
|
const v = e.target.value
|
|
e.target.className = `form-select form-select-sm mem-select ${memClass(v)}`
|
|
const secondSel = tr?.querySelector<HTMLSelectElement>(
|
|
'select[aria-label="Second in group"]',
|
|
)
|
|
void saveRow(classSection, p, v, secondSel?.value ?? '', rowKey)
|
|
}}
|
|
>
|
|
<option value="">—</option>
|
|
<option value="1">Yes</option>
|
|
<option value="0">No</option>
|
|
</select>
|
|
</td>
|
|
<td>{p.second_name}</td>
|
|
<td>
|
|
{p.second_phone ? (
|
|
<a href={`tel:${digitsOnly(p.second_phone)}`}>{p.second_phone}</a>
|
|
) : (
|
|
<span className="text-muted">—</span>
|
|
)}
|
|
</td>
|
|
<td className="text-center">
|
|
{p.second_id ? (
|
|
<select
|
|
className={`form-select form-select-sm mem-select ${memClass(sm)}`}
|
|
aria-label="Second in group"
|
|
defaultValue={sm}
|
|
onChange={(e) => {
|
|
const tr = e.target.closest('tr')
|
|
const v = e.target.value
|
|
e.target.className = `form-select form-select-sm mem-select ${memClass(v)}`
|
|
const priSel = tr?.querySelector<HTMLSelectElement>(
|
|
'select[aria-label="Primary in group"]',
|
|
)
|
|
void saveRow(classSection, p, priSel?.value ?? '', v, rowKey)
|
|
}}
|
|
>
|
|
<option value="">—</option>
|
|
<option value="1">Yes</option>
|
|
<option value="0">No</option>
|
|
</select>
|
|
) : (
|
|
<span className="text-muted">—</span>
|
|
)}
|
|
</td>
|
|
<td className="text-center small text-muted">{statusByKey[rowKey] ?? ''}</td>
|
|
</tr>
|
|
)
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|