Your IP : 216.73.217.79


Current Path : /var/www/cesa.co.za/php/includes/
Upload File :
Current File : /var/www/cesa.co.za/php/includes/WeeklySessionsParser.php

<?php
/**
 * Parse weekly sessions Excel: DATE, START TIME, LECTURE REFERENCE, DESCRIPTION,
 * PRESENTER, SESSION BACKUP, ASSIGNMENT, SUBMISSION DATE.
 * Date column spans rows (carry over until new date). Submission date is relative (e.g. "date + 1 week").
 */

use PhpOffice\PhpSpreadsheet\IOFactory;

class WeeklySessionsParser {

    const COL_DATE = 0;
    const COL_START_TIME = 1;
    const COL_LECTURE_REF = 2;
    const COL_DESCRIPTION = 3;
    const COL_PRESENTER = 4;
    const COL_SESSION_BACKUP = 5;
    const COL_ASSIGNMENT = 6;
    const COL_SUBMISSION_DATE = 7;

    /** @var int Year used when parsing DD-Mon dates (e.g. 2025) */
    public $year;

    public function __construct($year = null) {
        $this->year = $year ? (int) $year : (int) date('Y');
    }

    /**
     * @param string $path Path to .xlsx or .xls file (e.g. uploaded tmp_name)
     * @param string|null $originalName Original filename for extension check (tmp files often have no extension)
     * @return array{rows: array, errors: string[]}
     */
    public function parseFile($path, $originalName = null) {
        $errors = [];
        if (!is_readable($path)) {
            return ['rows' => [], 'errors' => ['File not readable.']];
        }
        $ext = strtolower(pathinfo($originalName !== null ? $originalName : $path, PATHINFO_EXTENSION));
        if ($ext !== '' && !in_array($ext, ['xlsx', 'xls'], true)) {
            return ['rows' => [], 'errors' => ['Only .xlsx or .xls files are allowed.']];
        }

        try {
            $spreadsheet = IOFactory::load($path);
            $sheet = $spreadsheet->getActiveSheet();
        } catch (Exception $e) {
            return ['rows' => [], 'errors' => ['Could not read Excel: ' . $e->getMessage()]];
        }

        $rows = [];
        $lastDate = null;
        $headerSkipped = false;
        $highestRow = $sheet->getHighestRow();

        for ($row = 1; $row <= $highestRow; $row++) {
            $dateVal = $this->getCellValue($sheet, $row, self::COL_DATE);
            $startTimeVal = $this->getCellValue($sheet, $row, self::COL_START_TIME);
            $lectureRef = $this->getCellValue($sheet, $row, self::COL_LECTURE_REF);
            $description = $this->getCellValue($sheet, $row, self::COL_DESCRIPTION);
            $presenter = $this->getCellValue($sheet, $row, self::COL_PRESENTER);
            $sessionBackup = $this->getCellValue($sheet, $row, self::COL_SESSION_BACKUP);
            $assignment = $this->getCellValue($sheet, $row, self::COL_ASSIGNMENT);
            $submissionDateDesc = $this->getCellValue($sheet, $row, self::COL_SUBMISSION_DATE);

            // Skip header row (optional: detect "DATE" in first column)
            if (!$headerSkipped && (
                stripos((string) $dateVal, 'date') !== false ||
                stripos((string) $startTimeVal, 'time') !== false
            )) {
                $headerSkipped = true;
                continue;
            }

            // Carry over date when empty
            if (trim((string) $dateVal) !== '') {
                $parsed = $this->parseDate($dateVal);
                if ($parsed) {
                    $lastDate = $parsed;
                }
            }

            // Skip rows with no date and no meaningful content
            if (!$lastDate && trim((string) $startTimeVal) === '' && trim((string) $description) === '') {
                continue;
            }
            if (!$lastDate) {
                continue;
            }

            $sessionDate = $lastDate;
            $startTime = $this->normalizeTime($startTimeVal);
            $submissionDate = $this->parseSubmissionDate($submissionDateDesc, $sessionDate);

            $descTrim = trim((string) $description);
            if (strcasecmp($descTrim, 'Break') === 0 || strcasecmp($descTrim, 'End') === 0) {
                continue; // do not import Break or End sessions
            }

            $rows[] = [
                'SessionDate'       => $sessionDate,
                'StartTime'         => $startTime,
                'EndTime'           => null, // set below from next session's StartTime
                'LectureReference'  => trim((string) $lectureRef) ?: null,
                'Description'       => trim((string) $description) ?: null,
                'Presenter'        => trim((string) $presenter) ?: null,
                'SessionBackup'     => trim((string) $sessionBackup) ?: null,
                'Assignment'        => trim((string) $assignment) ?: null,
                'SubmissionDateDesc' => trim((string) $submissionDateDesc) ?: null,
                'SubmissionDate'    => $submissionDate,
            ];
        }

        // End time = start time of the next session (same date)
        for ($i = 0, $n = count($rows); $i < $n; $i++) {
            if ($i + 1 < $n && $rows[$i]['SessionDate'] === $rows[$i + 1]['SessionDate']) {
                $rows[$i]['EndTime'] = $rows[$i + 1]['StartTime'];
            }
        }

        return ['rows' => $rows, 'errors' => $errors];
    }

