| Current Path : /var/www/cesa.co.za/php/includes/ |
| 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;
}
}