From fb224d66f773249b46923d7c92a60a33a89e38de Mon Sep 17 00:00:00 2001
From: Father Vlasie <33334298+fvlasie@users.noreply.github.com>
Date: Fri, 30 Jan 2026 11:32:39 -0800
Subject: [PATCH 1/4] init importer
---
CHANGEDB.php | 1 +
modules/Planner/planner_import.php | 51 ++++++++
modules/Planner/planner_importProcess.php | 141 ++++++++++++++++++++++
3 files changed, 193 insertions(+)
create mode 100644 modules/Planner/planner_import.php
create mode 100644 modules/Planner/planner_importProcess.php
diff --git a/CHANGEDB.php b/CHANGEDB.php
index 9877af1603..436b13aa96 100644
--- a/CHANGEDB.php
+++ b/CHANGEDB.php
@@ -419,4 +419,5 @@
ALTER TABLE `gibbonLibraryItem` CHANGE `gibbonSpaceID` `gibbonSpaceID` INT UNSIGNED ZEROFILL NULL DEFAULT NULL;end
INSERT INTO `gibbonModule` (`name`, `description`, `entryURL`, `type`, `active`, `category`, `version`, `author`, `url`) VALUES ('User', 'User preferences and settings.', '', 'Core', 'Y', 'Other', '', 'Gibbon Foundation', 'https://gibbonedu.org');end
UPDATE `gibbonCountry` SET `printable_name` = 'North Macedonia' WHERE `printable_name` = 'Macedonia, the Former Yugoslav Republic of';end
+INSERT INTO gibbonAction (gibbonModuleID, name, precedence, category, description, URLList, entryURL, type, active) VALUES ('0009', 'Import', 0, 'Planner', 'Import lessons from CSV', 'planner_import.php', 'planner_import.php', 'Additional', 'Y');end
";
diff --git a/modules/Planner/planner_import.php b/modules/Planner/planner_import.php
new file mode 100644
index 0000000000..309accd38b
--- /dev/null
+++ b/modules/Planner/planner_import.php
@@ -0,0 +1,51 @@
+addError(__('You do not have access to this action.'));
+ return;
+}
+
+$page->breadcrumbs
+ ->add(__('Planner'))
+ ->add(__('Bulk Import Lessons'));
+
+$form = Form::create(
+ 'plannerImport',
+ $session->get('absoluteURL').'/modules/Planner/planner_importProcess.php'
+);
+
+$form->setClass('w-full max-w-3xl');
+
+// Template download
+$row = $form->addRow();
+$row->addLabel('template', __('Download Template'));
+$row->addContent(
+ ''.
+ __('Download CSV Template').
+ ''
+);
+
+// CSV upload
+$row = $form->addRow();
+$row->addLabel('file', __('Upload CSV File'));
+$row->addFileUpload('file')->required();
+
+// Submit
+$row = $form->addRow();
+$row->addSubmit();
+
+echo $form->getOutput();
+
+// Handle template download
+if (isset($_GET['template'])) {
+ header('Content-Type: text/csv');
+ header('Content-Disposition: attachment; filename="planner_template.csv"');
+
+ $out = fopen('php://output', 'w');
+ fputcsv($out, ['date', 'timeStart', 'timeEnd', 'course', 'class', 'name', 'description']);
+ fclose($out);
+ exit;
+}
diff --git a/modules/Planner/planner_importProcess.php b/modules/Planner/planner_importProcess.php
new file mode 100644
index 0000000000..1415d77a91
--- /dev/null
+++ b/modules/Planner/planner_importProcess.php
@@ -0,0 +1,141 @@
+addError(__('You do not have access to this action.'));
+ return;
+}
+
+$file = $_FILES['file'] ?? null;
+
+if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
+ $page->addError(__('File upload failed.'));
+ header('Location: '.$session->get('absoluteURL').'/modules/Planner/planner_import.php');
+ exit;
+}
+
+// Read CSV
+$rows = array_map('str_getcsv', file($file['tmp_name']));
+$headers = array_shift($rows);
+
+// Load hooks
+$hookGateway = $container->get(\Gibbon\Domain\System\HookGateway::class);
+$hookRows = $hookGateway->selectBy(['type' => 'Lesson Planner'])->fetchAll();
+
+$hooks = [];
+foreach ($hookRows as $hookRow) {
+ require_once $hookRow['file'];
+ $class = $hookRow['class'];
+ $hooks[] = new $class($pdo);
+}
+
+// Custom fields
+$customFieldHandler = $container->get(\Gibbon\Forms\CustomFieldHandler::class);
+
+$imported = 0;
+$errors = [];
+
+foreach ($rows as $rowNumber => $row) {
+ $data = array_combine($headers, $row);
+
+ // --- 1. Resolve class ---
+ $gibbonCourseClassID = resolveClass($connection2, $data['course'] ?? '', $data['class'] ?? '');
+ if (!$gibbonCourseClassID) {
+ $errors[] = "Row ".($rowNumber+2).": Invalid course/class";
+ continue;
+ }
+
+ // --- 2. Required fields ---
+ $date = $data['date'] ?? '';
+ $timeStart = $data['timeStart'] ?? '';
+ $timeEnd = $data['timeEnd'] ?? '';
+ $name = $data['name'] ?? '';
+ $description = $data['description'] ?? '';
+
+ if (!$date || !$timeStart || !$timeEnd || !$name) {
+ $errors[] = "Row ".($rowNumber+2).": Missing required fields";
+ continue;
+ }
+
+ // --- 3. Custom fields ---
+ $fields = $customFieldHandler->getFieldDataFromArray('Lesson Plan', $data);
+
+ // --- 4. Insert lesson ---
+ try {
+ $insertData = [
+ 'gibbonCourseClassID' => $gibbonCourseClassID,
+ 'date' => $date,
+ 'timeStart' => $timeStart,
+ 'timeEnd' => $timeEnd,
+ 'name' => $name,
+ 'description' => $description,
+ 'gibbonPersonIDCreator' => $session->get('gibbonPersonID'),
+ 'gibbonPersonIDLastEdit' => $session->get('gibbonPersonID'),
+ 'fields' => $fields,
+ ];
+
+ $sql = 'INSERT INTO gibbonPlannerEntry SET
+ gibbonCourseClassID=:gibbonCourseClassID,
+ date=:date,
+ timeStart=:timeStart,
+ timeEnd=:timeEnd,
+ name=:name,
+ description=:description,
+ gibbonPersonIDCreator=:gibbonPersonIDCreator,
+ gibbonPersonIDLastEdit=:gibbonPersonIDLastEdit,
+ fields=:fields';
+
+ $stmt = $connection2->prepare($sql);
+ $stmt->execute($insertData);
+
+ $gibbonPlannerEntryID = $connection2->lastInsertId();
+
+ // --- 5. Process hooks ---
+ foreach ($hooks as $hook) {
+ $hook->process($gibbonPlannerEntryID, $data);
+ }
+
+ $imported++;
+
+ } catch (PDOException $e) {
+ $errors[] = "Row ".($rowNumber+2).": Database error";
+ continue;
+ }
+}
+
+// --- 6. Report results ---
+if (!empty($errors)) {
+ foreach ($errors as $err) {
+ $page->addError($err);
+ }
+}
+
+$page->addMessage(sprintf(__('Imported %1$s lessons.'), $imported));
+
+header('Location: '.$session->get('absoluteURL').'/modules/Planner/planner_import.php');
+exit;
+
+
+// ------------------------------------------------------------
+// Helper: resolve course/class
+// ------------------------------------------------------------
+function resolveClass($connection2, $courseShort, $classShort)
+{
+ if (!$courseShort || !$classShort) {
+ return false;
+ }
+
+ $sql = "SELECT gibbonCourseClassID
+ FROM gibbonCourseClass
+ JOIN gibbonCourse USING (gibbonCourseID)
+ WHERE gibbonCourse.nameShort = :course
+ AND gibbonCourseClass.nameShort = :class";
+
+ $stmt = $connection2->prepare($sql);
+ $stmt->execute([
+ 'course' => $courseShort,
+ 'class' => $classShort,
+ ]);
+
+ return $stmt->fetchColumn() ?: false;
+}
From 1b21f78ff77d973adda2d723a493d75aa1cef67b Mon Sep 17 00:00:00 2001
From: Father Vlasie <33334298+fvlasie@users.noreply.github.com>
Date: Mon, 2 Feb 2026 15:55:52 -0800
Subject: [PATCH 2/4] Local planner import changes
---
.gitignore | 7 +
CHANGEDB.php | 1 -
modules/Planner/planner_import.php | 62 +--
modules/Planner/planner_importProcess.php | 524 +++++++++++++++++++--
modules/Planner/planner_import_export.php | 36 ++
modules/Planner/src/Tables/LessonTable.php | 8 +
6 files changed, 568 insertions(+), 70 deletions(-)
create mode 100644 modules/Planner/planner_import_export.php
diff --git a/.gitignore b/.gitignore
index b5d2236a57..7e70fadd86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,3 +38,10 @@ var/*
# Ignore node_modules folder
node_modules
+
+# Ignore testing modules
+/modules/Courses and Classes
+/modules/AccessAPI
+/modules/BigBlueButton
+/modules/Data Admin
+
diff --git a/CHANGEDB.php b/CHANGEDB.php
index 436b13aa96..9877af1603 100644
--- a/CHANGEDB.php
+++ b/CHANGEDB.php
@@ -419,5 +419,4 @@
ALTER TABLE `gibbonLibraryItem` CHANGE `gibbonSpaceID` `gibbonSpaceID` INT UNSIGNED ZEROFILL NULL DEFAULT NULL;end
INSERT INTO `gibbonModule` (`name`, `description`, `entryURL`, `type`, `active`, `category`, `version`, `author`, `url`) VALUES ('User', 'User preferences and settings.', '', 'Core', 'Y', 'Other', '', 'Gibbon Foundation', 'https://gibbonedu.org');end
UPDATE `gibbonCountry` SET `printable_name` = 'North Macedonia' WHERE `printable_name` = 'Macedonia, the Former Yugoslav Republic of';end
-INSERT INTO gibbonAction (gibbonModuleID, name, precedence, category, description, URLList, entryURL, type, active) VALUES ('0009', 'Import', 0, 'Planner', 'Import lessons from CSV', 'planner_import.php', 'planner_import.php', 'Additional', 'Y');end
";
diff --git a/modules/Planner/planner_import.php b/modules/Planner/planner_import.php
index 309accd38b..4e39769f94 100644
--- a/modules/Planner/planner_import.php
+++ b/modules/Planner/planner_import.php
@@ -1,51 +1,51 @@
addError(__('You do not have access to this action.'));
return;
}
$page->breadcrumbs
- ->add(__('Planner'))
+ ->add(__('Planner'), 'planner.php')
->add(__('Bulk Import Lessons'));
-$form = Form::create(
- 'plannerImport',
- $session->get('absoluteURL').'/modules/Planner/planner_importProcess.php'
-);
+// Display any import messages or errors stored in session BEFORE the form
+$importErrors = $_SESSION['planner_import_errors'] ?? null;
+if (!empty($importErrors) && is_array($importErrors)) {
+ foreach ($importErrors as $err) {
+ $page->addError($err);
+ }
+ unset($_SESSION['planner_import_errors']);
+}
+
+$importMsg = $_SESSION['planner_import_message'] ?? null;
+if (!empty($importMsg)) {
+ $page->addSuccess($importMsg);
+ unset($_SESSION['planner_import_message']);
+}
+$form = Form::create('plannerImport', $session->get('absoluteURL').'/modules/'.$session->get('module').'/planner_importProcess.php');
$form->setClass('w-full max-w-3xl');
+$form->setFactory(DatabaseFormFactory::create($pdo));
-// Template download
$row = $form->addRow();
-$row->addLabel('template', __('Download Template'));
-$row->addContent(
- ''.
- __('Download CSV Template').
- ''
-);
-
-// CSV upload
+ $row->addLabel('template', __('CSV Template'));
+ $row->addContent(''.__('Download CSV Template').'');
+
$row = $form->addRow();
-$row->addLabel('file', __('Upload CSV File'));
-$row->addFileUpload('file')->required();
+ $row->addLabel('file', __('Upload CSV'));
+ $row->addFileUpload('file')->required();
-// Submit
$row = $form->addRow();
-$row->addSubmit();
+ $row->addSubmit(__('Import'));
echo $form->getOutput();
-// Handle template download
-if (isset($_GET['template'])) {
- header('Content-Type: text/csv');
- header('Content-Disposition: attachment; filename="planner_template.csv"');
-
- $out = fopen('php://output', 'w');
- fputcsv($out, ['date', 'timeStart', 'timeEnd', 'course', 'class', 'name', 'description']);
- fclose($out);
- exit;
-}
diff --git a/modules/Planner/planner_importProcess.php b/modules/Planner/planner_importProcess.php
index 1415d77a91..282aa8c495 100644
--- a/modules/Planner/planner_importProcess.php
+++ b/modules/Planner/planner_importProcess.php
@@ -1,16 +1,355 @@
addError(__('You do not have access to this action.'));
- return;
+// Redirect target for errors/success (process scripts should redirect)
+$URL = $session->get('absoluteURL').'/index.php?q=/modules/'.$session->get('module').'/planner_import.php';
+
+// Use planner_edit permission as the guard so import processing follows edit access
+if (!isActionAccessible($guid, $connection2, '/modules/Planner/planner_edit.php')) {
+ $URL .= '&return=error0';
+ header("Location: {$URL}");
+ exit();
+}
+
+$action = $_GET['action'] ?? '';
+
+// Debug endpoint: output discovered custom fields and aliases
+if ($action === 'debugFields') {
+ $cf = getCustomFieldMap($container);
+ header('Content-Type: application/json');
+ echo json_encode($cf, JSON_PRETTY_PRINT);
+ exit;
+}
+
+// Helper: discover core planner columns
+function getPlannerColumns($pdo)
+{
+ $cols = [];
+ try {
+ $stmt = $pdo->select("DESCRIBE gibbonPlannerEntry");
+ $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
+ foreach ($rows as $row) $cols[] = $row['Field'];
+ } catch (Exception $e) {
+ return [];
+ }
+ $exclude = ['gibbonPlannerEntryID', 'dateCreated', 'dateModified', 'createdBy', 'modifiedBy', 'dateDeleted'];
+ // Use a blacklist: return all planner columns except internal/system fields so future fields
+ // are automatically included in the template/import unless explicitly excluded above.
+ $cols = array_values(array_diff($cols, $exclude));
+ return $cols;
+}
+
+// Helper: discover Lesson Plan custom fields (returns header => id)
+function getCustomFieldMap($container)
+{
+ $customFieldGateway = $container->get(\Gibbon\Domain\System\CustomFieldGateway::class);
+ $map = [];
+ $aliases = [];
+
+ try {
+ $grouped = $customFieldGateway->selectCustomFields('Lesson Plan')->fetchGrouped();
+ } catch (Exception $e) {
+ $grouped = [];
+ }
+
+ $excludeTypes = ['file', 'image', 'link', 'url', 'attachment'];
+
+ foreach ($grouped as $heading => $fields) {
+ if (empty($fields) || !is_array($fields)) continue;
+ foreach ($fields as $f) {
+ $fieldType = strtolower($f['type'] ?? 'text');
+ if (in_array($fieldType, $excludeTypes)) continue;
+
+ $context = $f['context'] ?? 'custom';
+ $contextSlug = preg_replace('/[^a-z0-9]+/', '_', strtolower($context));
+ $nameSlug = preg_replace('/[^a-z0-9]+/', '_', strtolower($f['name'] ?? ''));
+ $canonical = $contextSlug.'_custom_'.$f['gibbonCustomFieldID'].'_'.($nameSlug ?: $f['gibbonCustomFieldID']);
+
+ $map[$canonical] = [
+ 'id' => $f['gibbonCustomFieldID'],
+ 'type' => $f['type'] ?? 'text',
+ 'name' => $f['name'] ?? '',
+ 'context' => $context,
+ ];
+
+ $simple = preg_replace('/[^a-z0-9]+/', '_', strtolower($f['name'] ?? ''));
+ $aliases[$simple] = $canonical;
+ $aliases[strtolower($f['name'] ?? '')] = $canonical;
+ $aliases[$contextSlug.'_'.$simple] = $canonical;
+ $aliases[str_replace('_', ' ', $simple)] = $canonical;
+ $aliases[preg_replace('/[^a-z0-9]+/', '', strtolower($f['name'] ?? ''))] = $canonical;
+ }
+ }
+
+ // Fallback: include any remaining Lesson Plan custom fields (including inactive)
+ try {
+ $all = $customFieldGateway->selectBy(['context' => 'Lesson Plan'])->fetchAll();
+ } catch (Exception $e) {
+ $all = [];
+ }
+
+ foreach ($all as $f) {
+ $fieldType = strtolower($f['type'] ?? 'text');
+ if (in_array($fieldType, $excludeTypes)) continue;
+
+ $context = $f['context'] ?? 'custom';
+ $contextSlug = preg_replace('/[^a-z0-9]+/', '_', strtolower($context));
+ $nameSlug = preg_replace('/[^a-z0-9]+/', '_', strtolower($f['name'] ?? ''));
+ $canonical = $contextSlug.'_custom_'.$f['gibbonCustomFieldID'].'_'.($nameSlug ?: $f['gibbonCustomFieldID']);
+
+ if (isset($map[$canonical])) continue;
+
+ $map[$canonical] = [
+ 'id' => $f['gibbonCustomFieldID'],
+ 'type' => $f['type'] ?? 'text',
+ 'name' => $f['name'] ?? '',
+ 'context' => $context,
+ ];
+
+ $simple = preg_replace('/[^a-z0-9]+/', '_', strtolower($f['name'] ?? ''));
+ $aliases[$simple] = $canonical;
+ $aliases[strtolower($f['name'] ?? '')] = $canonical;
+ $aliases[$contextSlug.'_'.$simple] = $canonical;
+ $aliases[str_replace('_', ' ', $simple)] = $canonical;
+ $aliases[preg_replace('/[^a-z0-9]+/', '', strtolower($f['name'] ?? ''))] = $canonical;
+ }
+
+ return ['map' => $map, 'aliases' => $aliases];
+
}
+// Helper: probe accessible Lesson Planner hooks for import fields
+function getHookFields($container)
+{
+ global $guid, $connection2, $session, $page;
+
+ $hookGateway = $container->get(\Gibbon\Domain\System\HookGateway::class);
+ $hooks = $hookGateway->getAccessibleHooksByType('Lesson Planner', $container->get('session')->get('gibbonRoleIDCurrent'));
+ $hookFields = [];
+
+ foreach ($hooks as $hook) {
+ $include = $container->get('session')->get('absolutePath').'/modules/'.$hook['sourceModuleName'].'/'.$hook['sourceModuleInclude'];
+ if (!file_exists($include)) continue;
+
+ ob_start();
+ try {
+ include $include;
+ } catch (Exception $e) {
+ // ignore
+ }
+ ob_end_clean();
+
+ $fn = $hook['sourceModuleName'].'_lessonPlannerImportFields';
+ if (function_exists($fn)) {
+ $fields = $fn();
+ if (is_array($fields)) {
+ foreach ($fields as $key => $label) {
+ $header = 'hook_'.$hook['sourceModuleName'].'_'.$key;
+ $hookFields[$header] = $label;
+ }
+ }
+ continue;
+ }
+
+ if (isset($lessonPlannerImportFields) && is_array($lessonPlannerImportFields)) {
+ foreach ($lessonPlannerImportFields as $key => $label) {
+ $header = 'hook_'.$hook['sourceModuleName'].'_'.$key;
+ $hookFields[$header] = $label;
+ }
+ unset($lessonPlannerImportFields);
+ continue;
+ }
+ }
+
+ return $hookFields;
+}
+
+// Build a simple lookup table mapping DB/internal fields to user-facing labels
+function getHeaderLookup($container, $pdo)
+{
+ // Core DB -> User labels (ordered)
+ $coreOrder = [
+ 'gibbonCourseClassID','gibbonUnitID','date','timeStart','timeEnd','name','summary','description','teachersNotes',
+ 'homework','homeworkDueDateTime','homeworkTimeCap','homeworkDetails','homeworkSubmission','homeworkSubmissionDateOpen',
+ 'homeworkSubmissionDrafts','homeworkSubmissionType','homeworkSubmissionRequired','viewableStudents','viewableParents',
+ ];
+ $dbToUser = [
+ 'gibbonCourseClassID' => 'Class',
+ 'gibbonUnitID' => 'Unit',
+ 'date' => 'Date',
+ 'timeStart' => 'Start Time',
+ 'timeEnd' => 'End Time',
+ 'name' => 'Lesson Name',
+ 'summary' => 'Summary',
+ 'description' => 'Lesson Details',
+ 'teachersNotes' => "Teacher's Notes",
+ 'homework' => 'Add Homework',
+ 'homeworkDueDateTime' => 'Homework Due Date/Time',
+ 'homeworkTimeCap' => 'Homework Time Cap',
+ 'homeworkDetails' => 'Homework Details',
+ 'homeworkSubmission' => 'Online Submission',
+ 'homeworkSubmissionDateOpen' => 'Submission Open Date',
+ 'homeworkSubmissionDrafts' => 'Submission Drafts',
+ 'homeworkSubmissionType' => 'Submission Type',
+ 'homeworkSubmissionRequired' => 'Submission Required',
+ 'viewableStudents' => 'Viewable by Students',
+ 'viewableParents' => 'Viewable by Parents',
+ ];
+
+ // Custom fields and hooks
+ $cf = getCustomFieldMap($container);
+ $customMap = $cf['map'] ?? [];
+ $customAliases = $cf['aliases'] ?? [];
+ $hookFields = getHookFields($container);
+
+ // Build userToDb by using friendly labels for custom fields
+ $userToDb = [];
+ foreach ($coreOrder as $db) {
+ if (isset($dbToUser[$db])) $userToDb[$dbToUser[$db]] = $db;
+ }
+
+ foreach ($customMap as $canonical => $info) {
+ $label = $info['name'] ?? $canonical;
+ // Avoid collisions with core labels
+ $label = $label ?: $canonical;
+ $userToDb[$label] = $canonical;
+ // also accept simple slug forms as labels
+ $slug = preg_replace('/[^a-z0-9]+/', '_', strtolower($label));
+ $userToDb[$slug] = $canonical;
+ $userToDb[strtolower($label)] = $canonical;
+ }
+
+ foreach ($hookFields as $header => $label) {
+ $userToDb[$label] = $header; // header like hook_Module_key
+ $userToDb[strtolower($label)] = $header;
+ $userToDb[preg_replace('/[^a-z0-9]+/', '_', strtolower($label))] = $header;
+ }
+
+ return [
+ 'dbToUser' => $dbToUser,
+ 'userToDb' => $userToDb,
+ 'customMap' => $customMap,
+ 'customAliases' => $customAliases,
+ 'hookFields' => $hookFields,
+ 'coreOrder' => $coreOrder,
+ ];
+}
+
+// Template download: core + custom + hook fields
+if ($action === 'downloadTemplate') {
+ $core = getPlannerColumns($pdo);
+ $lookup = getHeaderLookup($container, $pdo);
+ $dbToUser = $lookup['dbToUser'];
+ $userToDb = $lookup['userToDb'];
+ $customMap = $lookup['customMap'];
+ $customAliases = $lookup['customAliases'];
+ $hookFields = $lookup['hookFields'];
+ $coreOrder = $lookup['coreOrder'];
+
+ // Build ordered headers using friendly labels
+ // Insert all custom fields immediately after `name` (Lesson Name) and before `summary`.
+ $headers = [];
+ $customInserted = false;
+ foreach ($coreOrder as $db) {
+ if (in_array($db, $core)) {
+ $headers[] = $dbToUser[$db] ?? $db;
+
+ // After the Lesson Name, insert all custom fields (friendly labels)
+ if (!$customInserted && $db === 'name') {
+ foreach ($customMap as $canonical => $info) {
+ $label = $info['name'] ?? $canonical;
+ $headers[] = $label;
+ }
+ $customInserted = true;
+ }
+ }
+ }
+ // Hook fields (labels)
+ foreach ($hookFields as $header => $label) {
+ $headers[] = $label;
+ }
+
+ $examples = [];
+ foreach ($headers as $h) {
+ // Generate actual example values (not just format hints)
+ switch ($h) {
+ case 'Class': $examples[] = 'COURSE1.CLASSA'; break;
+ case 'Date': $examples[] = date('Y-m-d'); break;
+ case 'Start Time': $examples[] = '09:00'; break;
+ case 'End Time': $examples[] = '10:00'; break;
+ case 'Lesson Name': $examples[] = 'Introduction to Topic'; break;
+ case 'Unit': $examples[] = '1'; break;
+ case 'Summary': $examples[] = 'Key learning objectives'; break;
+ case 'Lesson Details': $examples[] = 'Detailed content and activities'; break;
+ case "Teacher's Notes": $examples[] = 'Notes for preparation'; break;
+ case 'Add Homework': $examples[] = 'Y'; break;
+ case 'Homework Due Date/Time': $examples[] = date('Y-m-d H:i:s'); break;
+ case 'Homework Details': $examples[] = 'Complete exercises 1-5'; break;
+ case 'Online Submission': $examples[] = 'Y'; break;
+ case 'Submission Required': $examples[] = 'Required'; break;
+ case 'Viewable by Students': $examples[] = 'Y'; break;
+ case 'Viewable by Parents': $examples[] = 'Y'; break;
+ default:
+ // Custom fields: use type to generate appropriate example
+ $type = null;
+ $fieldId = null;
+
+ // Resolve custom field type
+ $internal = $userToDb[strtolower($h)] ?? $userToDb[$h] ?? null;
+ if ($internal && isset($customMap[$internal])) {
+ $type = strtolower($customMap[$internal]['type'] ?? 'text');
+ } else {
+ // Try by field name in custom map
+ foreach ($customMap as $canonical => $info) {
+ if (strtolower($info['name'] ?? '') === strtolower($h)) {
+ $type = strtolower($info['type'] ?? 'text');
+ break;
+ }
+ }
+ }
+
+ // Generate example based on field type
+ switch ($type) {
+ case 'date': $examples[] = date('Y-m-d'); break;
+ case 'time':
+ case 'timeofday': $examples[] = '14:00'; break;
+ case 'number': $examples[] = '42'; break;
+ case 'checkbox':
+ case 'checkboxes':
+ case 'yesno': $examples[] = 'Y'; break;
+ case 'select':
+ case 'dropdown': $examples[] = 'Option 1'; break;
+ default: $examples[] = 'Sample value'; break;
+ }
+ }
+ }
+
+ $filename = 'planner_template_'.date('Y-m-d').'.csv';
+
+ // Prepend Class column (accepts COURSE.CLASS format like FL07.1)
+ array_unshift($headers, 'Class');
+ array_unshift($examples, 'COURSE1.CLASSA');
+
+ // Store template in session and redirect to export endpoint (Query Builder pattern)
+ $hash = md5(serialize($headers).time());
+ $session->set($hash, [
+ 'headers' => $headers,
+ 'examples' => $examples,
+ 'filename' => $filename,
+ ]);
+
+ header('Location: '.$session->get('absoluteURL').'/modules/Planner/planner_import_export.php?hash='.$hash);
+ exit;
+}
+
+// Otherwise: fall back to processing upload (existing behaviour)
+
+// Expect uploaded file field 'file'
$file = $_FILES['file'] ?? null;
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
- $page->addError(__('File upload failed.'));
- header('Location: '.$session->get('absoluteURL').'/modules/Planner/planner_import.php');
+ $_SESSION['planner_import_errors'] = [__('File upload failed.')];
+ header('Location: '.$URL);
exit;
}
@@ -18,25 +357,68 @@
$rows = array_map('str_getcsv', file($file['tmp_name']));
$headers = array_shift($rows);
-// Load hooks
-$hookGateway = $container->get(\Gibbon\Domain\System\HookGateway::class);
-$hookRows = $hookGateway->selectBy(['type' => 'Lesson Planner'])->fetchAll();
-
-$hooks = [];
-foreach ($hookRows as $hookRow) {
- require_once $hookRow['file'];
- $class = $hookRow['class'];
- $hooks[] = new $class($pdo);
-}
-
-// Custom fields
-$customFieldHandler = $container->get(\Gibbon\Forms\CustomFieldHandler::class);
+// Build lookup for import mapping
+$lookup = getHeaderLookup($container, $pdo);
+$userToDb = $lookup['userToDb'];
+$customMap = $lookup['customMap'];
+$customAliases = $lookup['customAliases'];
+$hookFields = $lookup['hookFields'];
$imported = 0;
$errors = [];
foreach ($rows as $rowNumber => $row) {
- $data = array_combine($headers, $row);
+ // Skip empty rows
+ if (count($row) === 1 && trim($row[0]) === '') continue;
+
+ // Ensure row has same number of columns as headers
+ $numHeaders = count($headers);
+ $numRow = count($row);
+ if ($numRow < $numHeaders) {
+ // Pad missing columns with empty strings
+ $row = array_pad($row, $numHeaders, '');
+ $errors[] = "Row ".($rowNumber+2).": Column count (got {$numRow}, expected {$numHeaders}) - padded missing values";
+ } elseif ($numRow > $numHeaders) {
+ // Truncate extra columns
+ $row = array_slice($row, 0, $numHeaders);
+ $errors[] = "Row ".($rowNumber+2).": Column count (got {$numRow}, expected {$numHeaders}) - truncated extra values";
+ }
+
+ // Combine headers with row data
+ $rowData = array_combine($headers, $row);
+
+ // Build reverse-mapped data: use userToDb to extract values by their internal keys
+ $data = [];
+ foreach ($rowData as $incomingHeader => $value) {
+ if ($value === '' || $value === null) continue;
+
+ // Parse Class field (COURSE.CLASS format) into course and class
+ $internalKey = null;
+ if (strtolower($incomingHeader) === 'class') {
+ // Split "COURSE.CLASS" format
+ if (strpos($value, '.') !== false) {
+ [$courseCode, $classCode] = explode('.', $value, 2);
+ $data['course'] = trim($courseCode);
+ $data['class'] = trim($classCode);
+ } else {
+ $errors[] = "Row ".($rowNumber+2).": Class must be in format COURSE.CLASS (e.g., FL07.1)";
+ continue 2; // Skip to next row
+ }
+ continue;
+ } else {
+ // Try userToDb lookup first (for friendly labels and internal keys)
+ $internalKey = $userToDb[strtolower($incomingHeader)] ?? $userToDb[$incomingHeader] ?? null;
+ if (!$internalKey) {
+ // Try case-insensitive match on custom field names
+ $norm = preg_replace('/[^a-z0-9]+/', '_', strtolower($incomingHeader));
+ $internalKey = $customAliases[$norm] ?? null;
+ }
+ }
+
+ if ($internalKey) {
+ $data[$internalKey] = $value;
+ }
+ }
// --- 1. Resolve class ---
$gibbonCourseClassID = resolveClass($connection2, $data['course'] ?? '', $data['class'] ?? '');
@@ -45,7 +427,7 @@
continue;
}
- // --- 2. Required fields ---
+ // --- 2. Extract and validate required fields ---
$date = $data['date'] ?? '';
$timeStart = $data['timeStart'] ?? '';
$timeEnd = $data['timeEnd'] ?? '';
@@ -53,14 +435,51 @@
$description = $data['description'] ?? '';
if (!$date || !$timeStart || !$timeEnd || !$name) {
- $errors[] = "Row ".($rowNumber+2).": Missing required fields";
+ $errors[] = "Row ".($rowNumber+2).": Missing required fields (date, timeStart, timeEnd, name)";
continue;
}
- // --- 3. Custom fields ---
- $fields = $customFieldHandler->getFieldDataFromArray('Lesson Plan', $data);
+ // --- 3. Extract custom fields and normalize values ---
+ $fieldsData = [];
+ foreach ($customMap as $canonical => $info) {
+ $id = $info['id'];
+ $type = strtolower($info['type'] ?? 'text');
+
+ // Try to find the value by canonical key or by aliases
+ $value = null;
+ if (isset($data[$canonical])) {
+ $value = $data[$canonical];
+ } else {
+ // Try by field name
+ if (isset($data[$info['name']])) {
+ $value = $data[$info['name']];
+ }
+ }
+
+ if ($value === '' || $value === null) continue;
+
+ // Normalize checkbox/yesno values to 'Include'
+ if (in_array($type, ['checkboxes', 'yesno'])) {
+ $valNorm = trim(strtolower($value));
+ $fieldsData[$id] = in_array($valNorm, ['y', 'yes', '1', 'true']) ? 'Include' : $value;
+ } else {
+ $fieldsData[$id] = $value;
+ }
+ }
+
+ // --- 4. Extract optional fields and normalize enums ---
+ $summary = $data['summary'] ?? '';
+ $teachersNotes = $data['teachersNotes'] ?? '';
+ $homework = normalizeEnum($data['homework'] ?? '', ['N', 'Y'], 'N');
+ $homeworkDueDateTime = $data['homeworkDueDateTime'] ?? '';
+ $homeworkDetails = $data['homeworkDetails'] ?? '';
+ $homeworkSubmission = normalizeEnum($data['homeworkSubmission'] ?? '', ['N', 'Y'], 'N');
+ $homeworkSubmissionRequired = normalizeEnum($data['homeworkSubmissionRequired'] ?? '', ['Optional', 'Required']);
+ $homeworkSubmissionType = normalizeEnum($data['homeworkSubmissionType'] ?? '', ['', 'Link', 'File', 'Link/File'], '');
+ $viewableStudents = normalizeEnum($data['viewableStudents'] ?? '', ['Y', 'N'], 'Y');
+ $viewableParents = normalizeEnum($data['viewableParents'] ?? '', ['Y', 'N'], 'N');
- // --- 4. Insert lesson ---
+ // --- 5. Insert lesson ---
try {
$insertData = [
'gibbonCourseClassID' => $gibbonCourseClassID,
@@ -69,9 +488,19 @@
'timeEnd' => $timeEnd,
'name' => $name,
'description' => $description,
+ 'summary' => $summary,
+ 'teachersNotes' => $teachersNotes,
+ 'homework' => $homework,
+ 'homeworkDueDateTime' => $homeworkDueDateTime,
+ 'homeworkDetails' => $homeworkDetails,
+ 'homeworkSubmission' => $homeworkSubmission,
+ 'homeworkSubmissionRequired' => $homeworkSubmissionRequired,
+ 'homeworkSubmissionType' => $homeworkSubmissionType,
+ 'viewableStudents' => $viewableStudents,
+ 'viewableParents' => $viewableParents,
'gibbonPersonIDCreator' => $session->get('gibbonPersonID'),
'gibbonPersonIDLastEdit' => $session->get('gibbonPersonID'),
- 'fields' => $fields,
+ 'fields' => !empty($fieldsData) ? json_encode($fieldsData) : null,
];
$sql = 'INSERT INTO gibbonPlannerEntry SET
@@ -81,6 +510,16 @@
timeEnd=:timeEnd,
name=:name,
description=:description,
+ summary=:summary,
+ teachersNotes=:teachersNotes,
+ homework=:homework,
+ homeworkDueDateTime=:homeworkDueDateTime,
+ homeworkDetails=:homeworkDetails,
+ homeworkSubmission=:homeworkSubmission,
+ homeworkSubmissionRequired=:homeworkSubmissionRequired,
+ homeworkSubmissionType=:homeworkSubmissionType,
+ viewableStudents=:viewableStudents,
+ viewableParents=:viewableParents,
gibbonPersonIDCreator=:gibbonPersonIDCreator,
gibbonPersonIDLastEdit=:gibbonPersonIDLastEdit,
fields=:fields';
@@ -88,34 +527,43 @@
$stmt = $connection2->prepare($sql);
$stmt->execute($insertData);
- $gibbonPlannerEntryID = $connection2->lastInsertId();
-
- // --- 5. Process hooks ---
- foreach ($hooks as $hook) {
- $hook->process($gibbonPlannerEntryID, $data);
- }
-
$imported++;
} catch (PDOException $e) {
- $errors[] = "Row ".($rowNumber+2).": Database error";
+ $errors[] = "Row ".($rowNumber+2).": Database error: ".$e->getMessage();
continue;
}
}
// --- 6. Report results ---
if (!empty($errors)) {
- foreach ($errors as $err) {
- $page->addError($err);
- }
+ $_SESSION['planner_import_errors'] = $errors;
}
-$page->addMessage(sprintf(__('Imported %1$s lessons.'), $imported));
+$_SESSION['planner_import_message'] = sprintf(__('Imported %1$s lessons.'), $imported);
-header('Location: '.$session->get('absoluteURL').'/modules/Planner/planner_import.php');
+header('Location: '.$URL);
exit;
+// ------------------------------------------------------------
+// Helper: normalize enum values to valid DB options
+// ------------------------------------------------------------
+function normalizeEnum($value, $validOptions = [], $default = '')
+{
+ if (empty($value)) return $default;
+
+ $normalized = strtolower(trim($value));
+ foreach ($validOptions as $opt) {
+ if (strtolower($opt) === $normalized) {
+ return $opt;
+ }
+ }
+
+ // If value doesn't match any option, return default
+ return $default;
+}
+
// ------------------------------------------------------------
// Helper: resolve course/class
// ------------------------------------------------------------
diff --git a/modules/Planner/planner_import_export.php b/modules/Planner/planner_import_export.php
new file mode 100644
index 0000000000..d3176be8c8
--- /dev/null
+++ b/modules/Planner/planner_import_export.php
@@ -0,0 +1,36 @@
+get($hash);
+if (empty($data) || !is_array($data)) {
+ $URL = $session->get('absoluteURL').'/index.php?q=/modules/'.$session->get('module').'/planner_import.php&return=error1';
+ header("Location: {$URL}");
+ exit();
+}
+
+$headers = $data['headers'] ?? [];
+$examples = $data['examples'] ?? [];
+$filename = $data['filename'] ?? 'planner_template.csv';
+
+// Remove session entry
+$session->remove($hash);
+
+// Stream CSV
+header('Pragma: public');
+header('Expires: 0');
+header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
+header('Cache-Control: private', false);
+header('Content-Type: text/csv');
+header('Content-Disposition: attachment; filename="'.preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $filename).'"');
+
+$out = fopen('php://output', 'w');
+if ($out) {
+ if (!empty($headers)) fputcsv($out, $headers);
+ if (!empty($examples)) fputcsv($out, $examples);
+ fclose($out);
+}
+
+exit;
diff --git a/modules/Planner/src/Tables/LessonTable.php b/modules/Planner/src/Tables/LessonTable.php
index 1721d9daf4..6df7832155 100644
--- a/modules/Planner/src/Tables/LessonTable.php
+++ b/modules/Planner/src/Tables/LessonTable.php
@@ -168,6 +168,14 @@ public function create($gibbonSchoolYearID, $gibbonCourseClassID, $gibbonPersonI
->addParam('date', $date)
->addParam('viewBy', $viewBy)
->displayLabel();
+
+ // Import action: opens the Planner import UI
+ $table->addHeaderAction('import', __('Import'))
+ ->setURL('/modules/Planner/planner_import.php')
+ ->addParam('gibbonCourseClassID', $gibbonCourseClassID)
+ ->addParam('viewBy', $viewBy)
+ ->setIcon('upload')
+ ->displayLabel();
}
if ($viewBy == 'year') {
From 72f470b68a8faec17f67e9f1719f60d03c232045 Mon Sep 17 00:00:00 2001
From: Father Vlasie <33334298+fvlasie@users.noreply.github.com>
Date: Tue, 3 Feb 2026 14:51:27 -0800
Subject: [PATCH 3/4] import csv and location updates
---
modules/Planner/planner_add.php | 9 +-
modules/Planner/planner_edit.php | 10 +-
modules/Planner/planner_import.php | 18 +-
modules/Planner/planner_importProcess.php | 326 ++++++++++++---------
modules/Planner/planner_import_export.php | 173 +++++++++--
modules/Planner/planner_view_full.php | 8 +-
modules/Planner/src/Tables/LessonTable.php | 2 +-
src/UI/Timetable/Layers/ClassesLayer.php | 11 +
8 files changed, 376 insertions(+), 181 deletions(-)
diff --git a/modules/Planner/planner_add.php b/modules/Planner/planner_add.php
index a27e3f800f..234574b596 100644
--- a/modules/Planner/planner_add.php
+++ b/modules/Planner/planner_add.php
@@ -251,12 +251,9 @@
$row->addLabel('timeEnd', __('End Time'))->description(__("Format: hh:mm (24hr)"));
$row->addTime('timeEnd')->setValue($nextTimeEnd)->required();
- if (empty($gibbonTTDayRowClassID)) {
- $row = $form->addRow();
- $row->addLabel('gibbonSpaceID', __('Location'));
- $row->addSelectSpace('gibbonSpaceID')
- ->placeholder();
- }
+ $row = $form->addRow();
+ $row->addLabel('gibbonSpaceID', __('Location'))->description(__('Override for timetable location'));
+ $row->addSelectSpace('gibbonSpaceID')->placeholder();
$form->addRow()->addHeading('Lesson Content', __('Lesson Content'));
diff --git a/modules/Planner/planner_edit.php b/modules/Planner/planner_edit.php
index 8d03a4f4ea..88c9149287 100644
--- a/modules/Planner/planner_edit.php
+++ b/modules/Planner/planner_edit.php
@@ -236,13 +236,9 @@
$row->addLabel('timeEnd', __('End Time'));
$row->addTime('timeEnd')->required();
- if (empty($values['gibbonTTDayRowClassID'])) {
- $row = $form->addRow();
- $row->addLabel('gibbonSpaceID', __('Location'));
- $row->addSelectSpace('gibbonSpaceID')
- ->placeholder();
- }
-
+ $row = $form->addRow();
+ $row->addLabel('gibbonSpaceID', __('Location'))->description(__('Override for timetable location'));
+ $row->addSelectSpace('gibbonSpaceID')->placeholder();
//LESSON
$form->addRow()->addHeading('Lesson Content', __('Lesson Content'));
diff --git a/modules/Planner/planner_import.php b/modules/Planner/planner_import.php
index 4e39769f94..49177ff44a 100644
--- a/modules/Planner/planner_import.php
+++ b/modules/Planner/planner_import.php
@@ -14,7 +14,7 @@
}
$page->breadcrumbs
- ->add(__('Planner'), 'planner.php')
+ ->add(__('Planner'), 'planner.php')
->add(__('Bulk Import Lessons'));
// Display any import messages or errors stored in session BEFORE the form
@@ -32,19 +32,25 @@
unset($_SESSION['planner_import_message']);
}
-$form = Form::create('plannerImport', $session->get('absoluteURL').'/modules/'.$session->get('module').'/planner_importProcess.php');
+$form = Form::create('plannerImport', $session->get('absoluteURL').'/index.php?q=/modules/Planner/planner_importProcess.php');
$form->setClass('w-full max-w-3xl');
$form->setFactory(DatabaseFormFactory::create($pdo));
-$row = $form->addRow();
- $row->addLabel('template', __('CSV Template'));
- $row->addContent(''.__('Download CSV Template').'');
+$form->addHeaderAction('download', __('Download Template'))
+ ->setIcon('download')
+ ->setURL('/modules/Planner/planner_import_export.php')
+ ->addParam('action', 'downloadTemplate')
+ ->directLink()
+ ->displayLabel();
+
+$form->addRow()->addHeading(__('Import Lessons from CSV'));
$row = $form->addRow();
- $row->addLabel('file', __('Upload CSV'));
+ $row->addLabel('file', __('Upload CSV'))->description(__('Select the CSV file containing lesson data to import.'));
$row->addFileUpload('file')->required();
$row = $form->addRow();
+ $row->addFooter();
$row->addSubmit(__('Import'));
echo $form->getOutput();
diff --git a/modules/Planner/planner_importProcess.php b/modules/Planner/planner_importProcess.php
index 282aa8c495..97d107a524 100644
--- a/modules/Planner/planner_importProcess.php
+++ b/modules/Planner/planner_importProcess.php
@@ -1,5 +1,5 @@
get('absoluteURL').'/index.php?q=/modules/'.$session->get('module').'/planner_import.php';
@@ -11,7 +11,7 @@
exit();
}
-$action = $_GET['action'] ?? '';
+$action = $_GET['action'] ?? '';
// Debug endpoint: output discovered custom fields and aliases
if ($action === 'debugFields') {
@@ -24,13 +24,17 @@
// Helper: discover core planner columns
function getPlannerColumns($pdo)
{
+ global $pdo;
$cols = [];
try {
$stmt = $pdo->select("DESCRIBE gibbonPlannerEntry");
$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
foreach ($rows as $row) $cols[] = $row['Field'];
} catch (Exception $e) {
- return [];
+ // If we can't query, just return the default core order columns
+ return ['gibbonUnitID','date','timeStart','timeEnd','name','summary','description','teachersNotes','gibbonSpaceID',
+ 'homework','homeworkDueDateTime','homeworkTimeCap','homeworkDetails','homeworkSubmission','homeworkSubmissionDateOpen',
+ 'homeworkSubmissionDrafts','homeworkSubmissionType','homeworkSubmissionRequired','viewableStudents','viewableParents'];
}
$exclude = ['gibbonPlannerEntryID', 'dateCreated', 'dateModified', 'createdBy', 'modifiedBy', 'dateDeleted'];
// Use a blacklist: return all planner columns except internal/system fields so future fields
@@ -81,39 +85,9 @@ function getCustomFieldMap($container)
}
}
- // Fallback: include any remaining Lesson Plan custom fields (including inactive)
- try {
- $all = $customFieldGateway->selectBy(['context' => 'Lesson Plan'])->fetchAll();
- } catch (Exception $e) {
- $all = [];
- }
-
- foreach ($all as $f) {
- $fieldType = strtolower($f['type'] ?? 'text');
- if (in_array($fieldType, $excludeTypes)) continue;
-
- $context = $f['context'] ?? 'custom';
- $contextSlug = preg_replace('/[^a-z0-9]+/', '_', strtolower($context));
- $nameSlug = preg_replace('/[^a-z0-9]+/', '_', strtolower($f['name'] ?? ''));
- $canonical = $contextSlug.'_custom_'.$f['gibbonCustomFieldID'].'_'.($nameSlug ?: $f['gibbonCustomFieldID']);
-
- if (isset($map[$canonical])) continue;
-
- $map[$canonical] = [
- 'id' => $f['gibbonCustomFieldID'],
- 'type' => $f['type'] ?? 'text',
- 'name' => $f['name'] ?? '',
- 'context' => $context,
- ];
-
- $simple = preg_replace('/[^a-z0-9]+/', '_', strtolower($f['name'] ?? ''));
- $aliases[$simple] = $canonical;
- $aliases[strtolower($f['name'] ?? '')] = $canonical;
- $aliases[$contextSlug.'_'.$simple] = $canonical;
- $aliases[str_replace('_', ' ', $simple)] = $canonical;
- $aliases[preg_replace('/[^a-z0-9]+/', '', strtolower($f['name'] ?? ''))] = $canonical;
- }
-
+ // Do NOT include a fallback for inactive fields - selectCustomFields() already filters by active='Y'
+ // This ensures the CSV template only includes fields that are currently active
+
return ['map' => $map, 'aliases' => $aliases];
}
@@ -131,22 +105,34 @@ function getHookFields($container)
$include = $container->get('session')->get('absolutePath').'/modules/'.$hook['sourceModuleName'].'/'.$hook['sourceModuleInclude'];
if (!file_exists($include)) continue;
+ // Skip hooks that are designed for viewing (not importing)
+ // They won't provide import fields and can cause fatal errors
+ if (strpos($hook['sourceModuleInclude'], 'hook_lessonPlannerView') !== false) {
+ continue;
+ }
+
ob_start();
try {
include $include;
} catch (Exception $e) {
// ignore
+ } catch (Throwable $e) {
+ // catch fatal errors and other issues
}
ob_end_clean();
$fn = $hook['sourceModuleName'].'_lessonPlannerImportFields';
if (function_exists($fn)) {
- $fields = $fn();
- if (is_array($fields)) {
- foreach ($fields as $key => $label) {
- $header = 'hook_'.$hook['sourceModuleName'].'_'.$key;
- $hookFields[$header] = $label;
+ try {
+ $fields = $fn();
+ if (is_array($fields)) {
+ foreach ($fields as $key => $label) {
+ $header = 'hook_'.$hook['sourceModuleName'].'_'.$key;
+ $hookFields[$header] = $label;
+ }
}
+ } catch (Throwable $e) {
+ // ignore errors from hook function
}
continue;
}
@@ -169,7 +155,7 @@ function getHeaderLookup($container, $pdo)
{
// Core DB -> User labels (ordered)
$coreOrder = [
- 'gibbonCourseClassID','gibbonUnitID','date','timeStart','timeEnd','name','summary','description','teachersNotes',
+ 'gibbonUnitID','date','timeStart','timeEnd','name','summary','description','teachersNotes','gibbonSpaceID',
'homework','homeworkDueDateTime','homeworkTimeCap','homeworkDetails','homeworkSubmission','homeworkSubmissionDateOpen',
'homeworkSubmissionDrafts','homeworkSubmissionType','homeworkSubmissionRequired','viewableStudents','viewableParents',
];
@@ -180,6 +166,7 @@ function getHeaderLookup($container, $pdo)
'timeStart' => 'Start Time',
'timeEnd' => 'End Time',
'name' => 'Lesson Name',
+ 'gibbonSpaceID' => 'Location',
'summary' => 'Summary',
'description' => 'Lesson Details',
'teachersNotes' => "Teacher's Notes",
@@ -237,109 +224,144 @@ function getHeaderLookup($container, $pdo)
// Template download: core + custom + hook fields
if ($action === 'downloadTemplate') {
- $core = getPlannerColumns($pdo);
- $lookup = getHeaderLookup($container, $pdo);
- $dbToUser = $lookup['dbToUser'];
- $userToDb = $lookup['userToDb'];
- $customMap = $lookup['customMap'];
- $customAliases = $lookup['customAliases'];
- $hookFields = $lookup['hookFields'];
- $coreOrder = $lookup['coreOrder'];
-
- // Build ordered headers using friendly labels
- // Insert all custom fields immediately after `name` (Lesson Name) and before `summary`.
- $headers = [];
- $customInserted = false;
- foreach ($coreOrder as $db) {
- if (in_array($db, $core)) {
- $headers[] = $dbToUser[$db] ?? $db;
-
- // After the Lesson Name, insert all custom fields (friendly labels)
- if (!$customInserted && $db === 'name') {
- foreach ($customMap as $canonical => $info) {
- $label = $info['name'] ?? $canonical;
- $headers[] = $label;
+ try {
+ error_log('[Planner Import] Starting downloadTemplate action');
+
+ $core = getPlannerColumns($pdo);
+ error_log('[Planner Import] Got core columns: ' . json_encode($core));
+
+ $lookup = getHeaderLookup($container, $pdo);
+ error_log('[Planner Import] Got header lookup successfully');
+
+ $dbToUser = $lookup['dbToUser'];
+ $userToDb = $lookup['userToDb'];
+ $customMap = $lookup['customMap'];
+ $customAliases = $lookup['customAliases'];
+ $hookFields = $lookup['hookFields'];
+ $coreOrder = $lookup['coreOrder'];
+
+ // Build ordered headers using friendly labels
+ // Insert all custom fields immediately after `name` (Lesson Name) and before `summary`.
+ $headers = [];
+ $customInserted = false;
+ foreach ($coreOrder as $db) {
+ if (in_array($db, $core)) {
+ $headers[] = $dbToUser[$db] ?? $db;
+
+ // After the Lesson Name, insert all custom fields (friendly labels)
+ if (!$customInserted && $db === 'name') {
+ foreach ($customMap as $canonical => $info) {
+ $label = $info['name'] ?? $canonical;
+ $headers[] = $label;
+ }
+ $customInserted = true;
}
- $customInserted = true;
}
}
- }
- // Hook fields (labels)
- foreach ($hookFields as $header => $label) {
- $headers[] = $label;
- }
+ // Hook fields (labels)
+ foreach ($hookFields as $header => $label) {
+ $headers[] = $label;
+ }
- $examples = [];
- foreach ($headers as $h) {
- // Generate actual example values (not just format hints)
- switch ($h) {
- case 'Class': $examples[] = 'COURSE1.CLASSA'; break;
- case 'Date': $examples[] = date('Y-m-d'); break;
- case 'Start Time': $examples[] = '09:00'; break;
- case 'End Time': $examples[] = '10:00'; break;
- case 'Lesson Name': $examples[] = 'Introduction to Topic'; break;
- case 'Unit': $examples[] = '1'; break;
- case 'Summary': $examples[] = 'Key learning objectives'; break;
- case 'Lesson Details': $examples[] = 'Detailed content and activities'; break;
- case "Teacher's Notes": $examples[] = 'Notes for preparation'; break;
- case 'Add Homework': $examples[] = 'Y'; break;
- case 'Homework Due Date/Time': $examples[] = date('Y-m-d H:i:s'); break;
- case 'Homework Details': $examples[] = 'Complete exercises 1-5'; break;
- case 'Online Submission': $examples[] = 'Y'; break;
- case 'Submission Required': $examples[] = 'Required'; break;
- case 'Viewable by Students': $examples[] = 'Y'; break;
- case 'Viewable by Parents': $examples[] = 'Y'; break;
- default:
- // Custom fields: use type to generate appropriate example
- $type = null;
- $fieldId = null;
-
- // Resolve custom field type
- $internal = $userToDb[strtolower($h)] ?? $userToDb[$h] ?? null;
- if ($internal && isset($customMap[$internal])) {
- $type = strtolower($customMap[$internal]['type'] ?? 'text');
- } else {
- // Try by field name in custom map
- foreach ($customMap as $canonical => $info) {
- if (strtolower($info['name'] ?? '') === strtolower($h)) {
- $type = strtolower($info['type'] ?? 'text');
- break;
+ error_log('[Planner Import] Generated headers: ' . json_encode($headers));
+
+ $examples = [];
+ foreach ($headers as $h) {
+ // Generate actual example values (not just format hints)
+ switch ($h) {
+ case 'Class': $examples[] = 'COURSE1.CLASSA'; break;
+ case 'Date': $examples[] = date('Y-m-d'); break;
+ case 'Start Time': $examples[] = '09:00'; break;
+ case 'End Time': $examples[] = '10:00'; break;
+ case 'Lesson Name': $examples[] = 'Introduction to Topic'; break;
+ case 'Location': $examples[] = ''; break;
+ case 'Unit': $examples[] = '1'; break;
+ case 'Summary': $examples[] = 'Key learning objectives'; break;
+ case 'Lesson Details': $examples[] = 'Detailed content and activities'; break;
+ case "Teacher's Notes": $examples[] = 'Notes for preparation'; break;
+ case 'Add Homework': $examples[] = 'Y'; break;
+ case 'Homework Due Date/Time': $examples[] = date('Y-m-d H:i:s'); break;
+ case 'Homework Time Cap': $examples[] = '30'; break;
+ case 'Homework Details': $examples[] = 'Complete exercises 1-5'; break;
+ case 'Online Submission': $examples[] = 'Y'; break;
+ case 'Submission Open Date': $examples[] = date('Y-m-d'); break;
+ case 'Submission Drafts': $examples[] = '2'; break;
+ case 'Submission Type': $examples[] = 'Link'; break;
+ case 'Submission Required': $examples[] = 'Y'; break;
+ case 'Viewable by Students': $examples[] = 'Y'; break;
+ case 'Viewable by Parents': $examples[] = 'Y'; break;
+ default:
+ // Custom fields: use type to generate appropriate example
+ $type = null;
+ $fieldId = null;
+
+ // Resolve custom field type
+ $internal = $userToDb[strtolower($h)] ?? $userToDb[$h] ?? null;
+ if ($internal && isset($customMap[$internal])) {
+ $type = strtolower($customMap[$internal]['type'] ?? 'text');
+ } else {
+ // Try by field name in custom map
+ foreach ($customMap as $canonical => $info) {
+ if (strtolower($info['name'] ?? '') === strtolower($h)) {
+ $type = strtolower($info['type'] ?? 'text');
+ break;
+ }
}
}
- }
- // Generate example based on field type
- switch ($type) {
- case 'date': $examples[] = date('Y-m-d'); break;
- case 'time':
- case 'timeofday': $examples[] = '14:00'; break;
- case 'number': $examples[] = '42'; break;
- case 'checkbox':
- case 'checkboxes':
- case 'yesno': $examples[] = 'Y'; break;
- case 'select':
- case 'dropdown': $examples[] = 'Option 1'; break;
- default: $examples[] = 'Sample value'; break;
- }
+ // Generate example based on field type
+ switch ($type) {
+ case 'date': $examples[] = date('Y-m-d'); break;
+ case 'time':
+ case 'timeofday': $examples[] = '14:00'; break;
+ case 'number': $examples[] = '42'; break;
+ case 'checkbox':
+ case 'checkboxes':
+ case 'yesno': $examples[] = 'Y'; break;
+ case 'select':
+ case 'dropdown': $examples[] = 'Option 1'; break;
+ default: $examples[] = 'Sample value'; break;
+ }
+ }
}
- }
- $filename = 'planner_template_'.date('Y-m-d').'.csv';
+ // Prepend Class column (accepts COURSE.CLASS format like FL07.1)
+ array_unshift($headers, 'Class');
+ array_unshift($examples, 'COURSE1.CLASSA');
- // Prepend Class column (accepts COURSE.CLASS format like FL07.1)
- array_unshift($headers, 'Class');
- array_unshift($examples, 'COURSE1.CLASSA');
+ $filename = 'planner_template_'.date('Y-m-d').'.csv';
- // Store template in session and redirect to export endpoint (Query Builder pattern)
- $hash = md5(serialize($headers).time());
- $session->set($hash, [
- 'headers' => $headers,
- 'examples' => $examples,
- 'filename' => $filename,
- ]);
+ error_log('[Planner Import] Outputting CSV directly');
- header('Location: '.$session->get('absoluteURL').'/modules/Planner/planner_import_export.php?hash='.$hash);
- exit;
+ // Output CSV directly (Gibbon pattern)
+ header('Pragma: public');
+ header('Expires: 0');
+ header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
+ header('Cache-Control: private', false);
+ header('Content-Type: text/csv; charset=utf-8');
+ header('Content-Disposition: attachment; filename="'.preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $filename).'"');
+
+ $out = fopen('php://output', 'w');
+ if ($out) {
+ fputcsv($out, $headers);
+ fputcsv($out, $examples);
+ fclose($out);
+ error_log('[Planner Import] CSV generated successfully');
+ }
+ exit;
+ } catch (Exception $e) {
+ error_log('[Planner Import] ERROR in downloadTemplate: ' . $e->getMessage());
+ error_log('[Planner Import] Stack trace: ' . $e->getTraceAsString());
+ $_SESSION['planner_import_errors'] = [__('Template download failed: ') . $e->getMessage()];
+ header('Location: '.$URL.'&return=error1');
+ exit;
+ } catch (Throwable $e) {
+ error_log('[Planner Import] FATAL ERROR in downloadTemplate: ' . $e->getMessage());
+ error_log('[Planner Import] Stack trace: ' . $e->getTraceAsString());
+ $_SESSION['planner_import_errors'] = [__('Template download failed: ') . $e->getMessage()];
+ header('Location: '.$URL.'&return=error1');
+ exit;
+ }
}
// Otherwise: fall back to processing upload (existing behaviour)
@@ -470,11 +492,12 @@ function getHeaderLookup($container, $pdo)
// --- 4. Extract optional fields and normalize enums ---
$summary = $data['summary'] ?? '';
$teachersNotes = $data['teachersNotes'] ?? '';
+ $gibbonSpaceID = resolveSpace($connection2, $data['gibbonSpaceID'] ?? '');
$homework = normalizeEnum($data['homework'] ?? '', ['N', 'Y'], 'N');
$homeworkDueDateTime = $data['homeworkDueDateTime'] ?? '';
$homeworkDetails = $data['homeworkDetails'] ?? '';
$homeworkSubmission = normalizeEnum($data['homeworkSubmission'] ?? '', ['N', 'Y'], 'N');
- $homeworkSubmissionRequired = normalizeEnum($data['homeworkSubmissionRequired'] ?? '', ['Optional', 'Required']);
+ $homeworkSubmissionRequired = normalizeEnumYesNo($data['homeworkSubmissionRequired'] ?? '', 'Optional', 'Required');
$homeworkSubmissionType = normalizeEnum($data['homeworkSubmissionType'] ?? '', ['', 'Link', 'File', 'Link/File'], '');
$viewableStudents = normalizeEnum($data['viewableStudents'] ?? '', ['Y', 'N'], 'Y');
$viewableParents = normalizeEnum($data['viewableParents'] ?? '', ['Y', 'N'], 'N');
@@ -490,6 +513,7 @@ function getHeaderLookup($container, $pdo)
'description' => $description,
'summary' => $summary,
'teachersNotes' => $teachersNotes,
+ 'gibbonSpaceID' => $gibbonSpaceID,
'homework' => $homework,
'homeworkDueDateTime' => $homeworkDueDateTime,
'homeworkDetails' => $homeworkDetails,
@@ -512,6 +536,7 @@ function getHeaderLookup($container, $pdo)
description=:description,
summary=:summary,
teachersNotes=:teachersNotes,
+ gibbonSpaceID=:gibbonSpaceID,
homework=:homework,
homeworkDueDateTime=:homeworkDueDateTime,
homeworkDetails=:homeworkDetails,
@@ -564,6 +589,19 @@ function normalizeEnum($value, $validOptions = [], $default = '')
return $default;
}
+// Helper: map Y/N to custom enum values (e.g., Y → 'Required', N → 'Optional')
+function normalizeEnumYesNo($value, $noValue = '', $yesValue = '')
+{
+ if (empty($value)) return $noValue;
+
+ $normalized = strtolower(trim($value));
+ if (in_array($normalized, ['y', 'yes', '1', 'true'])) {
+ return $yesValue;
+ } else {
+ return $noValue;
+ }
+}
+
// ------------------------------------------------------------
// Helper: resolve course/class
// ------------------------------------------------------------
@@ -587,3 +625,23 @@ function resolveClass($connection2, $courseShort, $classShort)
return $stmt->fetchColumn() ?: false;
}
+
+// Helper: resolve space name to gibbonSpaceID
+function resolveSpace($connection2, $spaceName)
+{
+ if (!$spaceName) {
+ return '';
+ }
+
+ // If it's already a number, assume it's a gibbonSpaceID
+ if (is_numeric($spaceName)) {
+ return $spaceName;
+ }
+
+ // Look up space by name
+ $sql = "SELECT gibbonSpaceID FROM gibbonSpace WHERE name = :name";
+ $stmt = $connection2->prepare($sql);
+ $stmt->execute(['name' => trim($spaceName)]);
+
+ return $stmt->fetchColumn() ?: '';
+}
diff --git a/modules/Planner/planner_import_export.php b/modules/Planner/planner_import_export.php
index d3176be8c8..aa07929f33 100644
--- a/modules/Planner/planner_import_export.php
+++ b/modules/Planner/planner_import_export.php
@@ -1,36 +1,157 @@
.
+*/
+
+$_POST['address'] = '/modules/Planner/planner_import.php';
+
+// System-wide include
include '../../gibbon.php';
-$hash = $_GET['hash'] ?? '';
-$data = $session->get($hash);
-if (empty($data) || !is_array($data)) {
- $URL = $session->get('absoluteURL').'/index.php?q=/modules/'.$session->get('module').'/planner_import.php&return=error1';
+$action = $_GET['action'] ?? '';
+
+$URL = $session->get('absoluteURL').'/index.php?q=/modules/Planner/planner_import.php';
+
+if (isActionAccessible($guid, $connection2, '/modules/Planner/planner_edit.php') == false) {
+ $URL = $URL.'&return=error0';
header("Location: {$URL}");
- exit();
+ exit;
}
-$headers = $data['headers'] ?? [];
-$examples = $data['examples'] ?? [];
-$filename = $data['filename'] ?? 'planner_template.csv';
-
-// Remove session entry
-$session->remove($hash);
-
-// Stream CSV
-header('Pragma: public');
-header('Expires: 0');
-header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
-header('Cache-Control: private', false);
-header('Content-Type: text/csv');
-header('Content-Disposition: attachment; filename="'.preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $filename).'"');
-
-$out = fopen('php://output', 'w');
-if ($out) {
- if (!empty($headers)) fputcsv($out, $headers);
- if (!empty($examples)) fputcsv($out, $examples);
- fclose($out);
+if ($action === 'downloadTemplate') {
+ try {
+ // Load the helper functions from planner_importProcess.php without executing it
+ // Extract just the function definitions
+ $processFile = file_get_contents(__DIR__ . '/planner_importProcess.php');
+
+ // Extract helper function definitions
+ preg_match('/function getPlannerColumns\(.*?\n\}/s', $processFile, $matches);
+ if (!empty($matches)) eval($matches[0]);
+
+ preg_match('/function getCustomFieldMap\(.*?\n\}/s', $processFile, $matches);
+ if (!empty($matches)) eval($matches[0]);
+
+ preg_match('/function getHookFields\(.*?\n\}/s', $processFile, $matches);
+ if (!empty($matches)) eval($matches[0]);
+
+ preg_match('/function getHeaderLookup\(.*?\n \}\n\}/s', $processFile, $matches);
+ if (!empty($matches)) eval($matches[0]);
+
+ error_log('[Planner Import Export] Starting template generation');
+
+ $core = getPlannerColumns($pdo);
+ error_log('[Planner Import Export] Got core columns: ' . count($core));
+
+ $lookup = getHeaderLookup($container, $pdo);
+ error_log('[Planner Import Export] Got header lookup');
+
+ $dbToUser = $lookup['dbToUser'];
+ $coreOrder = $lookup['coreOrder'];
+ $customMap = $lookup['customMap'];
+ $hookFields = $lookup['hookFields'];
+
+ // Filter custom fields to only include active ones
+ $customFieldGateway = $container->get(\Gibbon\Domain\System\CustomFieldGateway::class);
+ $customMap = array_filter($customMap, function($field) {
+ return ($field['active'] ?? 'Y') === 'Y';
+ });
+
+ // Build ordered headers using friendly labels - match planner_add.php form order
+ $headers = [];
+ $customInserted = false;
+ foreach ($coreOrder as $db) {
+ if (in_array($db, $core)) {
+ $headers[] = $dbToUser[$db] ?? $db;
+
+ // After the Lesson Name, insert all custom fields
+ if (!$customInserted && $db === 'name') {
+ foreach ($customMap as $canonical => $info) {
+ $label = $info['name'] ?? $canonical;
+ $headers[] = $label;
+ }
+ $customInserted = true;
+ }
+ }
+ }
+ // Hook fields
+ foreach ($hookFields as $header => $label) {
+ $headers[] = $label;
+ }
+
+ error_log('[Planner Import Export] Generated headers: ' . json_encode($headers));
+
+ // Generate example row
+ $examples = [];
+ foreach ($headers as $h) {
+ switch ($h) {
+ case 'Class': $examples[] = 'COURSE1.CLASSA'; break;
+ case 'Date': $examples[] = date('Y-m-d'); break;
+ case 'Start Time': $examples[] = '09:00'; break;
+ case 'End Time': $examples[] = '10:00'; break;
+ case 'Lesson Name': $examples[] = 'Introduction to Topic'; break;
+ case 'Location': $examples[] = 'Room 101'; break;
+ case 'Unit': $examples[] = '1'; break;
+ case 'Summary': $examples[] = 'Key learning objectives'; break;
+ case 'Lesson Details': $examples[] = 'Detailed content and activities'; break;
+ case "Teacher's Notes": $examples[] = 'Notes for preparation'; break;
+ case 'Add Homework': $examples[] = 'Y'; break;
+ case 'Homework Due Date/Time': $examples[] = date('Y-m-d H:i:s'); break;
+ case 'Homework Time Cap': $examples[] = '30'; break;
+ case 'Homework Details': $examples[] = 'Complete exercises 1-5'; break;
+ case 'Online Submission': $examples[] = 'Y'; break;
+ case 'Submission Open Date': $examples[] = date('Y-m-d'); break;
+ case 'Submission Drafts': $examples[] = '2'; break;
+ case 'Submission Type': $examples[] = 'Link'; break;
+ case 'Submission Required': $examples[] = 'Y'; break;
+ case 'Viewable by Students': $examples[] = 'Y'; break;
+ case 'Viewable by Parents': $examples[] = 'Y'; break;
+ default: $examples[] = 'Sample value'; break;
+ }
+ }
+
+ $filename = 'planner_template_'.date('Y-m-d').'.csv';
+
+ // Output CSV directly
+ header('Pragma: public');
+ header('Expires: 0');
+ header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
+ header('Cache-Control: private', false);
+ header('Content-Type: text/csv; charset=utf-8');
+ header('Content-Disposition: attachment; filename="'.preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $filename).'"');
+
+ $out = fopen('php://output', 'w');
+ if ($out) {
+ fputcsv($out, $headers);
+ fputcsv($out, $examples);
+ fclose($out);
+ error_log('[Planner Import Export] CSV generated successfully');
+ }
+ exit;
+ } catch (Exception $e) {
+ error_log('[Planner Import Export] ERROR: ' . $e->getMessage());
+ $URL = $URL.'&return=error1';
+ header("Location: {$URL}");
+ exit;
+ }
}
+// If we get here, invalid action
+$URL = $URL.'&return=error1';
+header("Location: {$URL}");
exit;
diff --git a/modules/Planner/planner_view_full.php b/modules/Planner/planner_view_full.php
index a749afe8a0..b2e927a7c4 100644
--- a/modules/Planner/planner_view_full.php
+++ b/modules/Planner/planner_view_full.php
@@ -314,7 +314,13 @@
$col->addColumn('class', __('Class'))->format(Format::using('courseClassName', ['course', 'class']));
$col->addColumn('date', __('Date'))->format(Format::using('date', 'date'));
$col->addColumn('time', __('Time'))->format(Format::using('timeRange', ['timeStart', 'timeEnd']));
- $col->addColumn('location', __('Location'))->format(function ($values) {return !empty($values['spaceName']) ? $values['spaceName'] : '';});
+ $col->addColumn('location', __('Location'))->addClass('col-span-3')->format(function ($values) {
+ // Lesson plan location takes precedence as override
+ if (!empty($values['spaceName'])) {
+ return $values['spaceName'];
+ }
+ return '';
+ });
$col->addColumn('summary', __('Summary'))->addClass('col-span-3');
diff --git a/modules/Planner/src/Tables/LessonTable.php b/modules/Planner/src/Tables/LessonTable.php
index 6df7832155..50de838d47 100644
--- a/modules/Planner/src/Tables/LessonTable.php
+++ b/modules/Planner/src/Tables/LessonTable.php
@@ -160,7 +160,7 @@ public function create($gibbonSchoolYearID, $gibbonCourseClassID, $gibbonPersonI
->displayLabel();
}
- if ($editAccess) {
+ if ($editAccess) {
$table->addHeaderAction('add', __('Add'))
->setURL('/modules/Planner/planner_add.php')
->addParam('gibbonCourseClassID', $gibbonCourseClassID)
diff --git a/src/UI/Timetable/Layers/ClassesLayer.php b/src/UI/Timetable/Layers/ClassesLayer.php
index 2d1c3bd4f9..6c43a10c11 100644
--- a/src/UI/Timetable/Layers/ClassesLayer.php
+++ b/src/UI/Timetable/Layers/ClassesLayer.php
@@ -168,6 +168,17 @@ protected function loadItemsByPerson(\DatePeriod $dateRange, TimetableContext $c
$planner = $lessons[$class['lessonID']] ?? [];
if (!empty($planner)) {
unset($lessons[$class['lessonID']]);
+
+ // Use lesson plan location as override if available
+ if (!empty($planner['plannerRoomName'])) {
+ $item->set('subtitle', $planner['plannerRoomName'])
+ ->set('location', $planner['plannerRoomName']);
+ }
+
+ // Use lesson plan phone if available
+ if (!empty($planner['plannerRoomPhone'])) {
+ $item->set('phone', $planner['plannerRoomPhone']);
+ }
}
if ($context->get('edit')) {
From cc5008d8f22a3e413236cbb4df8943835a3dc304 Mon Sep 17 00:00:00 2001
From: Father Vlasie <33334298+fvlasie@users.noreply.github.com>
Date: Tue, 3 Feb 2026 15:57:39 -0800
Subject: [PATCH 4/4] remove debug code
---
modules/Planner/planner_importProcess.php | 13 -------------
modules/Planner/planner_import_export.php | 8 --------
2 files changed, 21 deletions(-)
diff --git a/modules/Planner/planner_importProcess.php b/modules/Planner/planner_importProcess.php
index 97d107a524..536304023d 100644
--- a/modules/Planner/planner_importProcess.php
+++ b/modules/Planner/planner_importProcess.php
@@ -225,13 +225,9 @@ function getHeaderLookup($container, $pdo)
// Template download: core + custom + hook fields
if ($action === 'downloadTemplate') {
try {
- error_log('[Planner Import] Starting downloadTemplate action');
-
$core = getPlannerColumns($pdo);
- error_log('[Planner Import] Got core columns: ' . json_encode($core));
$lookup = getHeaderLookup($container, $pdo);
- error_log('[Planner Import] Got header lookup successfully');
$dbToUser = $lookup['dbToUser'];
$userToDb = $lookup['userToDb'];
@@ -263,8 +259,6 @@ function getHeaderLookup($container, $pdo)
$headers[] = $label;
}
- error_log('[Planner Import] Generated headers: ' . json_encode($headers));
-
$examples = [];
foreach ($headers as $h) {
// Generate actual example values (not just format hints)
@@ -331,8 +325,6 @@ function getHeaderLookup($container, $pdo)
$filename = 'planner_template_'.date('Y-m-d').'.csv';
- error_log('[Planner Import] Outputting CSV directly');
-
// Output CSV directly (Gibbon pattern)
header('Pragma: public');
header('Expires: 0');
@@ -346,18 +338,13 @@ function getHeaderLookup($container, $pdo)
fputcsv($out, $headers);
fputcsv($out, $examples);
fclose($out);
- error_log('[Planner Import] CSV generated successfully');
}
exit;
} catch (Exception $e) {
- error_log('[Planner Import] ERROR in downloadTemplate: ' . $e->getMessage());
- error_log('[Planner Import] Stack trace: ' . $e->getTraceAsString());
$_SESSION['planner_import_errors'] = [__('Template download failed: ') . $e->getMessage()];
header('Location: '.$URL.'&return=error1');
exit;
} catch (Throwable $e) {
- error_log('[Planner Import] FATAL ERROR in downloadTemplate: ' . $e->getMessage());
- error_log('[Planner Import] Stack trace: ' . $e->getTraceAsString());
$_SESSION['planner_import_errors'] = [__('Template download failed: ') . $e->getMessage()];
header('Location: '.$URL.'&return=error1');
exit;
diff --git a/modules/Planner/planner_import_export.php b/modules/Planner/planner_import_export.php
index aa07929f33..b442bff1b2 100644
--- a/modules/Planner/planner_import_export.php
+++ b/modules/Planner/planner_import_export.php
@@ -53,13 +53,9 @@
preg_match('/function getHeaderLookup\(.*?\n \}\n\}/s', $processFile, $matches);
if (!empty($matches)) eval($matches[0]);
- error_log('[Planner Import Export] Starting template generation');
-
$core = getPlannerColumns($pdo);
- error_log('[Planner Import Export] Got core columns: ' . count($core));
$lookup = getHeaderLookup($container, $pdo);
- error_log('[Planner Import Export] Got header lookup');
$dbToUser = $lookup['dbToUser'];
$coreOrder = $lookup['coreOrder'];
@@ -94,8 +90,6 @@
$headers[] = $label;
}
- error_log('[Planner Import Export] Generated headers: ' . json_encode($headers));
-
// Generate example row
$examples = [];
foreach ($headers as $h) {
@@ -140,11 +134,9 @@
fputcsv($out, $headers);
fputcsv($out, $examples);
fclose($out);
- error_log('[Planner Import Export] CSV generated successfully');
}
exit;
} catch (Exception $e) {
- error_log('[Planner Import Export] ERROR: ' . $e->getMessage());
$URL = $URL.'&return=error1';
header("Location: {$URL}");
exit;