    private function getCellValue($sheet, $row, $colIndex) {
        $colLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex + 1);
        $cell = $sheet->getCell($colLetter . $row);
        $v = $cell->getValue();
        if ($v instanceof \PhpOffice\PhpSpreadsheet\RichText\RichText) {
            $v = $v->getPlainText();
        }
        return $v === null ? '' : trim((string) $v);
    }

    /**
     * Parse DD-Mon or similar to Y-m-d.
     */
    private function parseDate($value) {
        $value = trim((string) $value);
        if ($value === '') return null;
        // Excel may have serial date number
        if (is_numeric($value)) {
            $d = \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($value);
            return $d ? $d->format('Y-m-d') : null;
        }
        // DD-Mon or D-Mon
        if (preg_match('/^(\d{1,2})[- ]([A-Za-z]{3})$/i', $value, $m)) {
            $day = (int) $m[1];
            $mon = $m[2];
            $ts = strtotime($day . ' ' . $mon . ' ' . $this->year);
            if ($ts !== false) {
                return date('Y-m-d', $ts);
            }
        }
        $ts = strtotime($value . ' ' . $this->year);
        if ($ts !== false) {
            return date('Y-m-d', $ts);
        }
        return null;
    }

    private function normalizeTime($value) {
        $value = trim((string) $value);
        if ($value === '') return null;
        if (is_numeric($value)) {
            $dt = \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($value);
            return $dt ? $dt->format('H:i:s') : null;
        }
        if (preg_match('/^(\d{1,2}):(\d{2})(?::(\d{2}))?\s*(am|pm)?$/i', $value, $m)) {
            $h = (int) $m[1];
            $i = (int) $m[2];
            $s = isset($m[3]) ? (int) $m[3] : 0;
            if (isset($m[4]) && strtolower($m[4]) === 'pm' && $h < 12) $h += 12;
            if (isset($m[4]) && strtolower($m[4]) === 'am' && $h === 12) $h = 0;
            return sprintf('%02d:%02d:%02d', $h, $i, $s);
        }
        if (preg_match('/^(\d{1,2}):(\d{2})/', $value, $m)) {
            return sprintf('%02d:%02d:00', (int) $m[1], (int) $m[2]);
        }
        return null;
    }

    /**
     * Parse relative submission date (e.g. "date + 1 week", "lecture date + 1 week") to Y-m-d.
     */
    private function parseSubmissionDate($desc, $sessionDate) {
        $desc = trim((string) $desc);
        if ($desc === '') return null;
        $base = $sessionDate; // Y-m-d
        if (preg_match('/\+?\s*(\d+)\s*week/i', $desc, $m)) {
            $weeks = (int) $m[1];
            $d = new DateTime($base);
            $d->modify('+' . $weeks . ' weeks');
            return $d->format('Y-m-d');
        }
        if (preg_match('/\+?\s*(\d+)\s*day/i', $desc, $m)) {
            $days = (int) $m[1];
            $d = new DateTime($base);
            $d->modify('+' . $days . ' days');
            return $d->format('Y-m-d');
        }
        return null;
    }
}