feat: SQLite DB with migrations replaces all JSON file storage

- Add db.php with PDO singleton, migration runner, and all helper functions
- Add migrations/001_initial_schema.sql (full schema)
- Add migrations/002_import_json.php (one-time JSON → DB importer)
- Add _incl/switch_tenant.php POST endpoint for tenant/centro switching
- Update tools.auth.php: DB-backed login, cookie auth, session reload, init_active_centro()
- Update all sysadmin pages (users, centros, aularios, invitations, reset_password) to use DB
- Update aulatek/index.php, aulario.php, supercafe.php, supercafe_edit.php to use DB
- Update aulatek/comedor.php and api/comedor.php to use DB
- Update aulatek/paneldiario.php: aulario config + comedor data from DB
- Update aulatek/proyectos.php: aulario config + sharing metadata from DB
- Update club/cal.php, index.php, edit_data.php, upload/upload.php to use DB
- Update account/index.php: rich profile, tenant list, aula list, session info, permissions
- Update pre-body.php account dropdown: shows active org + inline tenant switcher
- Update DATA_STRUCTURE.md to document DB approach and migration system

Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-06 22:00:48 +00:00
parent 937a0f4083
commit 0c362fd40b
30 changed files with 2050 additions and 1646 deletions

View File

@@ -2,24 +2,7 @@
header("Content-Type: application/json; charset=utf-8");
require_once "_incl/auth_redir.php";
require_once "../_incl/tools.security.php";
function menu_types_path($centro_id, $aulario_id) {
$centro = safe_centro_id($centro_id);
$aulario = safe_id_segment($aulario_id);
if ($centro === '' || $aulario === '') {
return null;
}
return "/DATA/entreaulas/Centros/$centro/Aularios/$aulario/Comedor-MenuTypes.json";
}
function comedor_day_base_dir($centro_id, $aulario_id, $ym, $day) {
$centro = safe_centro_id($centro_id);
$aulario = safe_id_segment($aulario_id);
if ($centro === '' || $aulario === '') {
return null;
}
return "/DATA/entreaulas/Centros/$centro/Aularios/$aulario/Comedor/$ym/$day";
}
require_once "../../_incl/db.php";
// Check permissions
if (!in_array("entreaulas:docente", $_SESSION["auth_data"]["permissions"] ?? [])) {
@@ -33,64 +16,48 @@ if ($centro_id === "") {
die(json_encode(["error" => "Centro not found in session", "code" => "INVALID_SESSION"]));
}
$action = $_GET["action"] ?? ($_POST["action"] ?? "");
$action = $_GET["action"] ?? ($_POST["action"] ?? "");
$aulario_id = safe_id_segment($_GET["aulario"] ?? $_POST["aulario"] ?? "");
// Validate aulario_id
if ($aulario_id === "") {
http_response_code(400);
die(json_encode(["error" => "aulario parameter is required", "code" => "MISSING_PARAM"]));
}
// Verify that the user has access to this aulario
$userAulas = $_SESSION["auth_data"]["entreaulas"]["aulas"] ?? [];
$userAulas = array_values(array_filter(array_map('safe_id_segment', $userAulas)));
$userAulas = array_values(array_filter(array_map('safe_id_segment', $_SESSION["auth_data"]["entreaulas"]["aulas"] ?? [])));
if (!in_array($aulario_id, $userAulas, true)) {
http_response_code(403);
die(json_encode(["error" => "Access denied to this aulario", "code" => "FORBIDDEN"]));
}
$aulario_path = safe_aulario_config_path($centro_id, $aulario_id);
$aulario = ($aulario_path && file_exists($aulario_path)) ? json_decode(file_get_contents($aulario_path), true) : null;
$aulario = db_get_aulario($centro_id, $aulario_id);
// Handle shared comedor data
$source_aulario_id = $aulario_id;
$is_shared = false;
if ($aulario && !empty($aulario["shared_comedor_from"])) {
$shared_from = safe_id_segment($aulario["shared_comedor_from"]);
$shared_aulario_path = safe_aulario_config_path($centro_id, $shared_from);
if ($shared_aulario_path && file_exists($shared_aulario_path)) {
if (db_get_aulario($centro_id, $shared_from)) {
$source_aulario_id = $shared_from;
$is_shared = true;
}
}
// Check edit permissions (must be sysadmin and not shared)
$canEdit = in_array("sysadmin:access", $_SESSION["auth_data"]["permissions"] ?? []) && !$is_shared;
// Helper functions
function get_menu_types($centro_id, $source_aulario_id) {
$menuTypesPath = menu_types_path($centro_id, $source_aulario_id);
$defaultMenuTypes = [
["id" => "basal", "label" => "Menú basal", "color" => "#0d6efd"],
["id" => "vegetariano", "label" => "Menú vegetariano", "color" => "#198754"],
["id" => "alergias", "label" => "Menú alergias", "color" => "#dc3545"],
];
$defaultMenuTypes = [
["id" => "basal", "label" => "Menú basal", "color" => "#0d6efd"],
["id" => "vegetariano", "label" => "Menú vegetariano", "color" => "#198754"],
["id" => "alergias", "label" => "Menú alergias", "color" => "#dc3545"],
];
if ($menuTypesPath === null) {
function get_menu_types($centro_id, $source_aulario_id) {
global $defaultMenuTypes;
$types = db_get_comedor_menu_types($centro_id, $source_aulario_id);
if (empty($types)) {
db_set_comedor_menu_types($centro_id, $source_aulario_id, $defaultMenuTypes);
return $defaultMenuTypes;
}
if (!file_exists($menuTypesPath)) {
if (!is_dir(dirname($menuTypesPath))) {
mkdir(dirname($menuTypesPath), 0777, true);
}
file_put_contents($menuTypesPath, json_encode($defaultMenuTypes, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
return $defaultMenuTypes;
}
$menuTypes = json_decode(@file_get_contents($menuTypesPath), true);
return (is_array($menuTypes) && count($menuTypes) > 0) ? $menuTypes : $defaultMenuTypes;
return $types;
}
function blank_menu() {
@@ -98,7 +65,7 @@ function blank_menu() {
"plates" => [
"primero" => ["name" => "", "pictogram" => ""],
"segundo" => ["name" => "", "pictogram" => ""],
"postre" => ["name" => "", "pictogram" => ""],
"postre" => ["name" => "", "pictogram" => ""],
]
];
}
@@ -113,43 +80,25 @@ switch ($action) {
case "get_menu_types":
handle_get_menu_types();
break;
case "get_menu":
handle_get_menu();
break;
case "save_menu":
if (!$canEdit) {
http_response_code(403);
die(json_encode(["error" => "Insufficient permissions to edit", "code" => "FORBIDDEN"]));
}
if (!$canEdit) { http_response_code(403); die(json_encode(["error" => "Insufficient permissions to edit", "code" => "FORBIDDEN"])); }
handle_save_menu();
break;
case "add_menu_type":
if (!$canEdit) {
http_response_code(403);
die(json_encode(["error" => "Insufficient permissions to edit", "code" => "FORBIDDEN"]));
}
if (!$canEdit) { http_response_code(403); die(json_encode(["error" => "Insufficient permissions to edit", "code" => "FORBIDDEN"])); }
handle_add_menu_type();
break;
case "delete_menu_type":
if (!$canEdit) {
http_response_code(403);
die(json_encode(["error" => "Insufficient permissions to edit", "code" => "FORBIDDEN"]));
}
if (!$canEdit) { http_response_code(403); die(json_encode(["error" => "Insufficient permissions to edit", "code" => "FORBIDDEN"])); }
handle_delete_menu_type();
break;
case "rename_menu_type":
if (!$canEdit) {
http_response_code(403);
die(json_encode(["error" => "Insufficient permissions to edit", "code" => "FORBIDDEN"]));
}
if (!$canEdit) { http_response_code(403); die(json_encode(["error" => "Insufficient permissions to edit", "code" => "FORBIDDEN"])); }
handle_rename_menu_type();
break;
default:
http_response_code(400);
die(json_encode(["error" => "Invalid action", "code" => "INVALID_ACTION"]));
@@ -157,298 +106,102 @@ switch ($action) {
function handle_get_menu_types() {
global $centro_id, $source_aulario_id;
$menuTypes = get_menu_types($centro_id, $source_aulario_id);
echo json_encode([
"success" => true,
"menu_types" => $menuTypes
]);
echo json_encode(["success" => true, "menu_types" => get_menu_types($centro_id, $source_aulario_id)]);
}
function handle_get_menu() {
global $centro_id, $source_aulario_id;
$date = $_GET["date"] ?? date("Y-m-d");
$menuTypeId = safe_id_segment($_GET["menu"] ?? "");
// Validate date
$dateObj = DateTime::createFromFormat("Y-m-d", $date);
if (!$dateObj) {
http_response_code(400);
die(json_encode(["error" => "Invalid date format", "code" => "INVALID_FORMAT"]));
}
if (!$dateObj) { http_response_code(400); die(json_encode(["error" => "Invalid date format", "code" => "INVALID_FORMAT"])); }
$date = $dateObj->format("Y-m-d");
// Get menu types
$menuTypes = get_menu_types($centro_id, $source_aulario_id);
$menuTypeIds = [];
foreach ($menuTypes as $t) {
if (!empty($t["id"])) {
$menuTypeIds[] = $t["id"];
}
}
if ($menuTypeId === "" || !in_array($menuTypeId, $menuTypeIds)) {
$menuTypeId = $menuTypeIds[0] ?? "basal";
}
// Get menu data
$ym = $dateObj->format("Y-m");
$menuTypes = get_menu_types($centro_id, $source_aulario_id);
$menuTypeIds = array_column($menuTypes, "id");
if ($menuTypeId === "" || !in_array($menuTypeId, $menuTypeIds)) { $menuTypeId = $menuTypeIds[0] ?? "basal"; }
$ym = $dateObj->format("Y-m");
$day = $dateObj->format("d");
$baseDir = comedor_day_base_dir($centro_id, $source_aulario_id, $ym, $day);
if ($baseDir === null) {
http_response_code(400);
die(json_encode(["error" => "Invalid path parameters", "code" => "INVALID_PATH"]));
}
$dataPath = "$baseDir/_datos.json";
$menuData = [
"date" => $date,
"menus" => []
];
if (file_exists($dataPath)) {
$existing = json_decode(file_get_contents($dataPath), true);
if (is_array($existing)) {
$menuData = array_merge($menuData, $existing);
}
}
if (!isset($menuData["menus"][$menuTypeId])) {
$menuData["menus"][$menuTypeId] = blank_menu();
}
$menuForType = $menuData["menus"][$menuTypeId];
echo json_encode([
"success" => true,
"date" => $date,
"menu_type" => $menuTypeId,
"menu_types" => $menuTypes,
"menu" => $menuForType
]);
$menuData = ["date" => $date, "menus" => []];
$existing = db_get_comedor_entry($centro_id, $source_aulario_id, $ym, $day);
if (!empty($existing)) { $menuData = array_merge($menuData, $existing); }
if (!isset($menuData["menus"][$menuTypeId])) { $menuData["menus"][$menuTypeId] = blank_menu(); }
echo json_encode(["success" => true, "date" => $date, "menu_type" => $menuTypeId, "menu_types" => $menuTypes, "menu" => $menuData["menus"][$menuTypeId]]);
}
function handle_save_menu() {
global $centro_id, $source_aulario_id;
// Parse JSON body
$input = json_decode(file_get_contents("php://input"), true);
if (!$input) {
$input = $_POST;
}
$date = $input["date"] ?? date("Y-m-d");
$input = json_decode(file_get_contents("php://input"), true) ?: $_POST;
$date = $input["date"] ?? date("Y-m-d");
$menuTypeId = safe_id_segment($input["menu_type"] ?? "");
$plates = $input["plates"] ?? [];
// Validate date
$plates = $input["plates"] ?? [];
$dateObj = DateTime::createFromFormat("Y-m-d", $date);
if (!$dateObj) {
http_response_code(400);
die(json_encode(["error" => "Invalid date format", "code" => "INVALID_FORMAT"]));
}
if (!$dateObj) { http_response_code(400); die(json_encode(["error" => "Invalid date format", "code" => "INVALID_FORMAT"])); }
$date = $dateObj->format("Y-m-d");
// Validate menu type
$menuTypes = get_menu_types($centro_id, $source_aulario_id);
$validMenuTypeIds = [];
foreach ($menuTypes as $t) {
if (!empty($t["id"])) {
$validMenuTypeIds[] = $t["id"];
}
}
if (!in_array($menuTypeId, $validMenuTypeIds)) {
http_response_code(400);
die(json_encode(["error" => "Invalid menu type", "code" => "INVALID_MENU_TYPE"]));
}
// Get existing menu data
$ym = $dateObj->format("Y-m");
$menuTypes = get_menu_types($centro_id, $source_aulario_id);
$validMenuTypeIds = array_column($menuTypes, "id");
if (!in_array($menuTypeId, $validMenuTypeIds)) { http_response_code(400); die(json_encode(["error" => "Invalid menu type", "code" => "INVALID_MENU_TYPE"])); }
$ym = $dateObj->format("Y-m");
$day = $dateObj->format("d");
$baseDir = comedor_day_base_dir($centro_id, $source_aulario_id, $ym, $day);
if ($baseDir === null) {
http_response_code(400);
die(json_encode(["error" => "Invalid path parameters", "code" => "INVALID_PATH"]));
}
$dataPath = "$baseDir/_datos.json";
$menuData = [
"date" => $date,
"menus" => []
];
if (file_exists($dataPath)) {
$existing = json_decode(file_get_contents($dataPath), true);
if (is_array($existing)) {
$menuData = array_merge($menuData, $existing);
$menuData = ["date" => $date, "menus" => []];
$existing = db_get_comedor_entry($centro_id, $source_aulario_id, $ym, $day);
if (!empty($existing)) { $menuData = array_merge($menuData, $existing); }
if (!isset($menuData["menus"][$menuTypeId])) { $menuData["menus"][$menuTypeId] = blank_menu(); }
foreach (["primero", "segundo", "postre"] as $plateKey) {
if (isset($plates[$plateKey]["name"])) {
$menuData["menus"][$menuTypeId]["plates"][$plateKey]["name"] = trim($plates[$plateKey]["name"]);
}
}
if (!isset($menuData["menus"][$menuTypeId])) {
$menuData["menus"][$menuTypeId] = blank_menu();
}
// Update plates
$validPlates = ["primero", "segundo", "postre"];
foreach ($validPlates as $plateKey) {
if (isset($plates[$plateKey])) {
if (isset($plates[$plateKey]["name"])) {
$menuData["menus"][$menuTypeId]["plates"][$plateKey]["name"] = trim($plates[$plateKey]["name"]);
}
// Note: pictogram upload not supported via JSON API - use form-data instead
}
}
// Save menu
if (!is_dir($baseDir)) {
mkdir($baseDir, 0777, true);
}
file_put_contents($dataPath, json_encode($menuData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
echo json_encode([
"success" => true,
"date" => $date,
"menu_type" => $menuTypeId,
"menu" => $menuData["menus"][$menuTypeId]
]);
db_set_comedor_entry($centro_id, $source_aulario_id, $ym, $day, $menuData);
echo json_encode(["success" => true, "date" => $date, "menu_type" => $menuTypeId, "menu" => $menuData["menus"][$menuTypeId]]);
}
function handle_add_menu_type() {
global $centro_id, $source_aulario_id;
$input = json_decode(file_get_contents("php://input"), true);
if (!$input) {
$input = $_POST;
}
$newId = safe_id_segment(strtolower(trim($input["id"] ?? "")));
$input = json_decode(file_get_contents("php://input"), true) ?: $_POST;
$newId = safe_id_segment(strtolower(trim($input["id"] ?? "")));
$newLabel = trim($input["label"] ?? "");
$newColor = trim($input["color"] ?? "#0d6efd");
if ($newId === "" || $newLabel === "") {
http_response_code(400);
die(json_encode(["error" => "id and label are required", "code" => "MISSING_PARAM"]));
}
$menuTypesPath = menu_types_path($centro_id, $source_aulario_id);
if ($menuTypesPath === null) {
http_response_code(400);
die(json_encode(["error" => "Invalid path parameters", "code" => "INVALID_PATH"]));
}
if ($newId === "" || $newLabel === "") { http_response_code(400); die(json_encode(["error" => "id and label are required", "code" => "MISSING_PARAM"])); }
$menuTypes = get_menu_types($centro_id, $source_aulario_id);
// Check if already exists
foreach ($menuTypes as $t) {
if (($t["id"] ?? "") === $newId) {
http_response_code(400);
die(json_encode(["error" => "Menu type already exists", "code" => "DUPLICATE"]));
}
if (($t["id"] ?? "") === $newId) { http_response_code(400); die(json_encode(["error" => "Menu type already exists", "code" => "DUPLICATE"])); }
}
$menuTypes[] = ["id" => $newId, "label" => $newLabel, "color" => $newColor];
file_put_contents($menuTypesPath, json_encode($menuTypes, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
echo json_encode([
"success" => true,
"menu_type" => ["id" => $newId, "label" => $newLabel, "color" => $newColor],
"message" => "Menu type added successfully"
]);
db_set_comedor_menu_types($centro_id, $source_aulario_id, $menuTypes);
echo json_encode(["success" => true, "menu_type" => ["id" => $newId, "label" => $newLabel, "color" => $newColor], "message" => "Menu type added successfully"]);
}
function handle_delete_menu_type() {
global $centro_id, $source_aulario_id;
$input = json_decode(file_get_contents("php://input"), true);
if (!$input) {
$input = $_POST;
}
$input = json_decode(file_get_contents("php://input"), true) ?: $_POST;
$deleteId = safe_id_segment(trim($input["id"] ?? ""));
if ($deleteId === "") {
http_response_code(400);
die(json_encode(["error" => "id is required", "code" => "MISSING_PARAM"]));
}
$menuTypesPath = menu_types_path($centro_id, $source_aulario_id);
if ($menuTypesPath === null) {
http_response_code(400);
die(json_encode(["error" => "Invalid path parameters", "code" => "INVALID_PATH"]));
}
$menuTypes = get_menu_types($centro_id, $source_aulario_id);
$deleted = false;
$newMenuTypes = [];
foreach ($menuTypes as $t) {
if (($t["id"] ?? "") === $deleteId) {
$deleted = true;
} else {
$newMenuTypes[] = $t;
}
}
if (!$deleted) {
http_response_code(404);
die(json_encode(["error" => "Menu type not found", "code" => "NOT_FOUND"]));
}
file_put_contents($menuTypesPath, json_encode($newMenuTypes, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
echo json_encode([
"success" => true,
"message" => "Menu type deleted successfully"
]);
if ($deleteId === "") { http_response_code(400); die(json_encode(["error" => "id is required", "code" => "MISSING_PARAM"])); }
$menuTypes = get_menu_types($centro_id, $source_aulario_id);
$newMenuTypes = array_values(array_filter($menuTypes, fn($t) => ($t["id"] ?? "") !== $deleteId));
if (count($newMenuTypes) === count($menuTypes)) { http_response_code(404); die(json_encode(["error" => "Menu type not found", "code" => "NOT_FOUND"])); }
db_set_comedor_menu_types($centro_id, $source_aulario_id, $newMenuTypes);
echo json_encode(["success" => true, "message" => "Menu type deleted successfully"]);
}
function handle_rename_menu_type() {
global $centro_id, $source_aulario_id;
$input = json_decode(file_get_contents("php://input"), true);
if (!$input) {
$input = $_POST;
}
$input = json_decode(file_get_contents("php://input"), true) ?: $_POST;
$renameId = safe_id_segment(trim($input["id"] ?? ""));
$newLabel = trim($input["label"] ?? "");
$newColor = trim($input["color"] ?? "");
if ($renameId === "" || $newLabel === "") {
http_response_code(400);
die(json_encode(["error" => "id and label are required", "code" => "MISSING_PARAM"]));
}
$menuTypesPath = menu_types_path($centro_id, $source_aulario_id);
if ($menuTypesPath === null) {
http_response_code(400);
die(json_encode(["error" => "Invalid path parameters", "code" => "INVALID_PATH"]));
}
if ($renameId === "" || $newLabel === "") { http_response_code(400); die(json_encode(["error" => "id and label are required", "code" => "MISSING_PARAM"])); }
$menuTypes = get_menu_types($centro_id, $source_aulario_id);
$found = false;
foreach ($menuTypes as &$t) {
if (($t["id"] ?? "") === $renameId) {
$t["label"] = $newLabel;
if ($newColor !== "") {
$t["color"] = $newColor;
}
if ($newColor !== "") { $t["color"] = $newColor; }
$found = true;
break;
}
}
unset($t);
if (!$found) {
http_response_code(404);
die(json_encode(["error" => "Menu type not found", "code" => "NOT_FOUND"]));
}
file_put_contents($menuTypesPath, json_encode($menuTypes, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
echo json_encode([
"success" => true,
"menu_type" => ["id" => $renameId, "label" => $newLabel, "color" => $newColor],
"message" => "Menu type renamed successfully"
]);
if (!$found) { http_response_code(404); die(json_encode(["error" => "Menu type not found", "code" => "NOT_FOUND"])); }
db_set_comedor_menu_types($centro_id, $source_aulario_id, $menuTypes);
echo json_encode(["success" => true, "message" => "Menu type renamed successfully"]);
}