forecastService = new TuitionForecastService(); helper(['url']); } public function index() { $schoolYear = trim((string) ($this->request->getGet('school_year') ?? '')); $semester = trim((string) ($this->request->getGet('semester') ?? '')); $mode = (string) ($this->request->getGet('calculator_mode') ?? 'compare'); $filters = $this->buildOptionsFromRequest('get'); $result = $this->forecastService->calculate($schoolYear, $semester, $mode, $filters); return view('administrator/tuition_forecast', [ 'title' => 'Tuition Collection Forecast', 'schoolYears' => $this->forecastService->getAvailableSchoolYears(), 'semesters' => $this->forecastService->getAvailableSemesters(), 'filters' => [ 'school_year' => $result['school_year'], 'semester' => $result['semester'], 'calculator_mode' => $result['calculator_mode'], ] + $result['options'], 'result' => $result, ]); } public function calculate() { $schoolYear = trim((string) ($this->request->getPost('school_year') ?? '')); $semester = trim((string) ($this->request->getPost('semester') ?? '')); $mode = (string) ($this->request->getPost('calculator_mode') ?? 'compare'); return $this->response->setJSON( $this->forecastService->calculate($schoolYear, $semester, $mode, $this->buildOptionsFromRequest('post')) ); } public function exportCsv() { $schoolYear = trim((string) ($this->request->getGet('school_year') ?? '')); $semester = trim((string) ($this->request->getGet('semester') ?? '')); $mode = (string) ($this->request->getGet('calculator_mode') ?? 'compare'); $result = $this->forecastService->calculate($schoolYear, $semester, $mode, $this->buildOptionsFromRequest('get')); $filename = sprintf( 'tuition_forecast_%s_%s_%s.csv', preg_replace('/[^A-Za-z0-9_-]+/', '-', $result['school_year']) ?: 'school-year', preg_replace('/[^A-Za-z0-9_-]+/', '-', $result['semester']) ?: 'all-year', $result['calculator_mode'] ); $handle = fopen('php://temp', 'w+'); if ($handle === false) { throw new \RuntimeException('Unable to create CSV export.'); } fputcsv($handle, ['School Year', $result['school_year']]); fputcsv($handle, ['Semester', $result['semester'] !== '' ? $result['semester'] : 'All Year']); fputcsv($handle, ['Calculator Mode', $result['calculator_mode']]); fputcsv($handle, []); fputcsv($handle, ['Summary']); fputcsv($handle, ['Families', 'Students', 'Billable Students', 'Unit Price', 'Old Projected Income', 'New Projected Income', 'Difference']); fputcsv($handle, [ $result['summary']['family_count'], $result['summary']['student_count'], $result['summary']['billable_student_count'], $result['summary']['unit_price'], $result['summary']['old_projected_income'] ?? $result['summary']['old_projected_tuition'], $result['summary']['new_projected_income'] ?? $result['summary']['new_projected_tuition'], $result['summary']['difference'], ]); fputcsv($handle, []); fputcsv($handle, ['Families']); fputcsv($handle, ['Parent/Family', 'Student Count', 'Billable Student Count', 'Old Tuition Total', 'New Tuition Total', 'Difference', 'Warnings']); foreach ($result['families'] as $family) { fputcsv($handle, [ $family['parent_name'] ?? '', $family['student_count'] ?? 0, $family['billable_student_count'] ?? 0, $family['old_total'] ?? '0.00', $family['new_total'] ?? '0.00', $family['difference'] ?? '0.00', implode(' | ', $family['warnings'] ?? []), ]); fputcsv($handle, ['Student Name', 'Grade', 'Billable', 'Excluded Reason', 'Old Rule', 'Old Amount', 'New Rule', 'New Amount']); foreach ($family['student_details'] as $detail) { fputcsv($handle, [ $detail['student_name'] ?? '', $detail['grade_level'] ?? '', !empty($detail['billable']) ? 'Yes' : 'No', $detail['excluded_reason'] ?? '', $detail['old_rule'] ?? '', $detail['old_amount'] ?? '0.00', $detail['new_rule'] ?? '', $detail['new_amount'] ?? '0.00', ]); } fputcsv($handle, []); } rewind($handle); $csv = stream_get_contents($handle) ?: ''; fclose($handle); return $this->response ->setHeader('Content-Type', 'text/csv; charset=UTF-8') ->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"') ->setBody($csv); } protected function buildOptionsFromRequest(string $method): array { $source = $method === 'post' ? 'getPost' : 'getGet'; return [ 'include_withdrawn_mode' => (string) ($this->request->{$source}('include_withdrawn_mode') ?? 'refund_deadline'), 'include_payment_pending' => $this->request->{$source}('include_payment_pending') ?? '0', 'include_event_only' => $this->request->{$source}('include_event_only') ?? '0', 'include_paid_invoices' => $this->request->{$source}('include_paid_invoices') ?? '0', 'unit_price' => $this->request->{$source}('unit_price') ?? '', ]; } }