cleanup codeigniter code

This commit is contained in:
root
2026-06-04 02:55:19 -04:00
parent 4dde38004f
commit 1af02dadd3
31 changed files with 824 additions and 756 deletions
-47
View File
@@ -1,47 +0,0 @@
import { Link, useParams } from 'react-router-dom'
import { getCiMeta } from '../lib/ciRouteLookup'
/** Fallback for CI-style paths not yet ported to dedicated React pages. */
export function CiPlaceholderPage() {
const params = useParams()
const star = params['*'] ?? ''
const segments = star.split('/').filter(Boolean)
const meta = getCiMeta(star)
const title = meta?.title ?? 'Section placeholder'
return (
<div>
<nav aria-label="breadcrumb">
<ol className="breadcrumb">
<li className="breadcrumb-item">
<Link to="/app/home">Home</Link>
</li>
{segments.map((s, i) => (
<li
key={`${s}-${i}`}
className={`breadcrumb-item ${i === segments.length - 1 ? 'active' : ''}`}
aria-current={i === segments.length - 1 ? 'page' : undefined}
>
{s}
</li>
))}
</ol>
</nav>
<h2 className="h4 mb-3">{title}</h2>
<p className="text-muted small mb-3">
Implement this screen using the matching routes in <code>app_laravel/routes/api.php</code>{' '}
(JWT <code>Authorization: Bearer</code>). See also <Link to="/docs">API docs</Link>.
</p>
<p className="small mb-3">
SPA path: <code>/app/{star || '…'}</code>
</p>
<p className="small mb-0">
<Link to="/app/browse">Browse all CI view routes</Link>
{' · '}
</p>
</div>
)
}
-1
View File
@@ -2,7 +2,6 @@ import { type FormEvent, useEffect, useRef, useState } from 'react'
import { Link } from 'react-router-dom'
import { requestForgotPassword } from '../api/passwordFlow'
/** Mirrors CodeIgniter `user/forgot_password.php` + confirmation modal from `password_reset_confirmation.php`. */
export function ForgotPasswordPage() {
const [email, setEmail] = useState('')
const [error, setError] = useState<string | null>(null)
-1
View File
@@ -1,4 +1,3 @@
/* Ported from CodeIgniter `Views/index.php` — scoped to avoid overriding global layout. */
.home-landing {
--home-primary: #3a8fd1;
--home-secondary: #2c5e8a;
+1 -1
View File
@@ -12,7 +12,7 @@ function userMayLoadAdministratorHomeMetrics(user: AuthUser | null): boolean {
return Boolean(r.administrator || r.admin || r.super_admin)
}
/** Carousel + sections ported from CodeIgniter `Views/index.php` (Al Rahma public home). */
/** Carousel + sections adapted from the previous public home content. */
const CAROUSEL_SLIDES = [
{
image: '/images/carousel-1.png',
-1
View File
@@ -4,7 +4,6 @@ import { fetchDashboardRoute } from '../api/session'
import { useAuth } from '../auth/AuthProvider'
import { spaPathFromCiUrl } from '../lib/ciSpaPaths'
/** Matches CodeIgniter `Views/user/login.php` (JWT via `/api/v1/auth/login`). */
export function LoginPage() {
const { login } = useAuth()
const navigate = useNavigate()
-1
View File
@@ -15,7 +15,6 @@ function formatUsPhone(input: string): string {
return `${raw.slice(0, 3)}-${raw.slice(3, 6)}-${raw.slice(6)}`
}
/** Matches CodeIgniter `Views/user/register.php` + Laravel `POST /api/v1/auth/register` (captcha flow). */
export function RegisterPage() {
const navigate = useNavigate()
+49
View File
@@ -0,0 +1,49 @@
import { Link, useParams } from 'react-router-dom'
import { getViewRouteMeta } from '../lib/viewRouteLookup'
/** Fallback for mapped source-view paths not yet ported to dedicated React pages. */
export function ViewRoutePlaceholderPage() {
const params = useParams()
const star = params['*'] ?? ''
const segments = star.split('/').filter(Boolean)
const meta = getViewRouteMeta(star)
const title = meta?.title ?? 'Section placeholder'
return (
<div>
<nav aria-label="breadcrumb">
<ol className="breadcrumb">
<li className="breadcrumb-item">
<Link to="/app/home">Home</Link>
</li>
{segments.map((segment, index) => (
<li
key={
segment + '-' + index
}
className={
'breadcrumb-item ' + (index === segments.length - 1 ? 'active' : '')
}
aria-current={index === segments.length - 1 ? 'page' : undefined}
>
{segment}
</li>
))}
</ol>
</nav>
<h2 className="h4 mb-3">{title}</h2>
<p className="text-muted small mb-3">
Implement this screen using the matching Laravel routes and API endpoints (JWT{' '}
<code>Authorization: Bearer</code>). See also <Link to="/docs">API docs</Link>.
</p>
<p className="small mb-3">
SPA path: <code>/app/{star || '…'}</code>
</p>
<p className="small mb-0">
<Link to="/app/browse">Browse all mapped view routes</Link>
</p>
</div>
)
}
@@ -1,60 +1,59 @@
import { useMemo } from 'react'
import { Link } from 'react-router-dom'
import { ciRouteMeta } from '../lib/ciRouteMeta.generated'
import { ciRegistryKeyToAppPath } from '../lib/ciRouteLookup'
/** Browse every CodeIgniter view file mapped under `/app/…` (parity placeholders unless a dedicated route exists). */
export function CiSitemapPage() {
const grouped = useMemo(() => {
const g = new Map<string, { key: string; title: string }[]>()
for (const key of Object.keys(ciRouteMeta).sort()) {
const top = key.includes('/') ? key.slice(0, key.indexOf('/')) : key
const title = ciRouteMeta[key]?.title ?? key
const list = g.get(top) ?? []
list.push({ key, title })
g.set(top, list)
}
return [...g.entries()].sort(([a], [b]) => a.localeCompare(b))
}, [])
return (
<div>
<nav aria-label="breadcrumb">
<ol className="breadcrumb">
<li className="breadcrumb-item">
<Link to="/app/home">Home</Link>
</li>
<li className="breadcrumb-item active" aria-current="page">
CI views
</li>
</ol>
</nav>
<p className="text-muted small mb-4">
Generated from <code>app_codeigniter/app/Views/**/*.php</code>. Each link opens the parity
route under <code>/app/</code>; unimplemented screens use the section placeholder with the
matching view path. Regenerate: <code>npm run gen:ci-meta</code>.
</p>
<div className="row g-4">
{grouped.map(([section, items]) => (
<div key={section} className="col-12 col-md-6 col-xl-4">
<div className="border rounded bg-white shadow-sm">
<div className="px-3 py-2 border-bottom small fw-semibold text-muted">{section}</div>
<ul className="list-unstyled mb-0 px-2 py-2" style={{ maxHeight: 280, overflow: 'auto' }}>
{items.map(({ key, title }) => (
<li key={key} className="small py-1">
<Link to={ciRegistryKeyToAppPath(key)}>{title}</Link>
<div className="text-muted" style={{ fontSize: '0.75rem' }}>
<code>{key.replace(/\//g, ' / ')}</code>
</div>
</li>
))}
</ul>
</div>
</div>
))}
</div>
</div>
)
}
import { useMemo } from 'react'
import { Link } from 'react-router-dom'
import { viewRouteMeta } from '../lib/viewRouteMeta.generated'
import { registryKeyToAppPath } from '../lib/viewRouteLookup'
export function ViewRouteSitemapPage() {
const grouped = useMemo(() => {
const groups = new Map<string, { key: string; title: string }[]>()
for (const key of Object.keys(viewRouteMeta).sort()) {
const top = key.includes('/') ? key.slice(0, key.indexOf('/')) : key
const title = viewRouteMeta[key]?.title ?? key
const list = groups.get(top) ?? []
list.push({ key, title })
groups.set(top, list)
}
return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b))
}, [])
return (
<div>
<nav aria-label="breadcrumb">
<ol className="breadcrumb">
<li className="breadcrumb-item">
<Link to="/app/home">Home</Link>
</li>
<li className="breadcrumb-item active" aria-current="page">
View routes
</li>
</ol>
</nav>
<p className="text-muted small mb-4">
Generated from discovered PHP view sources. Each link opens the mapped route under{' '}
<code>/app/</code>; unimplemented screens use the section placeholder with the matching
source view path. Regenerate: <code>npm run gen:view-meta</code>.
</p>
<div className="row g-4">
{grouped.map(([section, items]) => (
<div key={section} className="col-12 col-md-6 col-xl-4">
<div className="border rounded bg-white shadow-sm">
<div className="px-3 py-2 border-bottom small fw-semibold text-muted">{section}</div>
<ul className="list-unstyled mb-0 px-2 py-2" style={{ maxHeight: 280, overflow: 'auto' }}>
{items.map(({ key, title }) => (
<li key={key} className="small py-1">
<Link to={registryKeyToAppPath(key)}>{title}</Link>
<div className="text-muted" style={{ fontSize: '0.75rem' }}>
<code>{key.replace(/\//g, ' / ')}</code>
</div>
</li>
))}
</ul>
</div>
</div>
))}
</div>
</div>
)
}
-1
View File
@@ -1,4 +1,3 @@
/* Mirrors CodeIgniter `layout/email_layout.php` — scoped for admin preview only */
.email-preview-root {
font-family: Arial, sans-serif;
background-color: #f9f9f9;
@@ -26,7 +26,7 @@ export function EmailTemplatePreviewPage() {
<h2 className="h5 mb-0">{entry.title}</h2>
</div>
<p className="small text-muted mb-3">
CI: <code>{entry.ciView}</code>
Source template: <code>{entry.sourceTemplatePath}</code>
</p>
<EmailPreviewLayout>{entry.render()}</EmailPreviewLayout>
</div>
+7 -5
View File
@@ -8,8 +8,8 @@ export function EmailTemplatesIndexPage() {
<div className="container-fluid py-3">
<h2 className="h4 mb-3">Email templates (preview)</h2>
<p className="text-muted small mb-4">
React previews matching CodeIgniter <code>Views/emails/*.php</code>. Laravel should render the same HTML for
outgoing mail; this UI is for QA and copy review.
React previews matching the current PHP email templates. Laravel should render the same HTML
for outgoing mail; this UI is for QA and copy review.
</p>
<div className="mb-3">
<Link to="/app/administrator/parent-email-extractor" className="btn btn-outline-primary btn-sm">
@@ -21,7 +21,7 @@ export function EmailTemplatesIndexPage() {
<thead>
<tr>
<th>Template</th>
<th>CI view</th>
<th>Source template</th>
<th />
</tr>
</thead>
@@ -32,10 +32,12 @@ export function EmailTemplatesIndexPage() {
<tr key={slug}>
<td>{row.title}</td>
<td>
<code className="small">{row.ciView}</code>
<code className="small">{row.sourceTemplatePath}</code>
</td>
<td className="text-end">
<Link className="btn btn-sm btn-outline-secondary" to={`/app/administrator/email-templates/${slug}`}>
<Link className="btn btn-sm btn-outline-secondary" to={
'/app/administrator/email-templates/' + slug
}>
Preview
</Link>
</td>
+32 -32
View File
@@ -71,164 +71,164 @@ export type EmailTemplateSlug =
export type RegistryEntry = {
title: string
ciView: string
sourceTemplatePath: string
render: () => JSX.Element
}
export const EMAIL_TEMPLATE_REGISTRY: Record<EmailTemplateSlug, RegistryEntry> = {
'final-warning': {
title: 'Final attendance warning',
ciView: 'app/Views/emails/final_warning.php',
sourceTemplatePath: 'emails/final_warning.php',
render: () => <FinalWarningBody />,
},
'follow-up': {
title: 'Attendance follow-up',
ciView: 'app/Views/emails/follow_up.php',
sourceTemplatePath: 'emails/follow_up.php',
render: () => <FollowUpBody />,
},
dismissal: {
title: 'Dismissal (absences)',
ciView: 'app/Views/emails/dismissal.php',
sourceTemplatePath: 'emails/dismissal.php',
render: () => <DismissalBody />,
},
'parent-attendance-admin': {
title: 'Parent attendance — admin notification',
ciView: 'app/Views/emails/parent_attendance_admin.php',
sourceTemplatePath: 'emails/parent_attendance_admin.php',
render: () => <ParentAttendanceAdminBody />,
},
'parent-attendance-parent': {
title: 'Parent attendance — confirmation to parent',
ciView: 'app/Views/emails/parent_attendance_parent.php',
sourceTemplatePath: 'emails/parent_attendance_parent.php',
render: () => <ParentAttendanceParentBody />,
},
'below-sixty-performance': {
title: 'Below 60 performance',
ciView: 'app/Views/emails/below_sixty_performance.php',
sourceTemplatePath: 'emails/below_sixty_performance.php',
render: () => <BelowSixtyPerformanceBody />,
},
'payment-receipt': {
title: 'Payment receipt',
ciView: 'app/Views/emails/payment_receipt.php',
sourceTemplatePath: 'emails/payment_receipt.php',
render: () => <PaymentReceiptBody />,
},
'extra-charge-notice': {
title: 'Extra charge notice',
ciView: 'app/Views/emails/extra_charge_notice.php',
sourceTemplatePath: 'emails/extra_charge_notice.php',
render: () => <ExtraChargeNoticeBody />,
},
'reset-password': {
title: 'Reset password',
ciView: 'app/Views/emails/reset_password.php',
sourceTemplatePath: 'emails/reset_password.php',
render: () => <ResetPasswordBody />,
},
'welcome-parent': {
title: 'Welcome — parent activation',
ciView: 'app/Views/emails/welcome_parent.php',
sourceTemplatePath: 'emails/welcome_parent.php',
render: () => <WelcomeParentBody />,
},
'welcome-staff': {
title: 'Welcome — staff activation',
ciView: 'app/Views/emails/welcome_staff.php',
sourceTemplatePath: 'emails/welcome_staff.php',
render: () => <WelcomeStaffBody />,
},
'welcome-user': {
title: 'Welcome — generic user',
ciView: 'app/Views/emails/welcome_user.php',
sourceTemplatePath: 'emails/welcome_user.php',
render: () => <WelcomeUserBody />,
},
'student-removed': {
title: 'Student removed',
ciView: 'app/Views/emails/student_removed.php',
sourceTemplatePath: 'emails/student_removed.php',
render: () => <StudentRemovedBody />,
},
'support-new-account': {
title: 'Support — new account alert',
ciView: 'app/Views/emails/support_new_account.php',
sourceTemplatePath: 'emails/support_new_account.php',
render: () => <SupportNewAccountBody />,
},
'admin-student-registered': {
title: 'Admin — student registered',
ciView: 'app/Views/emails/admin_student_registered.php',
sourceTemplatePath: 'emails/admin_student_registered.php',
render: () => <AdminStudentRegisteredBody />,
},
'status-admission-review': {
title: 'Status — admission under review',
ciView: 'app/Views/emails/status_admission_review.php',
sourceTemplatePath: 'emails/status_admission_review.php',
render: () => <StatusAdmissionReviewBody />,
},
'status-denied': {
title: 'Status — admission denied',
ciView: 'app/Views/emails/status_denied.php',
sourceTemplatePath: 'emails/status_denied.php',
render: () => <StatusDeniedBody />,
},
'status-enrolled': {
title: 'Status — enrolled',
ciView: 'app/Views/emails/status_enrolled.php',
sourceTemplatePath: 'emails/status_enrolled.php',
render: () => <StatusEnrolledBody />,
},
'status-not-enrolled': {
title: 'Status — not enrolled',
ciView: 'app/Views/emails/status_not_enrolled.php',
sourceTemplatePath: 'emails/status_not_enrolled.php',
render: () => <StatusNotEnrolledBody />,
},
'status-payment-pending': {
title: 'Status — payment pending',
ciView: 'app/Views/emails/status_payment_pending.php',
sourceTemplatePath: 'emails/status_payment_pending.php',
render: () => <StatusPaymentPendingBody />,
},
'status-refund-pending': {
title: 'Status — refund pending',
ciView: 'app/Views/emails/status_refund_pending.php',
sourceTemplatePath: 'emails/status_refund_pending.php',
render: () => <StatusRefundPendingBody />,
},
'status-waitlist': {
title: 'Status — waitlist',
ciView: 'app/Views/emails/status_waitlist.php',
sourceTemplatePath: 'emails/status_waitlist.php',
render: () => <StatusWaitlistBody />,
},
'status-withdraw-review': {
title: 'Status — withdraw under review',
ciView: 'app/Views/emails/status_withdraw_review.php',
sourceTemplatePath: 'emails/status_withdraw_review.php',
render: () => <StatusWithdrawReviewBody />,
},
'status-withdrawn': {
title: 'Status — withdrawn',
ciView: 'app/Views/emails/status_withdrawn.php',
sourceTemplatePath: 'emails/status_withdrawn.php',
render: () => <StatusWithdrawnBody />,
},
'calendar-notification': {
title: 'Calendar notification',
ciView: 'app/Views/emails/calendar_notification.php',
sourceTemplatePath: 'emails/calendar_notification.php',
render: () => <CalendarNotificationBody />,
},
'event-broadcast': {
title: 'Event broadcast',
ciView: 'app/Views/emails/event_broadcast.php',
sourceTemplatePath: 'emails/event_broadcast.php',
render: () => <EventBroadcastBody />,
},
'custom-html': {
title: 'Custom HTML body',
ciView: 'app/Views/emails/custom_html.php',
sourceTemplatePath: 'emails/custom_html.php',
render: () => <CustomHtmlBody />,
},
'wrap-layout-sample': {
title: 'Wrap layout ($body_html)',
ciView: 'app/Views/emails/_wrap_layout.php',
sourceTemplatePath: 'emails/_wrap_layout.php',
render: () => <WrapLayoutSampleBody />,
},
'broadcast-wrapper-sample': {
title: 'Broadcast wrapper ($content)',
ciView: 'app/Views/emails/broadcast_wrapper.php',
sourceTemplatePath: 'emails/broadcast_wrapper.php',
render: () => <BroadcastWrapperSampleBody />,
},
'whatsapp-invite': {
title: 'WhatsApp invite',
ciView: 'app/Views/emails/whatsapp_invite.php',
sourceTemplatePath: 'emails/whatsapp_invite.php',
render: () => <WhatsAppInviteBody />,
},
'whatsapp-group-invitation': {
title: 'WhatsApp group invitation (draft text)',
ciView: 'app/Views/emails/whatsapp_group_invitation.php',
sourceTemplatePath: 'emails/whatsapp_group_invitation.php',
render: () => <WhatsAppGroupInvitationDraftBody />,
},
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { Link } from 'react-router-dom'
/** Port of CodeIgniter `Views/errors/access_denied.php`. */
export function AccessDeniedPage() {
return (
<div className="container mt-5">
-1
View File
@@ -1,6 +1,5 @@
import { Link } from 'react-router-dom'
/** Port of CodeIgniter `Views/errors/invalid_token.php` (registration token invalid/expired). */
export function InvalidTokenPage() {
return (
<div className="registration-form container mt-5 mb-5">
@@ -1,9 +1,5 @@
import { useEffect, useMemo, useState } from 'react'
/**
* Vendor / retailor UX from CodeIgniter `expenses/create.php` and `edit.php`
* (dropdown + Other text field).
*/
export function ExpenseVendorFields({
retailors,
initialRetailor,
+12 -13
View File
@@ -1,13 +1,12 @@
import type { ReactNode } from 'react'
export type ParentParitySpec = {
title: string
/** File name under `app/Views/parent/` (e.g. `calendar.php`). */
ciViewFile: string
/** Example CodeIgniter URL paths (with leading slash). */
legacyCiRoutes?: string[]
}
title: string
/** File name under `app/Views/parent/` (e.g. `calendar.php`). */
ciViewFile: string
legacyCiRoutes?: string[]
}
type ShellProps = ParentParitySpec & {
children?: ReactNode
fullWidth?: boolean
@@ -20,12 +19,12 @@ export function ParentParityShell({
}: ShellProps) {
return (
<div>
<div className="text-center mb-3">
<h2 className="h4 text-success" style={{ fontFamily: 'Arial, sans-serif' }}>
{title}
</h2>
</div>
<div className="text-center mb-3">
<h2 className="h4 text-success" style={{ fontFamily: 'Arial, sans-serif' }}>
{title}
</h2>
</div>
<div className="row g-3">
-1
View File
@@ -1,3 +1,2 @@
/** Barrel for CodeIgniter `Views/teacher/*` React parity screens. */
export * from './TeacherCorePages'
export * from './TeacherRestPages'