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

@@ -1,4 +1,65 @@
# Example Data Structure for Axia4
# Data Architecture for Axia4
Axia4 uses a **SQLite database** (`/DATA/axia4.sqlite`) for all structured data, with the
filesystem reserved for binary assets (photos, uploaded files, project documents).
## Database (`/DATA/axia4.sqlite`)
The schema is defined in `public_html/_incl/migrations/001_initial_schema.sql` and
applied automatically on first boot via `db.php`.
| Table | Replaces |
|----------------------|---------------------------------------------------|
| `config` | `/DATA/AuthConfig.json` |
| `users` | `/DATA/Usuarios/*.json` |
| `invitations` | `/DATA/Invitaciones_de_usuarios.json` |
| `centros` | Directory existence at `.../Centros/{id}/` |
| `user_centros` | `entreaulas.centro` + `entreaulas.aulas` in users |
| `aularios` | `.../Aularios/{id}.json` |
| `supercafe_menu` | `.../SuperCafe/Menu.json` |
| `supercafe_orders` | `.../SuperCafe/Comandas/*.json` |
| `comedor_menu_types` | `.../Comedor-MenuTypes.json` |
| `comedor_entries` | `.../Comedor/{ym}/{day}/_datos.json` |
| `club_events` | `/DATA/club/IMG/{date}/data.json` |
| `club_config` | `/DATA/club/config.json` |
## Migrations
Migrations live in `public_html/_incl/migrations/`:
- `001_initial_schema.sql` — DDL for all tables.
- `002_import_json.php` — One-time importer: reads existing JSON files and
inserts them into the database. Run automatically on first boot if JSON files
exist and the DB is empty.
## Filesystem (binary / large assets)
```
DATA/
└── entreaulas/
└── Centros/
└── {centro_id}/
├── Aularios/
│ └── {aulario_id}/
│ ├── Alumnos/ # Student photo directories
│ │ └── {alumno}/photo.jpg
│ ├── Comedor/{ym}/{day}/ # Comedor pictogram images
│ └── Proyectos/ # Project binary files
└── Panel/
└── Actividades/{name}/photo.jpg # Activity photos
└── club/
└── IMG/{date}/ # Club event photos (still on filesystem)
```
## Multi-Tenant Support
A user can belong to **multiple centros** (organizations). The active centro
is stored in `$_SESSION['active_centro']` and can be switched at any time via
`POST /_incl/switch_tenant.php`.
The account page (`/account/`) shows all assigned organizations and lets the
user switch between them.
This directory contains example data files that demonstrate the structure needed for the Axia4 application.

View File

@@ -8,7 +8,7 @@ FROM dunglas/frankenphp
# && rm -rf /var/lib/apt/lists/*
# Configure PHP extensions
RUN install-php-extensions gd opcache
RUN install-php-extensions gd opcache pdo pdo_sqlite
# Set working directory
WORKDIR /var/www/html

View File

@@ -8,7 +8,7 @@ FROM dunglas/frankenphp
# && rm -rf /var/lib/apt/lists/*
# Configure PHP extensions
RUN install-php-extensions gd opcache
RUN install-php-extensions gd opcache pdo pdo_sqlite
# Set working directory
WORKDIR /var/www/html

589
public_html/_incl/db.php Normal file
View File

@@ -0,0 +1,589 @@
<?php
/**
* Axia4 Database Layer
*
* Provides a PDO SQLite connection and a lightweight migration runner.
* All application data previously stored as JSON files under /DATA is now
* persisted in /DATA/axia4.db.
*
* Usage: db() → returns the shared PDO instance (auto-migrates on first call).
*/
define('DB_PATH', '/DATA/axia4.db');
define('MIGRATIONS_DIR', __DIR__ . '/migrations');
// ── Connection ────────────────────────────────────────────────────────────────
function db(): PDO
{
static $pdo = null;
if ($pdo !== null) {
return $pdo;
}
if (!is_dir('/DATA')) {
mkdir('/DATA', 0755, true);
}
$pdo = new PDO('sqlite:' . DB_PATH);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->exec('PRAGMA journal_mode = WAL');
$pdo->exec('PRAGMA foreign_keys = ON');
$pdo->exec('PRAGMA synchronous = NORMAL');
db_migrate($pdo);
return $pdo;
}
// ── Migration runner ──────────────────────────────────────────────────────────
function db_migrate(PDO $pdo): void
{
$pdo->exec(
'CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
applied_at TEXT NOT NULL DEFAULT (datetime(\'now\'))
)'
);
$applied = $pdo->query('SELECT version FROM schema_migrations ORDER BY version')
->fetchAll(PDO::FETCH_COLUMN);
$files = glob(MIGRATIONS_DIR . '/*.{sql,php}', GLOB_BRACE) ?: [];
sort($files);
foreach ($files as $file) {
if (!preg_match('/^(\d+)/', basename($file), $m)) {
continue;
}
$version = (int) $m[1];
if (in_array($version, $applied, true)) {
continue;
}
if (str_ends_with($file, '.sql')) {
$pdo->exec((string) file_get_contents($file));
} elseif (str_ends_with($file, '.php')) {
// PHP migration receives the connection as $db
$db = $pdo;
require $file;
}
$pdo->prepare('INSERT INTO schema_migrations (version) VALUES (?)')->execute([$version]);
}
}
// ── Config helpers ────────────────────────────────────────────────────────────
function db_get_config(string $key, $default = null)
{
$stmt = db()->prepare('SELECT value FROM config WHERE key = ?');
$stmt->execute([$key]);
$row = $stmt->fetch();
if ($row === false) {
return $default;
}
$decoded = json_decode($row['value'], true);
return $decoded !== null ? $decoded : $row['value'];
}
function db_set_config(string $key, $value): void
{
db()->prepare('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)')
->execute([$key, is_string($value) ? $value : json_encode($value)]);
}
function db_get_all_config(): array
{
$rows = db()->query('SELECT key, value FROM config')->fetchAll();
$result = [];
foreach ($rows as $row) {
$decoded = json_decode($row['value'], true);
$result[$row['key']] = ($decoded !== null) ? $decoded : $row['value'];
}
return $result;
}
// ── User helpers ──────────────────────────────────────────────────────────────
/** Find a user by username (always lower-cased). Returns DB row or null. */
function db_get_user(string $username): ?array
{
$stmt = db()->prepare('SELECT * FROM users WHERE username = ?');
$stmt->execute([strtolower($username)]);
$row = $stmt->fetch();
return $row !== false ? $row : null;
}
/** Return all user rows ordered by username. */
function db_get_all_users(): array
{
return db()->query('SELECT * FROM users ORDER BY username')->fetchAll();
}
/**
* Build the auth_data session array from a DB user row.
* Preserves the same format existing code expects:
* auth_data.permissions, auth_data.entreaulas.centro, .role, .aulas, .centros
*/
function db_build_auth_data(array $row): array
{
$permissions = json_decode($row['permissions'] ?? '[]', true) ?: [];
$meta = json_decode($row['meta'] ?? '{}', true) ?: [];
// Fetch all centro assignments for this user
$stmt = db()->prepare(
'SELECT centro_id, role, aulas
FROM user_centros
WHERE user_id = ?
ORDER BY centro_id'
);
$stmt->execute([$row['id']]);
$centro_rows = $stmt->fetchAll();
$ea = ['centro' => '', 'centros' => [], 'role' => '', 'aulas' => []];
if (!empty($centro_rows)) {
$first = $centro_rows[0];
$ea['centro'] = $first['centro_id']; // legacy compat
$ea['role'] = $first['role'];
$ea['aulas'] = json_decode($first['aulas'] ?? '[]', true) ?: [];
$ea['centros'] = array_column($centro_rows, 'centro_id');
$ea['centros_data'] = $centro_rows;
}
return array_merge($meta, [
'display_name' => $row['display_name'],
'email' => $row['email'],
'password_hash' => $row['password_hash'],
'permissions' => $permissions,
'entreaulas' => $ea,
'google_auth' => (bool) $row['google_auth'],
]);
}
/**
* Create or update a user.
* $data keys: username, display_name, email, password_hash, permissions[],
* google_auth, entreaulas{centro,centros[],role,aulas[]}, + any extra meta.
* Returns the user ID.
*/
function db_upsert_user(array $data): int
{
$pdo = db();
$username = strtolower((string) ($data['username'] ?? ''));
$existing = $pdo->prepare('SELECT id FROM users WHERE username = ?');
$existing->execute([$username]);
$existing_row = $existing->fetch();
$permissions = json_encode($data['permissions'] ?? []);
$meta_skip = ['username', 'display_name', 'email', 'password_hash',
'permissions', 'entreaulas', 'google_auth'];
$meta = [];
foreach ($data as $k => $v) {
if (!in_array($k, $meta_skip, true)) {
$meta[$k] = $v;
}
}
if ($existing_row) {
$user_id = (int) $existing_row['id'];
$upd = $pdo->prepare(
"UPDATE users SET
display_name = ?,
email = ?,
permissions = ?,
google_auth = ?,
meta = ?,
updated_at = datetime('now')
WHERE id = ?"
);
$upd->execute([
$data['display_name'] ?? '',
$data['email'] ?? '',
$permissions,
(int) ($data['google_auth'] ?? 0),
json_encode($meta),
$user_id,
]);
if (!empty($data['password_hash'])) {
$pdo->prepare('UPDATE users SET password_hash = ? WHERE id = ?')
->execute([$data['password_hash'], $user_id]);
}
} else {
$pdo->prepare(
'INSERT INTO users (username, display_name, email, password_hash, permissions, google_auth, meta)
VALUES (?, ?, ?, ?, ?, ?, ?)'
)->execute([
$username,
$data['display_name'] ?? '',
$data['email'] ?? '',
$data['password_hash'] ?? '',
$permissions,
(int) ($data['google_auth'] ?? 0),
json_encode($meta),
]);
$user_id = (int) $pdo->lastInsertId();
}
// Update centro assignments when entreaulas data is provided
if (array_key_exists('entreaulas', $data)) {
$ea = $data['entreaulas'] ?? [];
$pdo->prepare('DELETE FROM user_centros WHERE user_id = ?')->execute([$user_id]);
// Support both legacy single centro and new multi-centro
$centros = [];
if (!empty($ea['centros']) && is_array($ea['centros'])) {
$centros = $ea['centros'];
} elseif (!empty($ea['centro'])) {
$centros = [$ea['centro']];
}
$role = $ea['role'] ?? '';
$aulas = json_encode($ea['aulas'] ?? []);
$ins_centro = $pdo->prepare('INSERT OR IGNORE INTO centros (centro_id) VALUES (?)');
$ins_uc = $pdo->prepare(
'INSERT OR REPLACE INTO user_centros (user_id, centro_id, role, aulas) VALUES (?, ?, ?, ?)'
);
foreach ($centros as $cid) {
if ($cid === '') {
continue;
}
$ins_centro->execute([$cid]);
$ins_uc->execute([$user_id, $cid, $role, $aulas]);
}
}
return $user_id;
}
/** Delete a user and their centro assignments. */
function db_delete_user(string $username): void
{
db()->prepare('DELETE FROM users WHERE username = ?')->execute([strtolower($username)]);
}
// ── Centro helpers ────────────────────────────────────────────────────────────
function db_get_centros(): array
{
return db()->query('SELECT centro_id, name FROM centros ORDER BY centro_id')->fetchAll();
}
function db_get_centro_ids(): array
{
return db()->query('SELECT centro_id FROM centros ORDER BY centro_id')->fetchAll(PDO::FETCH_COLUMN);
}
// ── Aulario helpers ───────────────────────────────────────────────────────────
/** Get a single aulario config. Returns merged array (name, icon, + extra fields) or null. */
function db_get_aulario(string $centro_id, string $aulario_id): ?array
{
$stmt = db()->prepare(
'SELECT name, icon, extra FROM aularios WHERE centro_id = ? AND aulario_id = ?'
);
$stmt->execute([$centro_id, $aulario_id]);
$row = $stmt->fetch();
if ($row === false) {
return null;
}
$extra = json_decode($row['extra'] ?? '{}', true) ?: [];
return array_merge($extra, ['name' => $row['name'], 'icon' => $row['icon']]);
}
/** Get all aularios for a centro as aulario_id → config array. */
function db_get_aularios(string $centro_id): array
{
$stmt = db()->prepare(
'SELECT aulario_id, name, icon, extra FROM aularios WHERE centro_id = ? ORDER BY aulario_id'
);
$stmt->execute([$centro_id]);
$result = [];
foreach ($stmt->fetchAll() as $row) {
$extra = json_decode($row['extra'] ?? '{}', true) ?: [];
$result[$row['aulario_id']] = array_merge($extra, [
'name' => $row['name'],
'icon' => $row['icon'],
]);
}
return $result;
}
// ── SuperCafe helpers ─────────────────────────────────────────────────────────
function db_get_supercafe_menu(string $centro_id): array
{
$stmt = db()->prepare('SELECT data FROM supercafe_menu WHERE centro_id = ?');
$stmt->execute([$centro_id]);
$row = $stmt->fetch();
if ($row === false) {
return [];
}
return json_decode($row['data'], true) ?: [];
}
function db_set_supercafe_menu(string $centro_id, array $menu): void
{
db()->prepare('INSERT OR REPLACE INTO supercafe_menu (centro_id, data, updated_at) VALUES (?, ?, datetime(\'now\'))')
->execute([$centro_id, json_encode($menu, JSON_UNESCAPED_UNICODE)]);
}
/** Return all SC orders for a centro as an array of rows. */
function db_get_supercafe_orders(string $centro_id): array
{
$stmt = db()->prepare(
'SELECT * FROM supercafe_orders WHERE centro_id = ? ORDER BY created_at DESC'
);
$stmt->execute([$centro_id]);
return $stmt->fetchAll();
}
/** Return a single SC order by ref, or null. */
function db_get_supercafe_order(string $centro_id, string $order_ref): ?array
{
$stmt = db()->prepare(
'SELECT * FROM supercafe_orders WHERE centro_id = ? AND order_ref = ?'
);
$stmt->execute([$centro_id, $order_ref]);
$row = $stmt->fetch();
return $row !== false ? $row : null;
}
/** Create or update an SC order. */
function db_upsert_supercafe_order(
string $centro_id,
string $order_ref,
string $fecha,
string $persona,
string $comanda,
string $notas,
string $estado
): void {
db()->prepare(
'INSERT INTO supercafe_orders (centro_id, order_ref, fecha, persona, comanda, notas, estado)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(centro_id, order_ref) DO UPDATE SET
fecha = excluded.fecha,
persona = excluded.persona,
comanda = excluded.comanda,
notas = excluded.notas,
estado = excluded.estado'
)->execute([$centro_id, $order_ref, $fecha, $persona, $comanda, $notas, $estado]);
}
/** Generate the next order_ref for a centro (sc001, sc002, …). */
function db_next_supercafe_ref(string $centro_id): string
{
$stmt = db()->prepare(
"SELECT order_ref FROM supercafe_orders WHERE centro_id = ? ORDER BY id DESC LIMIT 1"
);
$stmt->execute([$centro_id]);
$last = $stmt->fetchColumn();
$n = 0;
if ($last && preg_match('/^sc(\d+)$/', $last, $m)) {
$n = (int) $m[1];
}
return 'sc' . str_pad($n + 1, 3, '0', STR_PAD_LEFT);
}
/** Count 'Deuda' orders for a persona in a centro. */
function db_supercafe_count_debts(string $centro_id, string $persona_key): int
{
$stmt = db()->prepare(
"SELECT COUNT(*) FROM supercafe_orders WHERE centro_id = ? AND persona = ? AND estado = 'Deuda'"
);
$stmt->execute([$centro_id, $persona_key]);
return (int) $stmt->fetchColumn();
}
// ── Comedor helpers ───────────────────────────────────────────────────────────
function db_get_comedor_menu_types(string $centro_id, string $aulario_id): array
{
$stmt = db()->prepare(
'SELECT data FROM comedor_menu_types WHERE centro_id = ? AND aulario_id = ?'
);
$stmt->execute([$centro_id, $aulario_id]);
$row = $stmt->fetch();
if ($row === false) {
return [];
}
return json_decode($row['data'], true) ?: [];
}
function db_set_comedor_menu_types(string $centro_id, string $aulario_id, array $types): void
{
db()->prepare(
'INSERT OR REPLACE INTO comedor_menu_types (centro_id, aulario_id, data) VALUES (?, ?, ?)'
)->execute([$centro_id, $aulario_id, json_encode($types, JSON_UNESCAPED_UNICODE)]);
}
function db_get_comedor_entry(string $centro_id, string $aulario_id, string $ym, string $day): array
{
$stmt = db()->prepare(
'SELECT data FROM comedor_entries WHERE centro_id = ? AND aulario_id = ? AND year_month = ? AND day = ?'
);
$stmt->execute([$centro_id, $aulario_id, $ym, $day]);
$row = $stmt->fetch();
if ($row === false) {
return [];
}
return json_decode($row['data'], true) ?: [];
}
function db_set_comedor_entry(string $centro_id, string $aulario_id, string $ym, string $day, array $data): void
{
db()->prepare(
'INSERT OR REPLACE INTO comedor_entries (centro_id, aulario_id, year_month, day, data) VALUES (?, ?, ?, ?, ?)'
)->execute([$centro_id, $aulario_id, $ym, $day, json_encode($data, JSON_UNESCAPED_UNICODE)]);
}
// ── Diario helpers ────────────────────────────────────────────────────────────
function db_get_diario_entry(string $centro_id, string $aulario_id, string $entry_date): array
{
$stmt = db()->prepare(
'SELECT data FROM diario_entries WHERE centro_id = ? AND aulario_id = ? AND entry_date = ?'
);
$stmt->execute([$centro_id, $aulario_id, $entry_date]);
$row = $stmt->fetch();
if ($row === false) {
return [];
}
return json_decode($row['data'], true) ?: [];
}
function db_set_diario_entry(string $centro_id, string $aulario_id, string $entry_date, array $data): void
{
db()->prepare(
'INSERT OR REPLACE INTO diario_entries (centro_id, aulario_id, entry_date, data) VALUES (?, ?, ?, ?)'
)->execute([$centro_id, $aulario_id, $entry_date, json_encode($data, JSON_UNESCAPED_UNICODE)]);
}
// ── Panel alumno helpers ──────────────────────────────────────────────────────
function db_get_panel_alumno(string $centro_id, string $aulario_id, string $alumno): array
{
$stmt = db()->prepare(
'SELECT data FROM panel_alumno WHERE centro_id = ? AND aulario_id = ? AND alumno = ?'
);
$stmt->execute([$centro_id, $aulario_id, $alumno]);
$row = $stmt->fetch();
if ($row === false) {
return [];
}
return json_decode($row['data'], true) ?: [];
}
function db_set_panel_alumno(string $centro_id, string $aulario_id, string $alumno, array $data): void
{
db()->prepare(
'INSERT OR REPLACE INTO panel_alumno (centro_id, aulario_id, alumno, data) VALUES (?, ?, ?, ?)'
)->execute([$centro_id, $aulario_id, $alumno, json_encode($data, JSON_UNESCAPED_UNICODE)]);
}
// ── Invitation helpers ────────────────────────────────────────────────────────
function db_get_all_invitations(): array
{
return db()->query('SELECT * FROM invitations ORDER BY code')->fetchAll();
}
function db_get_invitation(string $code): ?array
{
$stmt = db()->prepare('SELECT * FROM invitations WHERE code = ?');
$stmt->execute([strtoupper($code)]);
$row = $stmt->fetch();
return $row !== false ? $row : null;
}
function db_upsert_invitation(string $code, bool $active, bool $single_use): void
{
db()->prepare(
'INSERT OR REPLACE INTO invitations (code, active, single_use) VALUES (?, ?, ?)'
)->execute([strtoupper($code), (int) $active, (int) $single_use]);
}
function db_deactivate_invitation(string $code): void
{
db()->prepare('UPDATE invitations SET active = 0 WHERE code = ?')->execute([strtoupper($code)]);
}
function db_delete_invitation(string $code): void
{
db()->prepare('DELETE FROM invitations WHERE code = ?')->execute([strtoupper($code)]);
}
// ── Club helpers ──────────────────────────────────────────────────────────────
function db_get_club_config(): array
{
$stmt = db()->query('SELECT data FROM club_config WHERE id = 1');
$row = $stmt->fetch();
if ($row === false) {
return [];
}
return json_decode($row['data'], true) ?: [];
}
function db_set_club_config(array $config): void
{
db()->prepare('INSERT OR REPLACE INTO club_config (id, data) VALUES (1, ?)')
->execute([json_encode($config, JSON_UNESCAPED_UNICODE)]);
}
function db_get_all_club_events(): array
{
return db()->query('SELECT date_ref, data FROM club_events ORDER BY date_ref DESC')->fetchAll();
}
function db_get_club_event(string $date_ref): array
{
$stmt = db()->prepare('SELECT data FROM club_events WHERE date_ref = ?');
$stmt->execute([$date_ref]);
$row = $stmt->fetch();
if ($row === false) {
return [];
}
return json_decode($row['data'], true) ?: [];
}
function db_set_club_event(string $date_ref, array $data): void
{
db()->prepare('INSERT OR REPLACE INTO club_events (date_ref, data) VALUES (?, ?)')
->execute([$date_ref, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)]);
}
// ── Multi-tenant helpers ──────────────────────────────────────────────────────
/** Return all centro IDs the authenticated user belongs to. */
function get_user_centros(?array $auth_data = null): array
{
$data = $auth_data ?? $_SESSION['auth_data'] ?? [];
$ea = $data['entreaulas'] ?? [];
if (!empty($ea['centros']) && is_array($ea['centros'])) {
return array_values($ea['centros']);
}
if (!empty($ea['centro'])) {
return [$ea['centro']];
}
return [];
}
/** Ensure $_SESSION['active_centro'] is set to a valid centro. */
function init_active_centro(?array $auth_data = null): void
{
$centros = get_user_centros($auth_data);
if (empty($centros)) {
$_SESSION['active_centro'] = null;
return;
}
if (!empty($_SESSION['active_centro']) && in_array($_SESSION['active_centro'], $centros, true)) {
return;
}
$_SESSION['active_centro'] = $centros[0];
}

View File

@@ -0,0 +1,144 @@
-- Axia4 Migration 001: Initial Schema
-- Converts all JSON file-based storage to a proper relational schema.
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
-- ── Application configuration (replaces /DATA/AuthConfig.json) ─────────────
CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL DEFAULT ''
);
-- ── System installation flag (replaces /DATA/SISTEMA_INSTALADO.txt) ────────
-- Stored as a config row: key='installed', value='1'
-- ── Users (replaces /DATA/Usuarios/*.json) ─────────────────────────────────
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
display_name TEXT NOT NULL DEFAULT '',
email TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL DEFAULT '',
permissions TEXT NOT NULL DEFAULT '[]', -- JSON array
google_auth INTEGER NOT NULL DEFAULT 0,
meta TEXT NOT NULL DEFAULT '{}', -- JSON for extra fields
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- ── Invitations (replaces /DATA/Invitaciones_de_usuarios.json) ─────────────
CREATE TABLE IF NOT EXISTS invitations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
active INTEGER NOT NULL DEFAULT 1,
single_use INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- ── Centros/Organizations ───────────────────────────────────────────────────
-- Replaces directory existence at /DATA/entreaulas/Centros/{centro_id}/
CREATE TABLE IF NOT EXISTS centros (
id INTEGER PRIMARY KEY AUTOINCREMENT,
centro_id TEXT UNIQUE NOT NULL,
name TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- ── User ↔ Centro assignments (many-to-many) ───────────────────────────────
-- Replaces entreaulas.centro + entreaulas.aulas fields in user JSON.
-- A single user can belong to multiple centros (multi-tenant).
CREATE TABLE IF NOT EXISTS user_centros (
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
centro_id TEXT NOT NULL REFERENCES centros(centro_id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT '',
aulas TEXT NOT NULL DEFAULT '[]', -- JSON array of aulario_ids
PRIMARY KEY (user_id, centro_id)
);
-- ── Aularios (replaces /DATA/entreaulas/Centros/{}/Aularios/{id}.json) ──────
CREATE TABLE IF NOT EXISTS aularios (
id INTEGER PRIMARY KEY AUTOINCREMENT,
centro_id TEXT NOT NULL REFERENCES centros(centro_id) ON DELETE CASCADE,
aulario_id TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
icon TEXT NOT NULL DEFAULT '',
extra TEXT NOT NULL DEFAULT '{}', -- JSON for extra config
UNIQUE (centro_id, aulario_id)
);
-- ── SuperCafe menu (replaces .../SuperCafe/Menu.json) ──────────────────────
CREATE TABLE IF NOT EXISTS supercafe_menu (
id INTEGER PRIMARY KEY AUTOINCREMENT,
centro_id TEXT NOT NULL REFERENCES centros(centro_id) ON DELETE CASCADE,
data TEXT NOT NULL DEFAULT '{}', -- JSON matching existing format
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (centro_id)
);
-- ── SuperCafe orders (replaces .../SuperCafe/Comandas/*.json) ───────────────
CREATE TABLE IF NOT EXISTS supercafe_orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
centro_id TEXT NOT NULL REFERENCES centros(centro_id) ON DELETE CASCADE,
order_ref TEXT NOT NULL,
fecha TEXT NOT NULL,
persona TEXT NOT NULL,
comanda TEXT NOT NULL DEFAULT '',
notas TEXT NOT NULL DEFAULT '',
estado TEXT NOT NULL DEFAULT 'Pedido',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (centro_id, order_ref)
);
-- ── Comedor menu types (replaces .../Comedor-MenuTypes.json) ────────────────
CREATE TABLE IF NOT EXISTS comedor_menu_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
centro_id TEXT NOT NULL REFERENCES centros(centro_id) ON DELETE CASCADE,
aulario_id TEXT NOT NULL,
data TEXT NOT NULL DEFAULT '[]', -- JSON array of menu type objs
UNIQUE (centro_id, aulario_id)
);
-- ── Comedor daily entries (replaces .../Comedor/{ym}/{day}/_datos.json) ─────
CREATE TABLE IF NOT EXISTS comedor_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
centro_id TEXT NOT NULL REFERENCES centros(centro_id) ON DELETE CASCADE,
aulario_id TEXT NOT NULL,
year_month TEXT NOT NULL, -- "2024-01"
day TEXT NOT NULL, -- "15"
data TEXT NOT NULL DEFAULT '{}',
UNIQUE (centro_id, aulario_id, year_month, day)
);
-- ── Diary entries (replaces .../Diario/*.json) ──────────────────────────────
CREATE TABLE IF NOT EXISTS diario_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
centro_id TEXT NOT NULL REFERENCES centros(centro_id) ON DELETE CASCADE,
aulario_id TEXT NOT NULL,
entry_date TEXT NOT NULL,
data TEXT NOT NULL DEFAULT '{}',
UNIQUE (centro_id, aulario_id, entry_date)
);
-- ── Panel diario per-student data (replaces .../Alumnos/*/Panel.json) ───────
CREATE TABLE IF NOT EXISTS panel_alumno (
id INTEGER PRIMARY KEY AUTOINCREMENT,
centro_id TEXT NOT NULL REFERENCES centros(centro_id) ON DELETE CASCADE,
aulario_id TEXT NOT NULL,
alumno TEXT NOT NULL,
data TEXT NOT NULL DEFAULT '{}',
UNIQUE (centro_id, aulario_id, alumno)
);
-- ── Club event metadata (replaces /DATA/club/IMG/{date}/data.json) ──────────
CREATE TABLE IF NOT EXISTS club_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date_ref TEXT UNIQUE NOT NULL,
data TEXT NOT NULL DEFAULT '{}'
);
-- ── Club configuration (replaces /DATA/club/config.json) ────────────────────
CREATE TABLE IF NOT EXISTS club_config (
id INTEGER PRIMARY KEY CHECK (id = 1),
data TEXT NOT NULL DEFAULT '{}'
);

View File

@@ -0,0 +1,255 @@
<?php
/**
* Migration 002: Import existing JSON data from the filesystem into the DB.
* This runs once on first boot after the schema is created.
* It is safe to run even if /DATA doesn't have all files missing files are skipped.
*
* $db (PDO) is provided by the migration runner in db.php.
*/
// ── AuthConfig → config table ────────────────────────────────────────────────
$auth_config_file = '/DATA/AuthConfig.json';
if (file_exists($auth_config_file)) {
$auth_config = json_decode(file_get_contents($auth_config_file), true) ?? [];
$ins = $db->prepare("INSERT OR IGNORE INTO config (key, value) VALUES (?, ?)");
foreach ($auth_config as $k => $v) {
$ins->execute([$k, is_string($v) ? $v : json_encode($v)]);
}
}
// ── SISTEMA_INSTALADO marker ─────────────────────────────────────────────────
if (file_exists('/DATA/SISTEMA_INSTALADO.txt')) {
$db->prepare("INSERT OR IGNORE INTO config (key, value) VALUES ('installed', '1')")->execute();
}
// ── Users (/DATA/Usuarios/*.json) ────────────────────────────────────────────
$users_dir = '/DATA/Usuarios';
if (is_dir($users_dir)) {
$ins_user = $db->prepare(
"INSERT OR IGNORE INTO users
(username, display_name, email, password_hash, permissions, google_auth, meta)
VALUES (?, ?, ?, ?, ?, ?, ?)"
);
$ins_uc = $db->prepare(
"INSERT OR IGNORE INTO user_centros (user_id, centro_id, role, aulas)
VALUES (?, ?, ?, ?)"
);
$ins_centro = $db->prepare("INSERT OR IGNORE INTO centros (centro_id) VALUES (?)");
foreach (glob("$users_dir/*.json") ?: [] as $user_file) {
$username = basename($user_file, '.json');
$data = json_decode(file_get_contents($user_file), true);
if (!is_array($data)) {
continue;
}
$permissions = isset($data['permissions']) ? json_encode($data['permissions']) : '[]';
// Store remaining non-standard keys in meta
$meta_keys = ['display_name', 'email', 'password_hash', 'permissions', 'entreaulas', 'google_auth'];
$meta = [];
foreach ($data as $k => $v) {
if (!in_array($k, $meta_keys, true)) {
$meta[$k] = $v;
}
}
$ins_user->execute([
$username,
$data['display_name'] ?? '',
$data['email'] ?? '',
$data['password_hash'] ?? '',
$permissions,
(int) ($data['google_auth'] ?? 0),
json_encode($meta),
]);
$user_id = (int) $db->lastInsertId();
if ($user_id === 0) {
// Already existed look it up
$row = $db->prepare("SELECT id FROM users WHERE username = ?")->execute([$username]);
$user_id = (int) $db->query("SELECT id FROM users WHERE username = " . $db->quote($username))->fetchColumn();
}
// Entreaulas centro assignment
$ea = $data['entreaulas'] ?? [];
// Support both old single "centro" and new "centros" array
$centros = [];
if (!empty($ea['centros']) && is_array($ea['centros'])) {
$centros = $ea['centros'];
} elseif (!empty($ea['centro'])) {
$centros = [$ea['centro']];
}
$role = $ea['role'] ?? '';
$aulas = json_encode($ea['aulas'] ?? []);
foreach ($centros as $cid) {
if ($cid === '') {
continue;
}
$ins_centro->execute([$cid]);
$ins_uc->execute([$user_id, $cid, $role, $aulas]);
}
}
}
// ── Invitations (/DATA/Invitaciones_de_usuarios.json) ────────────────────────
$inv_file = '/DATA/Invitaciones_de_usuarios.json';
if (file_exists($inv_file)) {
$invs = json_decode(file_get_contents($inv_file), true) ?? [];
$ins = $db->prepare(
"INSERT OR IGNORE INTO invitations (code, active, single_use) VALUES (?, ?, ?)"
);
foreach ($invs as $code => $inv) {
$ins->execute([
strtoupper($code),
(int) ($inv['active'] ?? 1),
(int) ($inv['single_use'] ?? 1),
]);
}
}
// ── Centros & Aularios (directory structure) ──────────────────────────────────
$centros_base = '/DATA/entreaulas/Centros';
if (is_dir($centros_base)) {
$ins_centro = $db->prepare("INSERT OR IGNORE INTO centros (centro_id) VALUES (?)");
$ins_aulario = $db->prepare(
"INSERT OR IGNORE INTO aularios (centro_id, aulario_id, name, icon, extra) VALUES (?, ?, ?, ?, ?)"
);
foreach (glob("$centros_base/*", GLOB_ONLYDIR) ?: [] as $centro_dir) {
$centro_id = basename($centro_dir);
$ins_centro->execute([$centro_id]);
$aularios_dir = "$centro_dir/Aularios";
foreach (glob("$aularios_dir/*.json") ?: [] as $aulario_file) {
$aulario_id = basename($aulario_file, '.json');
$adata = json_decode(file_get_contents($aulario_file), true);
if (!is_array($adata)) {
continue;
}
$name = $adata['name'] ?? $aulario_id;
$icon = $adata['icon'] ?? '';
$extra_keys = ['name', 'icon'];
$extra = [];
foreach ($adata as $k => $v) {
if (!in_array($k, $extra_keys, true)) {
$extra[$k] = $v;
}
}
$ins_aulario->execute([$centro_id, $aulario_id, $name, $icon, json_encode($extra)]);
}
// SuperCafe menu
$menu_file = "$centro_dir/SuperCafe/Menu.json";
if (file_exists($menu_file)) {
$menu_data = file_get_contents($menu_file);
$db->prepare("INSERT OR IGNORE INTO supercafe_menu (centro_id, data) VALUES (?, ?)")
->execute([$centro_id, $menu_data]);
}
// SuperCafe orders
$comandas_dir = "$centro_dir/SuperCafe/Comandas";
if (is_dir($comandas_dir)) {
$ins_order = $db->prepare(
"INSERT OR IGNORE INTO supercafe_orders
(centro_id, order_ref, fecha, persona, comanda, notas, estado)
VALUES (?, ?, ?, ?, ?, ?, ?)"
);
foreach (glob("$comandas_dir/*.json") ?: [] as $order_file) {
$order_ref = basename($order_file, '.json');
$odata = json_decode(file_get_contents($order_file), true);
if (!is_array($odata)) {
continue;
}
$ins_order->execute([
$centro_id,
$order_ref,
$odata['Fecha'] ?? '',
$odata['Persona'] ?? '',
$odata['Comanda'] ?? '',
$odata['Notas'] ?? '',
$odata['Estado'] ?? 'Pedido',
]);
}
}
// Comedor menu types & daily entries per aulario
foreach (glob("$aularios_dir/*.json") ?: [] as $aulario_file) {
$aulario_id = basename($aulario_file, '.json');
$menu_types_file = "$aularios_dir/$aulario_id/Comedor-MenuTypes.json";
if (file_exists($menu_types_file)) {
$db->prepare(
"INSERT OR IGNORE INTO comedor_menu_types (centro_id, aulario_id, data) VALUES (?, ?, ?)"
)->execute([$centro_id, $aulario_id, file_get_contents($menu_types_file)]);
}
$comedor_base = "$aularios_dir/$aulario_id/Comedor";
if (is_dir($comedor_base)) {
$ins_centry = $db->prepare(
"INSERT OR IGNORE INTO comedor_entries (centro_id, aulario_id, year_month, day, data) VALUES (?, ?, ?, ?, ?)"
);
foreach (glob("$comedor_base/*", GLOB_ONLYDIR) ?: [] as $ym_dir) {
$ym = basename($ym_dir);
foreach (glob("$ym_dir/*", GLOB_ONLYDIR) ?: [] as $day_dir) {
$day = basename($day_dir);
$data_file = "$day_dir/_datos.json";
if (file_exists($data_file)) {
$ins_centry->execute([
$centro_id, $aulario_id, $ym, $day,
file_get_contents($data_file),
]);
}
}
}
}
// Diario entries
$diario_base = "$aularios_dir/$aulario_id/Diario";
if (is_dir($diario_base)) {
$ins_d = $db->prepare(
"INSERT OR IGNORE INTO diario_entries (centro_id, aulario_id, entry_date, data) VALUES (?, ?, ?, ?)"
);
foreach (glob("$diario_base/*.json") ?: [] as $diario_file) {
$entry_date = basename($diario_file, '.json');
$ins_d->execute([$centro_id, $aulario_id, $entry_date, file_get_contents($diario_file)]);
}
}
// Panel alumno data
$alumnos_base = "$aularios_dir/$aulario_id/Alumnos";
if (is_dir($alumnos_base)) {
$ins_pa = $db->prepare(
"INSERT OR IGNORE INTO panel_alumno (centro_id, aulario_id, alumno, data) VALUES (?, ?, ?, ?)"
);
foreach (glob("$alumnos_base/*/", GLOB_ONLYDIR) ?: [] as $alumno_dir) {
$alumno = basename($alumno_dir);
// Look for Panel.json (used by paneldiario)
$panel_files = glob("$alumno_dir/Panel*.json") ?: [];
foreach ($panel_files as $pf) {
$ins_pa->execute([
$centro_id, $aulario_id, $alumno,
file_get_contents($pf),
]);
}
}
}
}
}
}
// ── Club config (/DATA/club/config.json) ──────────────────────────────────────
$club_config_file = '/DATA/club/config.json';
if (file_exists($club_config_file)) {
$db->prepare("INSERT OR IGNORE INTO club_config (id, data) VALUES (1, ?)")
->execute([file_get_contents($club_config_file)]);
}
// ── Club events (/DATA/club/IMG/{date}/data.json) ─────────────────────────────
$club_img_dir = '/DATA/club/IMG';
if (is_dir($club_img_dir)) {
$ins_ev = $db->prepare("INSERT OR IGNORE INTO club_events (date_ref, data) VALUES (?, ?)");
foreach (glob("$club_img_dir/*/", GLOB_ONLYDIR) ?: [] as $event_dir) {
$date_ref = basename($event_dir);
$event_data_file = "$event_dir/data.json";
$ins_ev->execute([
$date_ref,
file_exists($event_data_file) ? file_get_contents($event_data_file) : '{}',
]);
}
}

View File

@@ -27,7 +27,11 @@ if (!empty($displayName)) {
$initials = mb_strtoupper($first . $last);
}
?>
// Tenant (centro) management
$userCentros = get_user_centros($_SESSION["auth_data"] ?? []);
$activeCentro = $_SESSION['active_centro'] ?? ($_SESSION["auth_data"]["entreaulas"]["centro"] ?? '');
<!DOCTYPE html>
<html lang="es">
@@ -526,6 +530,28 @@ if (!empty($displayName)) {
<div class="account-name"><?php echo htmlspecialchars($displayName); ?></div>
<div class="account-email"><?php echo htmlspecialchars($email); ?></div>
</div>
<?php if (!empty($userCentros) && $_SESSION["auth_ok"]): ?>
<div style="padding: 8px 16px; border-top: 1px solid #e0e0e0;">
<div style="font-size:.75rem;font-weight:600;color:#5f6368;text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px;">
Organización activa
</div>
<div style="font-size:.9rem;font-weight:600;color:#1a73e8;margin-bottom:<?= count($userCentros) > 1 ? '8px' : '0' ?>;">
<?= htmlspecialchars($activeCentro ?: '') ?>
</div>
<?php if (count($userCentros) > 1): ?>
<div style="font-size:.75rem;color:#5f6368;margin-bottom:4px;">Cambiar organización:</div>
<?php foreach ($userCentros as $cid): if ($cid === $activeCentro) continue; ?>
<form method="post" action="/_incl/switch_tenant.php" style="margin:0 0 4px;">
<input type="hidden" name="redir" value="<?= htmlspecialchars($_SERVER['REQUEST_URI'] ?? '/') ?>">
<button type="submit" name="centro" value="<?= htmlspecialchars($cid) ?>"
style="display:block;width:100%;text-align:left;padding:5px 8px;border:1px solid #e0e0e0;border-radius:6px;background:#f8f9fa;font-size:.85rem;cursor:pointer;">
<?= htmlspecialchars($cid) ?>
</button>
</form>
<?php endforeach; ?>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="account-actions">
<?php if ($_SESSION["auth_ok"]) { ?>
<a href="/account/" class="btn btn-outline-secondary w-100">Gestionar cuenta</a>

View File

@@ -0,0 +1,28 @@
<?php
/**
* switch_tenant.php
* POST endpoint to switch the active tenant/centro for the current user session.
* Validates the requested centro against the user's allowed centros before applying.
*/
require_once "tools.session.php";
require_once "tools.security.php";
require_once "db.php";
if (!isset($_SESSION["auth_ok"]) || $_SESSION["auth_ok"] !== true) {
header("HTTP/1.1 401 Unauthorized");
die("No autenticado.");
}
$requested = Sf($_POST['centro'] ?? '');
$redir = safe_redir($_POST['redir'] ?? '/');
$centros = get_user_centros($_SESSION['auth_data'] ?? []);
if ($requested !== '' && in_array($requested, $centros, true)) {
$_SESSION['active_centro'] = $requested;
// Also update session auth_data so it reflects immediately
$_SESSION['auth_data']['entreaulas']['centro'] = $requested;
}
header("Location: $redir");
exit;

View File

@@ -1,124 +1,80 @@
<?php
require_once "tools.session.php";
require_once "tools.security.php";
require_once __DIR__ . "/db.php";
// Load auth config from DB (replaces /DATA/AuthConfig.json)
if (!isset($AuthConfig)) {
$AuthConfig = json_decode(file_get_contents("/DATA/AuthConfig.json"), true);
$AuthConfig = db_get_all_config();
}
$ua = $_SERVER['HTTP_USER_AGENT'];
// ── Header-based auth (Axia4Auth/{user}/{pass}) ───────────────────────────────
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
if (str_starts_with($ua, "Axia4Auth/")) {
$username = explode("/", $ua)[1];
$userpass = explode("/", $ua)[2];
$user_filename = safe_username_to_filename($username);
if ($user_filename === "") {
$parts = explode("/", $ua);
$username = $parts[1] ?? '';
$userpass = $parts[2] ?? '';
$row = db_get_user($username);
if (!$row || !password_verify($userpass, $row['password_hash'])) {
header("HTTP/1.1 403 Forbidden");
die();
}
$userdata = json_decode(file_get_contents("/DATA/Usuarios/" . $user_filename . ".json"), true);
if (!$userdata) {
header("HTTP/1.1 403 Forbidden");
die();
}
if (!password_verify($userpass, $userdata["password_hash"])) {
header("HTTP/1.1 403 Forbidden");
die();
}
$_SESSION["auth_user"] = $username;
$_SESSION["auth_data"] = $userdata;
$_SESSION["auth_ok"] = true;
$_COOKIE["auth_user"] = $username;
$_COOKIE["auth_pass_b64"] = base64_encode($userpass);
$_SESSION["auth_external_lock"] = "header"; // Cannot logout because auth is done via header
$_SESSION["auth_user"] = $username;
$_SESSION["auth_data"] = db_build_auth_data($row);
$_SESSION["auth_ok"] = true;
$_COOKIE["auth_user"] = $username;
$_COOKIE["auth_pass_b64"] = base64_encode($userpass);
$_SESSION["auth_external_lock"] = "header";
init_active_centro($_SESSION["auth_data"]);
}
// If $_SESSION is empty, check for cookies "auth_user" and "auth_pass_b64"
if ($_SESSION["auth_ok"] != true && isset($_COOKIE["auth_user"]) && isset($_COOKIE["auth_pass_b64"])) {
// ── Cookie-based auto-login ───────────────────────────────────────────────────
if (($_SESSION["auth_ok"] ?? false) != true
&& isset($_COOKIE["auth_user"], $_COOKIE["auth_pass_b64"])
) {
$username = $_COOKIE["auth_user"];
$userpass_b64 = $_COOKIE["auth_pass_b64"];
$userpass = base64_decode($userpass_b64);
$user_filename = safe_username_to_filename($username);
if ($user_filename !== "") {
$userdata = json_decode(file_get_contents("/DATA/Usuarios/" . $user_filename . ".json"), true);
if ($userdata && password_verify($userpass, $userdata["password_hash"])) {
$_SESSION["auth_user"] = $username;
$_SESSION["auth_data"] = $userdata;
$_SESSION["auth_ok"] = true;
}
$userpass = base64_decode($_COOKIE["auth_pass_b64"]);
$row = db_get_user($username);
if ($row && password_verify($userpass, $row['password_hash'])) {
$_SESSION["auth_user"] = $username;
$_SESSION["auth_data"] = db_build_auth_data($row);
$_SESSION["auth_ok"] = true;
init_active_centro($_SESSION["auth_data"]);
}
}
// If session is older than 5min, reload user data
if (isset($_SESSION["auth_ok"]) && $_SESSION["auth_ok"] && isset($_SESSION["auth_user"])) {
if (isset($AuthConfig["session_load_mode"]) && $AuthConfig["session_load_mode"] === "force") {
$username = $_SESSION["auth_user"];
$user_filename = safe_username_to_filename($username);
if ($user_filename !== "") {
$userdata = json_decode(file_get_contents("/DATA/Usuarios/" . $user_filename . ".json"), true);
$_SESSION["auth_data"] = $userdata;
// ── Periodic session reload from DB ──────────────────────────────────────────
if (!empty($_SESSION["auth_ok"]) && !empty($_SESSION["auth_user"])) {
$load_mode = $AuthConfig["session_load_mode"] ?? '';
if ($load_mode === "force") {
$row = db_get_user($_SESSION["auth_user"]);
if ($row) {
$_SESSION["auth_data"] = db_build_auth_data($row);
init_active_centro($_SESSION["auth_data"]);
}
$_SESSION["last_reload_time"] = time();
} elseif (isset($AuthConfig["session_load_mode"]) && $AuthConfig["session_load_mode"] === "never") {
// Do nothing, never reload session data
} else {
if (isset($_SESSION["last_reload_time"])) {
$last_reload = $_SESSION["last_reload_time"];
if (time() - $last_reload > 300) {
$username = $_SESSION["auth_user"];
$user_filename = safe_username_to_filename($username);
if ($user_filename !== "") {
$userdata = json_decode(file_get_contents("/DATA/Usuarios/" . $user_filename . ".json"), true);
$_SESSION["auth_data"] = $userdata;
}
$_SESSION["last_reload_time"] = time();
} elseif ($load_mode !== "never") {
$last = $_SESSION["last_reload_time"] ?? 0;
if (time() - $last > 300) {
$row = db_get_user($_SESSION["auth_user"]);
if ($row) {
$_SESSION["auth_data"] = db_build_auth_data($row);
init_active_centro($_SESSION["auth_data"]);
}
} else {
$_SESSION["last_reload_time"] = time();
}
if (!isset($_SESSION["last_reload_time"])) {
$_SESSION["last_reload_time"] = time();
}
}
}
function user_is_authenticated()
function user_is_authenticated(): bool
{
return isset($_SESSION["auth_ok"]) && $_SESSION["auth_ok"] === true;
}
function user_has_permission($perm)
{
return in_array($perm, $_SESSION["auth_data"]["permissions"] ?? []);
}
/**
* Returns all centro/tenant IDs the authenticated user belongs to.
* Supports both the legacy single-centro format (entreaulas.centro = "string")
* and the new multi-tenant format (entreaulas.centros = ["a", "b"]).
*/
function get_user_centros($auth_data = null)
function user_has_permission(string $perm): bool
{
$data = $auth_data ?? $_SESSION["auth_data"] ?? [];
$ea = $data["entreaulas"] ?? [];
if (!empty($ea["centros"]) && is_array($ea["centros"])) {
return array_values($ea["centros"]);
}
if (!empty($ea["centro"])) {
return [$ea["centro"]];
}
return [];
}
/**
* Ensures $_SESSION['active_centro'] is set to a valid centro for the user.
* Call after user data is loaded/reloaded.
*/
function init_active_centro($auth_data = null)
{
$centros = get_user_centros($auth_data);
if (empty($centros)) {
$_SESSION['active_centro'] = null;
return;
}
// Keep existing selection only if it is still valid
if (!empty($_SESSION['active_centro']) && in_array($_SESSION['active_centro'], $centros, true)) {
return;
}
$_SESSION['active_centro'] = $centros[0];
return in_array($perm, $_SESSION["auth_data"]["permissions"] ?? [], true);
}

View File

@@ -1,28 +1,25 @@
<?php
if (file_exists("/DATA/SISTEMA_INSTALADO.txt")) {
require_once "_incl/db.php";
if (db_get_config('installed') === '1') {
header("Location: /");
die();
}
switch ($_GET['form'] ?? '') {
case 'create_admin':
$admin_user = trim(strtolower($_POST['admin_user'] ?? ''));
$admin_user = trim(strtolower($_POST['admin_user'] ?? ''));
$admin_password = $_POST['admin_password'] ?? '';
if (empty($admin_user) || empty($admin_password)) {
die("El nombre de usuario y la contraseña son obligatorios.");
}
$password_hash = password_hash($admin_password, PASSWORD_DEFAULT);
$admin_userdata = [
'display_name' => 'Administrador',
'email' => "$admin_user@nomail.arpa",
'permissions' => ['*', 'sysadmin:access', 'entreaulas:access'],
'password_hash' => $password_hash
];
if (!is_dir("/DATA/Usuarios")) {
mkdir("/DATA/Usuarios", 0777, true);
}
file_put_contents("/DATA/Usuarios/$admin_user.json", json_encode($admin_userdata, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
file_put_contents("/DATA/SISTEMA_INSTALADO.txt", "Sistema instalado el ".date("Y-m-d H:i:s")."\n");
db_upsert_user([
'username' => $admin_user,
'display_name' => 'Administrador',
'email' => "$admin_user@nomail.arpa",
'permissions' => ['*', 'sysadmin:access', 'entreaulas:access'],
'password_hash' => password_hash($admin_password, PASSWORD_DEFAULT),
]);
db_set_config('installed', '1');
header("Location: /_login.php");
exit;
break;

View File

@@ -1,8 +1,9 @@
<?php
require_once "_incl/tools.session.php";
require_once "_incl/tools.security.php";
require_once "_incl/db.php";
if (!isset($AuthConfig)) {
$AuthConfig = json_decode(file_get_contents("/DATA/AuthConfig.json"), true);
$AuthConfig = db_get_all_config();
}
$DOMAIN = $_SERVER["HTTP_X_FORWARDED_HOST"] ?? $_SERVER["HTTP_HOST"];
@@ -21,13 +22,13 @@ function safe_redir($url) {
}
if ($_GET["reload_user"] == "1") {
$user_filename = safe_username_to_filename($_SESSION["auth_user"] ?? "");
if ($user_filename === "") {
$row = db_get_user($_SESSION["auth_user"] ?? "");
if (!$row) {
header("Location: /");
die();
}
$userdata = json_decode(file_get_contents("/DATA/Usuarios/" . $user_filename . ".json"), true);
$_SESSION['auth_data'] = $userdata;
$_SESSION['auth_data'] = db_build_auth_data($row);
init_active_centro($_SESSION['auth_data']);
$redir = safe_redir($_GET["redir"] ?? "/");
header("Location: $redir");
die();
@@ -83,34 +84,36 @@ if ($_GET["google_callback"] == "1") {
}
$email = $user_info["email"];
$name = $user_info["name"] ?? explode("@", $email)[0];
$user_filename = safe_username_to_filename($email);
if ($user_filename === "") {
$name = $user_info["name"] ?? explode("@", $email)[0];
$username = strtolower($email);
if ($username === "") {
die("Error: Dirección de correo inválida.");
}
$userfile = "/DATA/Usuarios/" . $user_filename . ".json";
$password = bin2hex(random_bytes(16)); // Generar una contraseña aleatoria para el usuario, aunque no se usará para iniciar sesión
if (file_exists($userfile)) {
$userdata = json_decode(file_get_contents($userfile), true);
$password = bin2hex(random_bytes(16));
$existing = db_get_user($username);
if ($existing) {
$user_row = $existing;
} else {
$userdata = [
"display_name" => $name,
"email" => $email,
"permissions" => ["public"],
"password_hash" => password_hash($password, PASSWORD_DEFAULT),
"google_auth" => true,
"#" => "Este usuario fue creado automáticamente al iniciar sesión con Google por primera vez.",
];
file_put_contents($userfile, json_encode($userdata));
db_upsert_user([
'username' => $username,
'display_name' => $name,
'email' => $email,
'permissions' => ['public'],
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
'google_auth' => true,
'#' => 'Este usuario fue creado automáticamente al iniciar sesión con Google por primera vez.',
]);
$user_row = db_get_user($username);
}
session_regenerate_id(true);
$_SESSION['auth_user'] = $email;
$_SESSION['auth_data'] = $userdata;
$_SESSION['auth_ok'] = true;
$_SESSION['auth_user'] = $username;
$_SESSION['auth_data'] = db_build_auth_data($user_row);
$_SESSION['auth_ok'] = true;
init_active_centro($_SESSION['auth_data']);
$cookie_options = ["expires" => time() + (86400 * 30), "path" => "/", "httponly" => true, "secure" => true, "samesite" => "Lax"];
setcookie("auth_user", $email, $cookie_options);
setcookie("auth_pass_b64", base64_encode($password), $cookie_options);
setcookie("auth_user", $username, $cookie_options);
setcookie("auth_pass_b64", base64_encode($password), $cookie_options);
$redir = safe_redir($state["redir"] ?? "/");
@@ -161,20 +164,19 @@ if ($_GET["clear_session"] == "1") {
die();
}
if (isset($_POST["user"])) {
$valid = "";
$user = trim(strtolower($_POST["user"]));
$user = trim(strtolower($_POST["user"]));
$password = $_POST["password"];
$user_filename = safe_username_to_filename($user);
$userdata = ($user_filename !== "") ? json_decode(@file_get_contents("/DATA/Usuarios/" . $user_filename . ".json"), true) : null;
if (!is_array($userdata) || !isset($userdata["password_hash"])) {
$row = db_get_user($user);
if (!$row || !isset($row["password_hash"])) {
$_GET["_result"] = "El usuario no existe.";
} elseif (password_verify($password, $userdata["password_hash"])) {
} elseif (password_verify($password, $row["password_hash"])) {
session_regenerate_id(true);
$_SESSION['auth_user'] = $user;
$_SESSION['auth_data'] = $userdata;
$_SESSION['auth_ok'] = true;
$_SESSION['auth_data'] = db_build_auth_data($row);
$_SESSION['auth_ok'] = true;
init_active_centro($_SESSION['auth_data']);
$cookie_options = ["expires" => time() + (86400 * 30), "path" => "/", "httponly" => true, "secure" => true, "samesite" => "Lax"];
setcookie("auth_user", $user, $cookie_options);
setcookie("auth_user", $user, $cookie_options);
setcookie("auth_pass_b64", base64_encode($password), $cookie_options);
$redir = safe_redir($_GET["redir"] ?? "/");
header("Location: $redir");
@@ -182,9 +184,8 @@ if (isset($_POST["user"])) {
} else {
$_GET["_result"] = "La contraseña no es correcta.";
}
}
if (!file_exists("/DATA/SISTEMA_INSTALADO.txt")) {
if (db_get_config('installed') !== '1') {
header("Location: /_install.php");
die();
}

View File

@@ -1,44 +1,131 @@
<?php
require_once "_incl/auth_redir.php";
require_once "../_incl/db.php";
require_once "_incl/pre-body.php";
?>
<div id="grid">
<div class="card pad grid-item" style="text-align: center;">
<h2>¡Hola, <?php echo htmlspecialchars($_SESSION["auth_data"]["display_name"]); ?>!</h2>
<span><b>Tu Email:</b> <?php echo htmlspecialchars($_SESSION["auth_data"]["email"]); ?></span>
<span><b>Tu Nombre de Usuario:</b> <?php echo htmlspecialchars($_SESSION["auth_user"]); ?></span>
</div>
<div class="card pad grid-item" style="text-align: center;">
<b>Código QR</b>
<img src="https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=<?php echo urlencode($_SESSION["auth_user"]); ?>" alt="QR Code de Nombre de Usuario" style="margin: 0 auto;">
<small>Escanea este código para iniciar sesión. Es como tu contraseña, pero más fácil.</small>
</div>
</div>
<style>
.grid-item {
margin-bottom: 10px !important;
padding: 15px;
width: 300px;
text-align: center;
}
.grid-item img {
margin: 0 auto;
height: 150px;
}
$authData = $_SESSION["auth_data"] ?? [];
$username = $_SESSION["auth_user"] ?? '';
$displayName = $authData["display_name"] ?? 'Invitado';
$email = $authData["email"] ?? '';
$permissions = $authData["permissions"] ?? [];
// Tenant / centro management
$userCentros = get_user_centros($authData);
$activeCentro = $_SESSION['active_centro'] ?? ($authData['entreaulas']['centro'] ?? '');
$aularios = ($activeCentro !== '') ? db_get_aularios($activeCentro) : [];
$userAulas = $authData['entreaulas']['aulas'] ?? [];
$role = $authData['entreaulas']['role'] ?? '';
// Initials for avatar
$parts = preg_split('/\s+/', trim($displayName));
$initials = mb_strtoupper(mb_substr($parts[0] ?? '', 0, 1) . mb_substr($parts[1] ?? '', 0, 1));
if ($initials === '') {
$initials = '?';
}
?>
<style>
.account-grid { display: flex; flex-wrap: wrap; gap: 16px; padding: 16px; }
.account-card { background: #fff; border: 1px solid #e0e0e0; border-radius: 12px; padding: 24px; min-width: 280px; flex: 1 1 280px; }
.account-card h2 { font-size: 1rem; font-weight: 600; color: var(--gw-text-secondary, #5f6368); text-transform: uppercase; letter-spacing: .05em; margin: 0 0 16px; }
.avatar-lg { width: 80px; height: 80px; border-radius: 50%; background: var(--gw-blue, #1a73e8); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 2rem; font-weight: 700; margin: 0 auto 12px; }
.info-row { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #f1f3f4; font-size: .9rem; }
.info-row:last-child { border-bottom: none; }
.info-row .label { color: var(--gw-text-secondary, #5f6368); }
.badge-pill { display: inline-block; padding: 2px 10px; border-radius: 99px; font-size: .78rem; font-weight: 600; margin: 2px; }
.badge-active { background: #e6f4ea; color: #137333; }
.badge-perm { background: #e8f0fe; color: #1a73e8; }
.tenant-btn { display: block; width: 100%; text-align: left; padding: 8px 12px; border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 8px; background: #f8f9fa; cursor: pointer; font-size: .9rem; transition: background .15s; }
.tenant-btn:hover { background: #e8f0fe; }
.tenant-btn.active-tenant { border-color: var(--gw-blue, #1a73e8); background: #e8f0fe; font-weight: 600; }
</style>
<script>
var msnry = new Masonry('#grid', {
"columnWidth": 300,
"itemSelector": ".grid-item",
"gutter": 10,
"transitionDuration": 0
});
setTimeout(() => {
msnry.layout()
}, 250)
setInterval(() => {
msnry.layout()
}, 1000);
</script>
<div class="account-grid">
<!-- Profile Card -->
<div class="account-card" style="text-align:center;">
<h2>Mi Perfil</h2>
<div class="avatar-lg"><?= htmlspecialchars($initials) ?></div>
<div style="font-size:1.2rem; font-weight:700; margin-bottom:4px;"><?= htmlspecialchars($displayName) ?></div>
<div style="color:var(--gw-text-secondary,#5f6368); margin-bottom:16px;"><?= htmlspecialchars($email ?: 'Sin correo') ?></div>
<div class="info-row"><span class="label">Usuario</span><span><?= htmlspecialchars($username) ?></span></div>
<?php if ($role): ?>
<div class="info-row"><span class="label">Rol</span><span><?= htmlspecialchars($role) ?></span></div>
<?php endif; ?>
<div style="margin-top:16px;">
<a href="/account/change_password.php" class="btn btn-secondary btn-sm">Cambiar contraseña</a>
</div>
</div>
<!-- QR Card -->
<div class="account-card" style="text-align:center;">
<h2>Código QR de Acceso</h2>
<img src="https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=<?= urlencode($username) ?>"
alt="QR Code" style="margin:0 auto 12px; display:block; width:150px; height:150px;">
<small style="color:var(--gw-text-secondary,#5f6368);">Escanea este código para iniciar sesión rápidamente.</small>
</div>
<!-- Tenant / Centro Card -->
<?php if (!empty($userCentros)): ?>
<div class="account-card">
<h2>Organizaciones</h2>
<?php foreach ($userCentros as $cid): ?>
<form method="post" action="/_incl/switch_tenant.php" style="margin:0;">
<input type="hidden" name="redir" value="/account/">
<button type="submit" name="centro" value="<?= htmlspecialchars($cid) ?>"
class="tenant-btn <?= ($activeCentro === $cid) ? 'active-tenant' : '' ?>">
<?php if ($activeCentro === $cid): ?>
<span style="color:var(--gw-blue,#1a73e8);">✓ </span>
<?php endif; ?>
<?= htmlspecialchars($cid) ?>
<?php if ($activeCentro === $cid): ?>
<span class="badge-pill badge-active" style="float:right;">Activo</span>
<?php endif; ?>
</button>
</form>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Aulas Card -->
<?php if (!empty($userAulas)): ?>
<div class="account-card">
<h2>Mis Aulas (<?= htmlspecialchars($activeCentro) ?>)</h2>
<?php foreach ($userAulas as $aula_id): ?>
<?php $aula = $aularios[$aula_id] ?? null; ?>
<div class="info-row">
<?php if ($aula && !empty($aula['icon'])): ?>
<img src="<?= htmlspecialchars($aula['icon']) ?>" style="height:20px;vertical-align:middle;margin-right:6px;">
<?php endif; ?>
<span><?= htmlspecialchars($aula['name'] ?? $aula_id) ?></span>
<span class="badge-pill badge-active">Asignada</span>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Permissions Card -->
<?php if (!empty($permissions)): ?>
<div class="account-card">
<h2>Permisos</h2>
<div>
<?php foreach ($permissions as $p): ?>
<span class="badge-pill badge-perm"><?= htmlspecialchars($p) ?></span>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- Session Info Card -->
<div class="account-card">
<h2>Sesión Activa</h2>
<div class="info-row"><span class="label">ID Sesión</span><span style="font-family:monospace;font-size:.75rem;"><?= htmlspecialchars(substr(session_id(), 0, 12)) ?>…</span></div>
<div class="info-row"><span class="label">Org. activa</span><span><?= htmlspecialchars($activeCentro ?: '') ?></span></div>
<div class="info-row"><span class="label">Autenticación</span><span><?= empty($authData['google_auth']) ? 'Contraseña' : 'Google' ?></span></div>
<div style="margin-top:16px;">
<a href="/_incl/logout.php" class="btn btn-danger btn-sm">Cerrar sesión</a>
</div>
</div>
</div>
<?php require_once "_incl/post-body.php"; ?>

View File

@@ -1,36 +1,27 @@
<?php
require_once "_incl/pre-body.php";
if ($_SERVER["REQUEST_METHOD"] === "POST") {
// Handle form submission
$invitations = json_decode(file_get_contents("/DATA/Invitaciones_de_usuarios.json"), true);
$invi_code = strtoupper($_POST['invitation_code'] ?? '');
if (!isset($invitations[$invi_code])) {
$invi_code = strtoupper(trim($_POST['invitation_code'] ?? ''));
$invitation = db_get_invitation($invi_code);
if (!$invitation || !$invitation['active']) {
header("Location: /?_resultcolor=red&_result=" . urlencode("Código de invitación no válido."));
exit;
}
$userdata = [
'display_name' => $_POST['display_name'],
'email' => $_POST['email'],
'password_hash' => password_hash($_POST['password'], PASSWORD_DEFAULT),
'_meta_signup' => [
'invitation_code' => $invi_code
],
'permissions' => []
];
if ($invitations[$invi_code]["active"] != true) {
header("Location: /?_resultcolor=red&_result=" . urlencode("Código de invitación no válido."));
exit;
}
$username = $_POST['username'];
if (file_exists("/DATA/Usuarios/$username.json")) {
$username = strtolower(trim($_POST['username'] ?? ''));
if (db_get_user($username)) {
header("Location: /?_resultcolor=red&_result=" . urlencode("El nombre de usuario ya existe. Por favor, elige otro."));
exit;
}
file_put_contents("/DATA/Usuarios/$username.json", json_encode($userdata, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
// Deactivate invitation code if it's single-use
if ($invitations[$invi_code]["single_use"] === true) {
$invitations[$invi_code]["active"] = false;
file_put_contents("/DATA/Invitaciones_de_usuarios.json", json_encode($invitations, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
db_upsert_user([
'username' => $username,
'display_name' => $_POST['display_name'] ?? '',
'email' => $_POST['email'] ?? '',
'password_hash' => password_hash($_POST['password'], PASSWORD_DEFAULT),
'permissions' => [],
'_meta_signup' => ['invitation_code' => $invi_code],
]);
if ($invitation['single_use']) {
db_deactivate_invitation($invi_code);
}
header("Location: /?_result=" . urlencode("Cuenta creada correctamente. Ya puedes iniciar sesión."));
exit;

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
$defaultMenuTypes = [
["id" => "basal", "label" => "Menú basal", "color" => "#0d6efd"],
["id" => "vegetariano", "label" => "Menú vegetariano", "color" => "#198754"],
["id" => "alergias", "label" => "Menú alergias", "color" => "#dc3545"],
];
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"],
];
if ($menuTypesPath === null) {
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"]);
}

View File

@@ -2,11 +2,11 @@
require_once "_incl/auth_redir.php";
require_once "_incl/pre-body.php";
require_once "../_incl/tools.security.php";
require_once "../_incl/db.php";
$aulario_id = safe_id_segment($_GET["id"] ?? "");
$centro_id = safe_centro_id($_SESSION["auth_data"]["entreaulas"]["centro"] ?? "");
$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;
$centro_id = safe_centro_id($_SESSION["auth_data"]["entreaulas"]["centro"] ?? "");
$aulario = db_get_aulario($centro_id, $aulario_id);
if (!$aulario || !is_array($aulario)) {
?>

View File

@@ -1,6 +1,7 @@
<?php
require_once "_incl/auth_redir.php";
require_once "../_incl/tools.security.php";
require_once "../_incl/db.php";
if (in_array("entreaulas:docente", $_SESSION["auth_data"]["permissions"] ?? []) === false) {
header("HTTP/1.1 403 Forbidden");
@@ -22,38 +23,31 @@ if ($aulario_id === "" || $centro_id === "") {
exit;
}
$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);
// Check if this aulario shares comedor data from another aulario
$source_aulario_id = $aulario_id; // Default to current aulario
$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)) {
$source_aulario_id = $shared_from;
$source_aulario_name = json_decode(file_get_contents($shared_aulario_path), true)["name"] ?? $shared_from;
$shared_aulario = db_get_aulario($centro_id, $shared_from);
if ($shared_aulario) {
$source_aulario_id = $shared_from;
$source_aulario_name = $shared_aulario["name"] ?? $shared_from;
$is_shared = true;
}
}
$menuTypesPath = "/DATA/entreaulas/Centros/$centro_id/Aularios/$source_aulario_id/Comedor-MenuTypes.json";
$defaultMenuTypes = [
["id" => "basal", "label" => "Menú basal", "color" => "#0d6efd"],
["id" => "basal", "label" => "Menú basal", "color" => "#0d6efd"],
["id" => "vegetariano", "label" => "Menú vegetariano", "color" => "#198754"],
["id" => "alergias", "label" => "Menú alergias", "color" => "#dc3545"],
["id" => "alergias", "label" => "Menú alergias", "color" => "#dc3545"],
];
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));
}
$menuTypes = json_decode(@file_get_contents($menuTypesPath), true);
$menuTypes = db_get_comedor_menu_types($centro_id, $source_aulario_id);
if (!is_array($menuTypes) || count($menuTypes) === 0) {
$menuTypes = $defaultMenuTypes;
db_set_comedor_menu_types($centro_id, $source_aulario_id, $menuTypes);
}
$menuTypeIds = [];
@@ -71,10 +65,9 @@ if (!in_array($menuTypeId, $menuTypeIds, true)) {
$menuTypeId = $menuTypeIds[0] ?? "basal";
}
$ym = $dateObj->format("Y-m");
$ym = $dateObj->format("Y-m");
$day = $dateObj->format("d");
$baseDir = "/DATA/entreaulas/Centros/$centro_id/Aularios/$source_aulario_id/Comedor/$ym/$day";
$dataPath = "$baseDir/_datos.json";
function blank_menu()
{
@@ -91,11 +84,9 @@ $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);
}
$existing = db_get_comedor_entry($centro_id, $source_aulario_id, $ym, $day);
if (is_array($existing) && !empty($existing)) {
$menuData = array_merge($menuData, $existing);
}
if (!isset($menuData["menus"][$menuTypeId])) {
$menuData["menus"][$menuTypeId] = blank_menu();
@@ -251,14 +242,11 @@ if ($_SERVER["REQUEST_METHOD"] === "POST" && $canEdit) {
if ($newId !== "" && $newLabel !== "") {
$exists = false;
foreach ($menuTypes as $t) {
if (($t["id"] ?? "") === $newId) {
$exists = true;
break;
}
if (($t["id"] ?? "") === $newId) { $exists = true; break; }
}
if (!$exists) {
$menuTypes[] = ["id" => $newId, "label" => $newLabel, "color" => $newColor];
file_put_contents($menuTypesPath, json_encode($menuTypes, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
db_set_comedor_menu_types($centro_id, $source_aulario_id, $menuTypes);
header("Location: /entreaulas/comedor.php?aulario=" . urlencode($aulario_id) . "&date=" . urlencode($date) . "&menu=" . urlencode($newId));
exit;
}
@@ -268,19 +256,10 @@ if ($_SERVER["REQUEST_METHOD"] === "POST" && $canEdit) {
if ($action === "delete_type") {
$deleteId = safe_id_segment(trim($_POST["delete_type_id"] ?? ""));
if ($deleteId !== "") {
$deleted = false;
$newMenuTypes = [];
foreach ($menuTypes as $t) {
if (($t["id"] ?? "") === $deleteId) {
$deleted = true;
} else {
$newMenuTypes[] = $t;
}
}
if ($deleted) {
$newMenuTypes = array_values(array_filter($menuTypes, fn($t) => ($t["id"] ?? "") !== $deleteId));
if (count($newMenuTypes) !== count($menuTypes)) {
$menuTypes = $newMenuTypes;
file_put_contents($menuTypesPath, json_encode($menuTypes, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
// Redirect to the first available menu type or default
db_set_comedor_menu_types($centro_id, $source_aulario_id, $menuTypes);
$redirectMenuId = !empty($menuTypes) ? $menuTypes[0]["id"] : "basal";
header("Location: /entreaulas/comedor.php?aulario=" . urlencode($aulario_id) . "&date=" . urlencode($date) . "&menu=" . urlencode($redirectMenuId));
exit;
@@ -296,15 +275,12 @@ if ($_SERVER["REQUEST_METHOD"] === "POST" && $canEdit) {
foreach ($menuTypes as &$t) {
if (($t["id"] ?? "") === $renameId) {
$t["label"] = $newLabel;
if ($newColor !== "") {
$t["color"] = $newColor;
}
if ($newColor !== "") { $t["color"] = $newColor; }
break;
}
}
// Clean up the reference to avoid accidental usage after the loop
unset($t);
file_put_contents($menuTypesPath, json_encode($menuTypes, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
db_set_comedor_menu_types($centro_id, $source_aulario_id, $menuTypes);
header("Location: /entreaulas/comedor.php?aulario=" . urlencode($aulario_id) . "&date=" . urlencode($date) . "&menu=" . urlencode($renameId));
exit;
}
@@ -316,23 +292,19 @@ if ($_SERVER["REQUEST_METHOD"] === "POST" && $canEdit) {
$menuData["menus"][$menuTypeId] = blank_menu();
}
// Pictogram images still stored on filesystem in Comedor dir
$baseDir = "/DATA/entreaulas/Centros/$centro_id/Aularios/$source_aulario_id/Comedor/$ym/$day";
$plates = ["primero", "segundo", "postre"];
foreach ($plates as $plate) {
$name = trim($_POST["name_" . $plate] ?? "");
$menuData["menus"][$menuTypeId]["plates"][$plate]["name"] = $name;
$pictUpload = handle_image_upload("pictogram_file_" . $plate, $menuTypeId . "_" . $plate . "_pict", $baseDir, $uploadErrors);
if ($pictUpload !== null) {
$menuData["menus"][$menuTypeId]["plates"][$plate]["pictogram"] = $pictUpload;
}
}
if (!is_dir($baseDir)) {
mkdir($baseDir, 0777, true);
}
file_put_contents($dataPath, json_encode($menuData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
db_set_comedor_entry($centro_id, $source_aulario_id, $ym, $day, $menuData);
$saveNotice = "Menú guardado correctamente.";
}
}
@@ -359,11 +331,10 @@ foreach ($userAulas as $aulaId) {
if ($aulaIdSafe === "") {
continue;
}
$aulaPath = "/DATA/entreaulas/Centros/$centro_id/Aularios/$aulaIdSafe.json";
$aulaData = file_exists($aulaPath) ? json_decode(file_get_contents($aulaPath), true) : null;
$aulaData = db_get_aulario($centro_id, $aulaIdSafe);
$aulaOptions[] = [
"id" => $aulaIdSafe,
"name" => $aulaData["name"] ?? $aulaIdSafe
"id" => $aulaIdSafe,
"name" => $aulaData["name"] ?? $aulaIdSafe,
];
}
require_once "_incl/pre-body.php";

25
public_html/aulatek/index.php Executable file → Normal file
View File

@@ -2,30 +2,29 @@
require_once "_incl/auth_redir.php";
require_once "_incl/pre-body.php";
require_once "../_incl/tools.security.php";
require_once "../_incl/db.php";
?>
<div class="card pad">
<div>
<h1 class="card-title">¡Hola, <?php echo $_SESSION["auth_data"]["display_name"];?>!</h1>
<h1 class="card-title">¡Hola, <?php echo htmlspecialchars($_SESSION["auth_data"]["display_name"]); ?>!</h1>
<span>
Bienvenidx a la plataforma de gestión de aularios conectados. Desde aquí podrás administrar los aularios asociados a tu cuenta.
</span>
</div>
</div>
<div id="grid">
<?php $user_data = $_SESSION["auth_data"];
<?php
$user_data = $_SESSION["auth_data"];
$centro_id = safe_centro_id($user_data["entreaulas"]["centro"] ?? "");
foreach ($user_data["entreaulas"]["aulas"] as $aulario_id) {
$user_aulas = $user_data["entreaulas"]["aulas"] ?? [];
foreach ($user_aulas as $aulario_id) {
$aulario_id = safe_id_segment($aulario_id);
if ($aulario_id === "") {
continue;
}
$aulario_path = safe_aulario_config_path($centro_id, $aulario_id);
if (!$aulario_path || !file_exists($aulario_path)) {
continue;
}
$aulario = json_decode(file_get_contents($aulario_path), true);
if (!is_array($aulario)) {
$aulario = db_get_aulario($centro_id, $aulario_id);
if (!$aulario) {
continue;
}
$aulario_name = $aulario["name"] ?? $aulario_id;
@@ -61,7 +60,6 @@ require_once "../_incl/tools.security.php";
}
</style>
<script>
var msnry = new Masonry('#grid', {
"columnWidth": 250,
@@ -69,10 +67,7 @@ require_once "../_incl/tools.security.php";
"gutter": 10,
"transitionDuration": 0
});
setInterval(() => {
msnry.layout()
}, 1000);
msnry.layout()
setTimeout(() => { msnry.layout() }, 250);
setInterval(() => { msnry.layout() }, 1000);
</script>
<?php require_once "_incl/post-body.php"; ?>

View File

@@ -1,6 +1,7 @@
<?php
require_once "_incl/auth_redir.php";
require_once "../_incl/tools.security.php";
require_once "../_incl/db.php";
ini_set("display_errors", "0");
// Funciones auxiliares para el diario
@@ -784,12 +785,10 @@ switch ($view_action) {
$source_aulario_id = $aulario_id;
$is_shared = false;
if ($aulario_id !== "" && $centro_id !== "") {
$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);
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;
}
@@ -800,13 +799,12 @@ switch ($view_action) {
$dateObj = DateTime::createFromFormat("Y-m-d", $dateParam) ?: new DateTime();
$date = $dateObj->format("Y-m-d");
$menuTypesPath = ($centro_id !== '' && $source_aulario_id !== '') ? "/DATA/entreaulas/Centros/$centro_id/Aularios/$source_aulario_id/Comedor-MenuTypes.json" : "";
$defaultMenuTypes = [
["id" => "basal", "label" => "Menú basal", "color" => "#0d6efd"],
["id" => "vegetariano", "label" => "Menú vegetariano", "color" => "#198754"],
["id" => "alergias", "label" => "Menú alergias", "color" => "#dc3545"],
];
$menuTypes = ($menuTypesPath !== '' && file_exists($menuTypesPath)) ? json_decode(@file_get_contents($menuTypesPath), true) : null;
$menuTypes = ($centro_id !== '' && $source_aulario_id !== '') ? db_get_comedor_menu_types($centro_id, $source_aulario_id) : [];
if (!is_array($menuTypes) || count($menuTypes) === 0) {
$menuTypes = $defaultMenuTypes;
}
@@ -822,17 +820,13 @@ switch ($view_action) {
$menuTypeId = $menuTypeIds[0] ?? "basal";
}
$ym = $dateObj->format("Y-m");
$ym = $dateObj->format("Y-m");
$day = $dateObj->format("d");
$dataPath = ($centro_id !== '' && $source_aulario_id !== '') ? "/DATA/entreaulas/Centros/$centro_id/Aularios/$source_aulario_id/Comedor/$ym/$day/_datos.json" : "";
$menuData = [
"date" => $date,
"menus" => []
];
if ($dataPath !== '' && file_exists($dataPath)) {
$existing = json_decode(file_get_contents($dataPath), true);
if (is_array($existing)) {
$menuData = ["date" => $date, "menus" => []];
if ($centro_id !== '' && $source_aulario_id !== '') {
$existing = db_get_comedor_entry($centro_id, $source_aulario_id, $ym, $day);
if (!empty($existing)) {
$menuData = array_merge($menuData, $existing);
}
}

View File

@@ -1,6 +1,7 @@
<?php
require_once "_incl/auth_redir.php";
require_once "../_incl/tools.security.php";
require_once "../_incl/db.php";
if (in_array("entreaulas:docente", $_SESSION["auth_data"]["permissions"] ?? []) === false) {
header("HTTP/1.1 403 Forbidden");
@@ -23,8 +24,7 @@ if ($aulario_id === "" || $centro_id === "") {
exit;
}
$aulario_path = "/DATA/entreaulas/Centros/$centro_id/Aularios/$aulario_id.json";
$aulario = file_exists($aulario_path) ? json_decode(file_get_contents($aulario_path), true) : null;
$aulario = db_get_aulario($centro_id, $aulario_id);
$proyectos_dir = "/DATA/entreaulas/Centros/$centro_id/Proyectos";
if (!is_dir($proyectos_dir)) {
@@ -71,16 +71,6 @@ function safe_join_file($base_dir, $filename)
return rtrim($base_dir, '/') . '/' . $safe_name;
}
function safe_aulario_config_path($centro_id, $aulario_id)
{
$safe_centro = safe_path_segment($centro_id);
$safe_aulario = safe_path_segment($aulario_id);
if ($safe_centro === '' || $safe_aulario === '') {
return null;
}
return "/DATA/entreaulas/Centros/$safe_centro/Aularios/$safe_aulario.json";
}
function sanitize_html($html)
{
$html = trim($html ?? "");
@@ -459,23 +449,18 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
}
if ($action === "share_project") {
$project_id = safe_path_segment($_POST["project_id"] ?? "");
$project_id = safe_path_segment($_POST["project_id"] ?? "");
$target_aulario = safe_path_segment($_POST["target_aulario"] ?? "");
if ($project_id !== "" && $target_aulario !== "" && $target_aulario !== $aulario_id) {
// Only allow sharing local projects
$is_local_project = (load_project($proyectos_dir, $project_id) !== null);
if (!$is_local_project) {
$error = "No se puede compartir un proyecto ajeno.";
} else {
$target_config_path = safe_aulario_config_path($centro_id, $target_aulario);
if ($target_config_path === null || !file_exists($target_config_path)) {
$target_config = db_get_aulario($centro_id, $target_aulario);
if ($target_config === null) {
$error = "Aulario de destino no encontrado.";
} else {
$target_config = json_decode(file_get_contents($target_config_path), true);
if (!is_array($target_config)) {
$target_config = [];
}
if (!isset($target_config["linked_projects"]) || !is_array($target_config["linked_projects"])) {
$target_config["linked_projects"] = [];
}
@@ -494,13 +479,23 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
} else {
$target_config["linked_projects"][] = [
"source_aulario" => $aulario_id,
"project_id" => $project_id,
"permission" => "request_edit"
"project_id" => $project_id,
"permission" => "request_edit",
];
$message = "Proyecto compartido correctamente.";
}
file_put_contents($target_config_path, json_encode($target_config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
// Save back: build extra JSON excluding standard fields
$extra_skip = ['name', 'icon'];
$extra = [];
foreach ($target_config as $k => $v) {
if (!in_array($k, $extra_skip, true)) {
$extra[$k] = $v;
}
}
db()->prepare(
"UPDATE aularios SET extra = ? WHERE centro_id = ? AND aulario_id = ?"
)->execute([json_encode($extra, JSON_UNESCAPED_UNICODE), $centro_id, $target_aulario]);
}
}
}
@@ -1570,10 +1565,9 @@ $view = $current_project ? "project" : "list";
<path d="M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.34C15.11,18.55 15.08,18.77 15.08,19C15.08,20.61 16.39,21.91 18,21.91C19.61,21.91 20.92,20.61 20.92,19A2.92,2.92 0 0,0 18,16.08Z" />
</svg>
<?php
$source_aulario_path = safe_aulario_config_path($centro_id, $source_aulario_for_project);
$source_aulario_name = "";
if ($source_aulario_path && file_exists($source_aulario_path)) {
$source_aulario_data = json_decode(file_get_contents($source_aulario_path), true);
$source_aulario_data = db_get_aulario($centro_id, $source_aulario_for_project);
if ($source_aulario_data) {
$source_aulario_name = $source_aulario_data["name"] ?? "";
}
?>
@@ -1636,27 +1630,14 @@ $view = $current_project ? "project" : "list";
<?php
function list_aularios($centro_id)
{
$aularios_dir = "/DATA/entreaulas/Centros/$centro_id/Aularios";
$aularios_db = db_get_aularios($centro_id);
$aularios = [];
if (is_dir($aularios_dir)) {
$entries = scandir($aularios_dir);
foreach ($entries as $entry) {
if ($entry === "." || $entry === "..") {
continue;
}
$aulario_path = "$aularios_dir/$entry";
if (is_dir($aulario_path)) {
$config_file = "$aulario_path.json";
if (file_exists($config_file)) {
$config = json_decode(file_get_contents($config_file), true);
$aularios[] = [
"id" => $entry,
"name" => $config["name"] ?? "Aulario Desconocido",
"linked_projects" => $config["linked_projects"] ?? []
];
}
}
}
foreach ($aularios_db as $aid => $adata) {
$aularios[] = [
"id" => $aid,
"name" => $adata["name"] ?? "Aulario Desconocido",
"linked_projects" => $adata["linked_projects"] ?? [],
];
}
return $aularios;
}
@@ -1918,10 +1899,8 @@ $view = $current_project ? "project" : "list";
<div id="grid">
<?php foreach ($pending_changes as $change):
$requesting_aulario = $change["requested_by_aulario"] ?? "Desconocido";
// Get requesting aulario name
$requesting_aulario = safe_path_segment($requesting_aulario);
$req_aul_path = safe_aulario_config_path($centro_id, $requesting_aulario);
$req_aul_data = ($req_aul_path && file_exists($req_aul_path)) ? json_decode(file_get_contents($req_aul_path), true) : null;
$req_aul_data = db_get_aulario($centro_id, $requesting_aulario);
$req_aul_name = $req_aul_data["name"] ?? $requesting_aulario;
$req_persona_name = $change["requested_by_persona_name"] ?? "Desconocido";
?>

View File

@@ -1,6 +1,7 @@
<?php
require_once "_incl/auth_redir.php";
require_once "../_incl/tools.security.php";
require_once "../_incl/db.php";
if (!in_array('supercafe:access', $_SESSION['auth_data']['permissions'] ?? [])) {
header('HTTP/1.1 403 Forbidden');
@@ -8,28 +9,23 @@ if (!in_array('supercafe:access', $_SESSION['auth_data']['permissions'] ?? []))
}
/**
* Load personas from the existing Alumnos system.
* Returns array keyed by "{aulario_id}:{alumno_name}" with
* ['Nombre', 'Region' (aulario display name), 'AularioID', 'HasPhoto'] entries.
* Load personas from the Alumnos filesystem (photos still on disk).
* Returns array keyed by "{aulario_id}:{alumno_name}".
*/
function sc_load_personas_from_alumnos($centro_id)
{
$aularios_path = "/DATA/entreaulas/Centros/$centro_id/Aularios";
$personas = [];
if (!is_dir($aularios_path)) {
return $personas;
}
foreach (glob("$aularios_path/*.json") ?: [] as $aulario_file) {
$aulario_id = basename($aulario_file, '.json');
$aulario_data = json_decode(file_get_contents($aulario_file), true);
$aularios = db_get_aularios($centro_id);
$personas = [];
$aularios_dir = "/DATA/entreaulas/Centros/$centro_id/Aularios";
foreach ($aularios as $aulario_id => $aulario_data) {
$aulario_name = $aulario_data['name'] ?? $aulario_id;
$alumnos_path = "$aularios_path/$aulario_id/Alumnos";
$alumnos_path = "$aularios_dir/$aulario_id/Alumnos";
if (!is_dir($alumnos_path)) {
continue;
}
foreach (glob("$alumnos_path/*/", GLOB_ONLYDIR) ?: [] as $alumno_dir) {
$alumno_name = basename($alumno_dir);
$key = $aulario_id . ':' . $alumno_name;
$key = $aulario_id . ':' . $alumno_name;
$personas[$key] = [
'Nombre' => $alumno_name,
'Region' => $aulario_name,
@@ -62,7 +58,6 @@ if ($centro_id === '') {
exit;
}
define('SC_DATA_DIR', "/DATA/entreaulas/Centros/$centro_id/SuperCafe/Comandas");
define('SC_MAX_DEBTS', 3);
$estados_colores = [
@@ -81,20 +76,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $can_edit) {
$action = $_POST['action'] ?? '';
if ($action === 'change_status') {
$order_id = safe_id($_POST['order_id'] ?? '');
$order_id = safe_id($_POST['order_id'] ?? '');
$new_status = $_POST['status'] ?? '';
if ($order_id !== '' && array_key_exists($new_status, $estados_colores)) {
$order_file = SC_DATA_DIR . '/' . $order_id . '.json';
if (is_readable($order_file)) {
$data = json_decode(file_get_contents($order_file), true);
if (is_array($data)) {
$data['Estado'] = $new_status;
file_put_contents(
$order_file,
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE),
LOCK_EX
);
}
$row = db_get_supercafe_order($centro_id, $order_id);
if ($row) {
db_upsert_supercafe_order(
$centro_id, $order_id,
$row['fecha'], $row['persona'], $row['comanda'], $row['notas'], $new_status
);
}
}
header('Location: /entreaulas/supercafe.php');
@@ -104,34 +94,30 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $can_edit) {
if ($action === 'delete') {
$order_id = safe_id($_POST['order_id'] ?? '');
if ($order_id !== '') {
$order_file = SC_DATA_DIR . '/' . $order_id . '.json';
if (is_file($order_file)) {
unlink($order_file);
}
db()->prepare('DELETE FROM supercafe_orders WHERE centro_id = ? AND order_ref = ?')
->execute([$centro_id, $order_id]);
}
header('Location: /entreaulas/supercafe.php');
exit;
}
}
// Load all orders
// Load all orders from DB
$db_orders = db_get_supercafe_orders($centro_id);
$orders = [];
if (is_dir(SC_DATA_DIR)) {
$files = glob(SC_DATA_DIR . '/*.json') ?: [];
foreach ($files as $file) {
$data = json_decode(file_get_contents($file), true);
if (!is_array($data)) {
continue;
}
$data['_id'] = basename($file, '.json');
$orders[] = $data;
}
foreach ($db_orders as $row) {
$orders[] = [
'_id' => $row['order_ref'],
'Fecha' => $row['fecha'],
'Persona'=> $row['persona'],
'Comanda'=> $row['comanda'],
'Notas' => $row['notas'],
'Estado' => $row['estado'],
];
}
// Sort newest first (by Fecha desc)
usort($orders, function ($a, $b) {
return strcmp($b['Fecha'] ?? '', $a['Fecha'] ?? '');
});
usort($orders, fn($a, $b) => strcmp($b['Fecha'] ?? '', $a['Fecha'] ?? ''));
$orders_active = array_filter($orders, fn($o) => ($o['Estado'] ?? '') !== 'Deuda');
$orders_deuda = array_filter($orders, fn($o) => ($o['Estado'] ?? '') === 'Deuda');
@@ -139,6 +125,7 @@ $orders_deuda = array_filter($orders, fn($o) => ($o['Estado'] ?? '') === 'Deuda
require_once "_incl/pre-body.php";
?>
<div class="card pad">
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px;">
<h1 style="margin: 0;">SuperCafe Cafetería</h1>

View File

@@ -1,6 +1,7 @@
<?php
require_once "_incl/auth_redir.php";
require_once "../_incl/tools.security.php";
require_once "../_incl/db.php";
if (!in_array('supercafe:edit', $_SESSION['auth_data']['permissions'] ?? [])) {
header('HTTP/1.1 403 Forbidden');
@@ -15,42 +16,30 @@ if ($centro_id === '') {
exit;
}
$sc_base = "/DATA/entreaulas/Centros/$centro_id/SuperCafe";
define('SC_DATA_DIR', "$sc_base/Comandas");
define('SC_MAX_DEBTS', 3);
$valid_statuses = ['Pedido', 'En preparación', 'Listo', 'Entregado', 'Deuda'];
/**
* Load personas from the existing Alumnos system (alumnos.php).
* Returns array keyed by "{aulario_id}:{alumno_name}" with
* ['Nombre', 'Region' (aulario display name), 'AularioID'] entries.
* Groups are sorted by aulario name, alumnos sorted alphabetically.
* Load personas from the Alumnos filesystem (photos still on disk).
* Returns array keyed by "{aulario_id}:{alumno_name}".
*/
function sc_load_personas_from_alumnos($centro_id)
{
$aularios_path = "/DATA/entreaulas/Centros/$centro_id/Aularios";
$personas = [];
if (!is_dir($aularios_path)) {
return $personas;
}
$aulario_files = glob("$aularios_path/*.json") ?: [];
foreach ($aulario_files as $aulario_file) {
$aulario_id = basename($aulario_file, '.json');
$aulario_data = json_decode(file_get_contents($aulario_file), true);
$aularios = db_get_aularios($centro_id);
$personas = [];
$aularios_dir = "/DATA/entreaulas/Centros/$centro_id/Aularios";
foreach ($aularios as $aulario_id => $aulario_data) {
$aulario_name = $aulario_data['name'] ?? $aulario_id;
$alumnos_path = "$aularios_path/$aulario_id/Alumnos";
$alumnos_path = "$aularios_dir/$aulario_id/Alumnos";
if (!is_dir($alumnos_path)) {
continue;
}
$alumno_dirs = glob("$alumnos_path/*/", GLOB_ONLYDIR) ?: [];
usort($alumno_dirs, function ($a, $b) {
return strcasecmp(basename($a), basename($b));
});
usort($alumno_dirs, fn($a, $b) => strcasecmp(basename($a), basename($b)));
foreach ($alumno_dirs as $alumno_dir) {
$alumno_name = basename($alumno_dir);
// Key uses ':' as separator; safe_id_segment chars [A-Za-z0-9_-] exclude ':'
$key = $aulario_id . ':' . $alumno_name;
$key = $aulario_id . ':' . $alumno_name;
$personas[$key] = [
'Nombre' => $alumno_name,
'Region' => $aulario_name,
@@ -62,43 +51,14 @@ function sc_load_personas_from_alumnos($centro_id)
return $personas;
}
function sc_load_menu($sc_base)
{
$path = "$sc_base/Menu.json";
if (!file_exists($path)) {
return [];
}
$data = json_decode(file_get_contents($path), true);
return is_array($data) ? $data : [];
}
function sc_count_debts($persona_key)
{
if (!is_dir(SC_DATA_DIR)) {
return 0;
}
$count = 0;
foreach (glob(SC_DATA_DIR . '/*.json') ?: [] as $file) {
$data = json_decode(file_get_contents($file), true);
if (is_array($data)
&& ($data['Persona'] ?? '') === $persona_key
&& ($data['Estado'] ?? '') === 'Deuda') {
$count++;
}
}
return $count;
}
// Determine if creating or editing
$order_id = safe_id($_GET['id'] ?? '');
$is_new = $order_id === '';
if ($is_new) {
$raw_id = uniqid('sc', true);
$order_id = preg_replace('/[^a-zA-Z0-9_-]/', '', $raw_id);
$order_id = db_next_supercafe_ref($centro_id);
}
$order_file = SC_DATA_DIR . '/' . $order_id . '.json';
// Load existing order from DB (or defaults)
$order_data = [
'Fecha' => date('Y-m-d'),
'Persona' => '',
@@ -106,15 +66,21 @@ $order_data = [
'Notas' => '',
'Estado' => 'Pedido',
];
if (!$is_new && is_readable($order_file)) {
$existing = json_decode(file_get_contents($order_file), true);
if (is_array($existing)) {
$order_data = array_merge($order_data, $existing);
if (!$is_new) {
$existing = db_get_supercafe_order($centro_id, $order_id);
if ($existing) {
$order_data = [
'Fecha' => $existing['fecha'],
'Persona' => $existing['persona'],
'Comanda' => $existing['comanda'],
'Notas' => $existing['notas'],
'Estado' => $existing['estado'],
];
}
}
$personas = sc_load_personas_from_alumnos($centro_id);
$menu = sc_load_menu($sc_base);
$menu = db_get_supercafe_menu($centro_id);
// Group personas by aulario for the optgroup picker
$personas_by_aulario = [];
@@ -133,11 +99,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$estado = 'Pedido';
}
// Validar persona
if ($persona_key === '' || (!empty($personas) && !array_key_exists($persona_key, $personas))) {
$error = '¡Hay que elegir una persona válida!';
} else {
// Construir comanda desde los campos de categoría visual
$comanda_parts = [];
if (!empty($menu)) {
foreach ($menu as $category => $items) {
@@ -152,48 +116,28 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$comanda_parts[] = $manual;
}
}
$comanda_str = implode(', ', $comanda_parts);
// Comprobar deudas
$prev_persona = $order_data['Persona'] ?? '';
$comanda_str = implode(', ', $comanda_parts);
$prev_persona = $order_data['Persona'];
if ($is_new || $prev_persona !== $persona_key) {
$debt_count = sc_count_debts($persona_key);
$debt_count = db_supercafe_count_debts($centro_id, $persona_key);
if ($debt_count >= SC_MAX_DEBTS) {
$error = 'Esta persona tiene ' . $debt_count . ' comandas en deuda. No se puede realizar el pedido.';
}
}
if ($error === '') {
$new_data = [
'Fecha' => date('Y-m-d'),
'Persona' => $persona_key,
'Comanda' => $comanda_str,
'Notas' => $notas,
'Estado' => $is_new ? 'Pedido' : $estado,
];
if (!is_dir(SC_DATA_DIR)) {
mkdir(SC_DATA_DIR, 0755, true);
}
$tmp = SC_DATA_DIR . '/.' . $order_id . '.tmp';
$bytes = file_put_contents(
$tmp,
json_encode($new_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE),
LOCK_EX
db_upsert_supercafe_order(
$centro_id, $order_id,
date('Y-m-d'), $persona_key, $comanda_str, $notas,
$is_new ? 'Pedido' : $estado
);
if ($bytes === false || !rename($tmp, $order_file)) {
@unlink($tmp);
$error = 'Error al guardar la comanda.';
} else {
header('Location: /entreaulas/supercafe.php');
exit;
}
header('Location: /entreaulas/supercafe.php');
exit;
}
}
}
require_once "_incl/pre-body.php";
?>
<h1>Comanda <code><?= htmlspecialchars($order_id) ?></code></h1>

View File

@@ -1,10 +1,11 @@
<?php
require_once "../_incl/tools.session.php";
require_once "../_incl/tools.security.php";
require_once "../_incl/db.php";
ini_set("display_errors", 0);
$file = Sf($_GET["f"]);
$date = implode("/", array_reverse(explode("-", $file)));
$val = json_decode(file_get_contents("/DATA/club/IMG/$file/data.json"), true);
$val = db_get_club_event($file);
$fotos = glob("/DATA/club/IMG/$file/*/");

View File

@@ -1,19 +1,18 @@
<?php
ini_set("display_errors", 0);
require_once "../_incl/db.php";
$file = Sf($_GET["f"]);
$date = implode("/", array_reverse(explode("-", $file)));
$val = json_decode(file_get_contents("/DATA/club/IMG/$file/data.json"), true);
$config = json_decode(file_get_contents("/DATA/club/config.json"), true);
if(strtoupper($_POST["adminpw"]) == strtoupper($config["adminpw"] ?? "")) {
$val = db_get_club_event($file);
$adminpw = db_get_config('club_adminpw', '');
if (strtoupper($_POST["adminpw"] ?? '') === strtoupper($adminpw) && !empty($adminpw)) {
$data = [
"title" => $_POST["title"],
"note" => $_POST["note"],
"mapa" => [
"url" => $_POST["mapa_url"]
]
];
"title" => $_POST["title"],
"note" => $_POST["note"],
"mapa" => ["url" => $_POST["mapa_url"]],
];
$file = $_POST["date"];
$val = file_put_contents("/DATA/club/IMG/$file/data.json", json_encode($data, JSON_UNESCAPED_SLASHES));
db_set_club_event($file, $data);
header("Location: /club/");
die();
}

View File

@@ -1,5 +1,6 @@
<?php
ini_set("display_errors", 0);
require_once "../_incl/db.php";
$files = glob("/DATA/club/IMG/*/");
sort($files);
$files = array_reverse($files);
@@ -17,7 +18,7 @@ require_once "../_incl/pre-body.php"; ?>
<?php foreach ($files as $file) {
$filenam = str_replace("/", "", str_replace("/DATA/club/IMG/", "", $file));
$date = implode("/", array_reverse(explode("-", $filenam)));
$val = json_decode(file_get_contents($file . "data.json"), true)
$val = db_get_club_event($filenam);
?>
<li><a class="btn btn-secondary" href="cal.php?f=<?php echo $filenam; ?>"><b><?php echo $date; ?></b></a> -

View File

@@ -1,7 +1,8 @@
<?php
ini_set("display_errors", 1);
$config = json_decode(file_get_contents("/DATA/club/config.json"), true);
if (strtoupper($_GET["pw"]) != $config["uploadpw"]) {
require_once "../../_incl/db.php";
$uploadpw = db_get_config('club_uploadpw', '');
if ($uploadpw === '' || strtoupper($_GET["pw"] ?? '') !== strtoupper($uploadpw)) {
header("HTTP/1.1 401 Unauthorized");
die();
}

View File

@@ -1,10 +1,11 @@
<?php
require_once "_incl/auth_redir.php";
require_once "../_incl/tools.security.php";
require_once "../_incl/db.php";
function safe_path_segment($value)
{
$value = trim((string)$value);
$value = trim((string) $value);
$value = str_replace(["\0", "/", "\\"], "", $value);
$value = str_replace("..", "", $value);
$value = basename($value);
@@ -18,417 +19,214 @@ $form_action = $_GET["form"] ?? "";
switch ($form_action) {
case "delete":
$aulario_id = safe_path_segment(Sf($_POST["aulario_id"] ?? ""));
$centro_id = safe_path_segment(Sf($_POST["centro_id"] ?? ""));
$aulario_file = "/DATA/entreaulas/Centros/$centro_id/Aularios/$aulario_id.json";
if (!file_exists($aulario_file)) {
die("Aulario no encontrado.");
$centro_id = safe_path_segment(Sf($_POST["centro_id"] ?? ""));
if ($aulario_id === "" || $centro_id === "") {
die("Parámetros inválidos.");
}
// Remove aulario directory and contents
// Remove from DB
db()->prepare("DELETE FROM aularios WHERE centro_id = ? AND aulario_id = ?")
->execute([$centro_id, $aulario_id]);
// Remove comedor, diario, panel data
db()->prepare("DELETE FROM comedor_menu_types WHERE centro_id = ? AND aulario_id = ?")
->execute([$centro_id, $aulario_id]);
db()->prepare("DELETE FROM comedor_entries WHERE centro_id = ? AND aulario_id = ?")
->execute([$centro_id, $aulario_id]);
db()->prepare("DELETE FROM diario_entries WHERE centro_id = ? AND aulario_id = ?")
->execute([$centro_id, $aulario_id]);
db()->prepare("DELETE FROM panel_alumno WHERE centro_id = ? AND aulario_id = ?")
->execute([$centro_id, $aulario_id]);
// Remove filesystem directory with student photos
$aulario_dir = "/DATA/entreaulas/Centros/$centro_id/Aularios/$aulario_id";
function rrmdir($dir) {
function rrmdir($dir)
{
if (is_dir($dir)) {
$objects = scandir($dir);
foreach ($objects as $object) {
if ($object != "." && $object != "..") {
$obj_path = $dir . "/" . $object;
if (is_dir($obj_path)) {
rrmdir($obj_path);
} else {
unlink($obj_path);
}
foreach (scandir($dir) as $object) {
if ($object !== "." && $object !== "..") {
$p = "$dir/$object";
is_dir($p) ? rrmdir($p) : unlink($p);
}
}
rmdir($dir);
}
}
rrmdir($aulario_dir);
// Remove aulario config file
unlink($aulario_file);
header("Location: ?action=index");
exit();
break;
case "create":
$user_data = $_SESSION["auth_data"];
$centro_id = safe_path_segment(Sf($_POST["centro"] ?? ""));
if (empty($centro_id) || !is_dir("/DATA/entreaulas/Centros/$centro_id")) {
$centro_id = safe_path_segment(Sf($_POST["centro"] ?? ""));
$aulario_id = strtolower(preg_replace("/[^a-zA-Z0-9_-]/", "_", Sf($_POST["name"] ?? "")));
if (empty($centro_id) || empty($aulario_id)) {
die("Datos incompletos.");
}
// Ensure centro exists in DB
$stmt = db()->prepare("SELECT id FROM centros WHERE centro_id = ?");
$stmt->execute([$centro_id]);
if (!$stmt->fetch()) {
die("Centro no válido.");
}
$aulario_id = strtolower(preg_replace("/[^a-zA-Z0-9_-]/", "_", Sf($_POST["name"] ?? "")));
$aulario_data = [
"name" => Sf($_POST["name"] ?? ""),
"icon" => Sf($_POST["icon"] ?? "/static/logo-entreaulas.png")
];
// Make path recursive (mkdir -p equivalent)
@mkdir("/DATA/entreaulas/Centros/$centro_id/Aularios/", 0777, true);
@mkdir("/DATA/entreaulas/Centros/$centro_id/Aularios/$aulario_id/Proyectos/", 0777, true);
file_put_contents("/DATA/entreaulas/Centros/$centro_id/Aularios/$aulario_id.json", json_encode($aulario_data));
// Update user data
$_SESSION["auth_data"]["entreaulas"]["aulas"][] = $aulario_id;
db()->prepare(
"INSERT OR IGNORE INTO aularios (centro_id, aulario_id, name, icon) VALUES (?, ?, ?, ?)"
)->execute([
$centro_id, $aulario_id,
Sf($_POST["name"] ?? ""),
Sf($_POST["icon"] ?? "/static/logo-entreaulas.png"),
]);
// Create Alumnos directory for photo-based features
@mkdir("/DATA/entreaulas/Centros/$centro_id/Aularios/$aulario_id/Proyectos/", 0755, true);
header("Location: ?action=index");
exit();
break;
case "save_edit":
$aulario_id = safe_path_segment(Sf($_POST["aulario_id"] ?? ""));
$centro_id = safe_path_segment(Sf($_POST["centro_id"] ?? ""));
$aulario_file = "/DATA/entreaulas/Centros/$centro_id/Aularios/$aulario_id.json";
if (!file_exists($aulario_file)) {
$centro_id = safe_path_segment(Sf($_POST["centro_id"] ?? ""));
if ($aulario_id === "" || $centro_id === "") {
die("Parámetros inválidos.");
}
// Fetch existing extra data
$existing = db_get_aulario($centro_id, $aulario_id);
if ($existing === null) {
die("Aulario no encontrado.");
}
$aulario_data = json_decode(file_get_contents($aulario_file), true);
$aulario_data["name"] = Sf($_POST["name"] ?? "");
$aulario_data["icon"] = Sf($_POST["icon"] ?? "/static/logo-entreaulas.png");
// Handle shared comedor configuration
$share_comedor_from = safe_path_segment(Sf($_POST["share_comedor_from"] ?? ""));
if (!empty($share_comedor_from) && $share_comedor_from !== "none") {
$aulario_data["shared_comedor_from"] = $share_comedor_from;
} else {
unset($aulario_data["shared_comedor_from"]);
}
// Handle linked projects configuration
$linked_projects = [];
$linked_aularios = $_POST["linked_aulario"] ?? [];
$linked_project_ids = $_POST["linked_project_id"] ?? [];
$linked_permissions = $_POST["linked_permission"] ?? [];
for ($i = 0; $i < count($linked_aularios); $i++) {
$src_aul = safe_path_segment($linked_aularios[$i] ?? "");
$proj_id = safe_path_segment($linked_project_ids[$i] ?? "");
$perm = in_array(($linked_permissions[$i] ?? "read_only"), ["read_only", "request_edit", "full_edit"], true)
? ($linked_permissions[$i] ?? "read_only")
: "read_only";
if (!empty($src_aul) && !empty($proj_id)) {
$linked_projects[] = [
"source_aulario" => $src_aul,
"project_id" => $proj_id,
"permission" => $perm
];
// Build extra JSON preserving any existing extra fields
$extra_skip = ['name', 'icon'];
$extra = [];
foreach ($existing as $k => $v) {
if (!in_array($k, $extra_skip, true)) {
$extra[$k] = $v;
}
}
if (count($linked_projects) > 0) {
$aulario_data["linked_projects"] = $linked_projects;
} else {
unset($aulario_data["linked_projects"]);
// Update shared_comedor_from if posted
if (isset($_POST['shared_comedor_from'])) {
$extra['shared_comedor_from'] = Sf($_POST['shared_comedor_from']);
}
@mkdir("/DATA/entreaulas/Centros/$centro_id/Aularios/$aulario_id/Proyectos/", 0777, true);
file_put_contents($aulario_file, json_encode($aulario_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
header("Location: ?action=edit&aulario=" . urlencode($aulario_id) . "&centro=" . urlencode($centro_id) . "&saved=1");
db()->prepare(
"UPDATE aularios SET name = ?, icon = ?, extra = ? WHERE centro_id = ? AND aulario_id = ?"
)->execute([
Sf($_POST["name"] ?? ""),
Sf($_POST["icon"] ?? "/static/logo-entreaulas.png"),
json_encode($extra),
$centro_id,
$aulario_id,
]);
header("Location: ?action=edit&aulario=" . urlencode($aulario_id) . "&centro=" . urlencode($centro_id) . "&_result=" . urlencode("Cambios guardados."));
exit();
break;
}
require_once "_incl/pre-body.php";
$view_action = $_GET["action"] ?? "index";
switch ($view_action) {
case "new":
?>
require_once "_incl/pre-body.php";
$centro_id = safe_path_segment(Sf($_GET["centro"] ?? ""));
$all_centros = db_get_centro_ids();
?>
<div class="card pad">
<div>
<h1 class="card-title">Nuevo Aulario</h1>
<span>
Aquí puedes crear un nuevo aulario para el centro que administras.
</span>
<h1>Nuevo Aulario</h1>
<form method="post" action="?form=create">
<div class="mb-3">
<label for="centro" class="form-label"><b>Centro:</b></label>
<select required id="centro" name="centro" class="form-select">
<label for="centro" class="form-label">Centro:</label>
<select id="centro" name="centro" class="form-select" required>
<option value="">-- Selecciona un centro --</option>
<?php
foreach (glob("/DATA/entreaulas/Centros/*", GLOB_ONLYDIR) as $centro_folder) {
$centro_id = basename($centro_folder);
$selected = ($centro_id == $_SESSION["auth_data"]["entreaulas"]["centro"]) ? "selected" : "";
echo '<option value="' . htmlspecialchars($centro_id) . '" ' . $selected . '>' . htmlspecialchars($centro_id) . '</option>';
}
?>
<?php foreach ($all_centros as $cid): ?>
<option value="<?= htmlspecialchars($cid) ?>" <?= $cid === $centro_id ? 'selected' : '' ?>><?= htmlspecialchars($cid) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="name" class="form-label">Nombre del Aulario:</label>
<input required type="text" id="name" name="name" class="form-control" placeholder="Ej: Aulario Principal">
<label for="name" class="form-label">Nombre:</label>
<input required type="text" id="name" name="name" class="form-control" placeholder="Ej: Aula 1">
</div>
<div class="mb-3">
<label for="icon" class="form-label">Icono del Aulario (URL):</label>
<input type="text" id="icon" name="icon" class="form-control" placeholder="Ej: https://example.com/icon.png" value="/static/logo-entreaulas.png">
<label for="icon" class="form-label">URL del icono:</label>
<input type="url" id="icon" name="icon" class="form-control" value="/static/logo-entreaulas.png">
</div>
<button type="submit" class="btn btn-primary">Crear Aulario</button>
</form>
</div>
</div>
<?php
require_once "_incl/post-body.php";
break;
case "edit":
require_once "_incl/pre-body.php";
$aulario_id = safe_path_segment(Sf($_GET["aulario"] ?? ""));
$centro_id = safe_path_segment(Sf($_GET["centro"] ?? ""));
$aulario_file = "/DATA/entreaulas/Centros/$centro_id/Aularios/$aulario_id.json";
if (!file_exists($aulario_file)) {
$centro_id = safe_path_segment(Sf($_GET["centro"] ?? ""));
$aulario = db_get_aulario($centro_id, $aulario_id);
if (!$aulario) {
die("Aulario no encontrado.");
}
$aulario_data = json_decode(file_get_contents($aulario_file), true);
// Get all aularios from the same centro for sharing options
$available_aularios = [];
$aularios_files = glob("/DATA/entreaulas/Centros/$centro_id/Aularios/*.json");
foreach ($aularios_files as $aul_file) {
$aul_id = basename($aul_file, ".json");
if ($aul_id !== $aulario_id) { // Don't allow sharing from itself
$aul_data = json_decode(file_get_contents($aul_file), true);
$available_aularios[$aul_id] = $aul_data['name'] ?? $aul_id;
}
}
// Get available projects from other aularios
$available_projects_by_aulario = [];
foreach ($available_aularios as $aul_id => $aul_name) {
$proj_dir = "/DATA/entreaulas/Centros/$centro_id/Aularios/$aul_id/Proyectos";
if (is_dir($proj_dir)) {
$projects = [];
$files = glob("$proj_dir/*.json");
foreach ($files as $file) {
$proj_data = json_decode(file_get_contents($file), true);
// Only include root projects (no parent)
if ($proj_data && ($proj_data["parent_id"] ?? null) === null) {
$projects[] = [
"id" => $proj_data["id"] ?? basename($file, ".json"),
"name" => $proj_data["name"] ?? "Sin nombre"
];
}
}
if (count($projects) > 0) {
$available_projects_by_aulario[$aul_id] = $projects;
}
}
}
$other_aularios = db_get_aularios($centro_id);
?>
<?php if (isset($_GET['saved'])): ?>
<div class="alert alert-success">Cambios guardados correctamente.</div>
<?php endif; ?>
<div class="card pad">
<div>
<h1 class="card-title">Editar Aulario: <?php echo htmlspecialchars($aulario_data['name'] ?? 'Sin Nombre'); ?></h1>
<h1>Aulario: <?= htmlspecialchars($aulario['name'] ?? $aulario_id) ?></h1>
<form method="post" action="?form=save_edit">
<input type="hidden" name="aulario_id" value="<?= htmlspecialchars($aulario_id) ?>">
<input type="hidden" name="centro_id" value="<?= htmlspecialchars($centro_id) ?>">
<div class="mb-3">
<label for="name" class="form-label">Nombre del Aulario:</label>
<input required type="text" id="name" name="name" class="form-control" value="<?php echo htmlspecialchars($aulario_data['name'] ?? ''); ?>">
<label for="name" class="form-label">Nombre:</label>
<input required type="text" id="name" name="name" class="form-control" value="<?= htmlspecialchars($aulario['name'] ?? '') ?>">
</div>
<div class="mb-3">
<label for="icon" class="form-label">Icono del Aulario (URL):</label>
<input type="text" id="icon" name="icon" class="form-control" value="<?php echo htmlspecialchars($aulario_data['icon'] ?? '/static/iconexperience/blackboard.png'); ?>">
<label for="icon" class="form-label">URL del icono:</label>
<input type="text" id="icon" name="icon" class="form-control" value="<?= htmlspecialchars($aulario['icon'] ?? '') ?>">
</div>
<hr>
<h3>Compartir Menú Comedor</h3>
<p class="text-muted">Configura desde qué aulario compartir los datos del menú comedor. Si se selecciona un aulario origen, este aulario mostrará los menús del aulario seleccionado en lugar de los propios.</p>
<div class="mb-3">
<label for="share_comedor_from" class="form-label">Menú Comedor - Compartir desde:</label>
<select id="share_comedor_from" name="share_comedor_from" class="form-select">
<option value="none">No compartir (usar datos propios)</option>
<?php foreach ($available_aularios as $aul_id => $aul_name): ?>
<option value="<?php echo htmlspecialchars($aul_id); ?>"
<?php echo ($aulario_data['shared_comedor_from'] ?? '') === $aul_id ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($aul_name); ?>
</option>
<label for="shared_comedor_from" class="form-label">Compartir comedor de:</label>
<select id="shared_comedor_from" name="shared_comedor_from" class="form-select">
<option value="">-- Sin compartir --</option>
<?php foreach ($other_aularios as $aid => $adata): if ($aid === $aulario_id) continue; ?>
<option value="<?= htmlspecialchars($aid) ?>" <?= ($aulario['shared_comedor_from'] ?? '') === $aid ? 'selected' : '' ?>><?= htmlspecialchars($adata['name'] ?? $aid) ?></option>
<?php endforeach; ?>
</select>
</div>
<hr>
<h3>Proyectos Enlazados</h3>
<p class="text-muted">Selecciona proyectos raíz específicos de otros aularios para mostrarlos en este aulario. Puedes configurar el nivel de permisos: Solo lectura, Solicitar permiso para cambiar, o Cambiar sin solicitar.</p>
<div id="linked-projects-container">
<?php
$existing_links = $aulario_data['linked_projects'] ?? [];
if (count($existing_links) === 0) {
// Show one empty row
$existing_links = [["source_aulario" => "", "project_id" => "", "permission" => "read_only"]];
}
foreach ($existing_links as $idx => $link):
$source_aul = $link['source_aulario'] ?? '';
$proj_id = $link['project_id'] ?? '';
$permission = $link['permission'] ?? 'read_only';
?>
<div class="row mb-2 linked-project-row">
<div class="col-md-4">
<select name="linked_aulario[]" class="form-select linked-aulario-select" data-row="<?php echo $idx; ?>">
<option value="">-- Seleccionar aulario origen --</option>
<?php foreach ($available_aularios as $aul_id => $aul_name): ?>
<option value="<?php echo htmlspecialchars($aul_id); ?>"
<?php echo $source_aul === $aul_id ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($aul_name); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<select name="linked_project_id[]" class="form-select linked-project-select" data-row="<?php echo $idx; ?>">
<option value="">-- Seleccionar proyecto --</option>
<?php if (!empty($source_aul) && isset($available_projects_by_aulario[$source_aul])): ?>
<?php foreach ($available_projects_by_aulario[$source_aul] as $proj): ?>
<option value="<?php echo htmlspecialchars($proj['id']); ?>"
<?php echo $proj_id === $proj['id'] ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($proj['name']); ?>
</option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</div>
<div class="col-md-3">
<select name="linked_permission[]" class="form-select">
<option value="read_only" <?php echo $permission === 'read_only' ? 'selected' : ''; ?>>Solo lectura</option>
<option value="request_edit" <?php echo $permission === 'request_edit' ? 'selected' : ''; ?>>Solicitar permiso para cambiar</option>
<option value="full_edit" <?php echo $permission === 'full_edit' ? 'selected' : ''; ?>>Cambiar sin solicitar</option>
</select>
</div>
<div class="col-md-2">
<button type="button" class="btn btn-danger remove-link-btn" onclick="removeLinkedProject(this)">Eliminar</button>
</div>
</div>
<?php endforeach; ?>
</div>
<button type="button" class="btn btn-secondary mb-3" onclick="addLinkedProject()">+ Añadir Proyecto Enlazado</button>
<script>
// Store available projects data
const availableProjects = <?php echo json_encode($available_projects_by_aulario); ?>;
let rowCounter = <?php echo count($existing_links); ?>;
function addLinkedProject() {
const container = document.getElementById('linked-projects-container');
const newRow = document.createElement('div');
newRow.className = 'row mb-2 linked-project-row';
newRow.innerHTML = `
<div class="col-md-4">
<select name="linked_aulario[]" class="form-select linked-aulario-select" data-row="${rowCounter}">
<option value="">-- Seleccionar aulario origen --</option>
<?php foreach ($available_aularios as $aul_id => $aul_name): ?>
<option value="<?php echo htmlspecialchars($aul_id); ?>">
<?php echo htmlspecialchars($aul_name); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<select name="linked_project_id[]" class="form-select linked-project-select" data-row="${rowCounter}">
<option value="">-- Seleccionar proyecto --</option>
</select>
</div>
<div class="col-md-3">
<select name="linked_permission[]" class="form-select">
<option value="read_only">Solo lectura</option>
<option value="request_edit">Solicitar permiso para cambiar</option>
<option value="full_edit">Cambiar sin solicitar</option>
</select>
</div>
<div class="col-md-2">
<button type="button" class="btn btn-danger remove-link-btn" onclick="removeLinkedProject(this)">Eliminar</button>
</div>
`;
container.appendChild(newRow);
// Attach change event to new aulario select
const newAularioSelect = newRow.querySelector('.linked-aulario-select');
newAularioSelect.addEventListener('change', updateProjectOptions);
rowCounter++;
}
function removeLinkedProject(btn) {
btn.closest('.linked-project-row').remove();
}
function updateProjectOptions(event) {
const aularioSelect = event.target;
const rowId = aularioSelect.dataset.row;
const projectSelect = document.querySelector(`.linked-project-select[data-row="${rowId}"]`);
const selectedAulario = aularioSelect.value;
// Clear project options
projectSelect.innerHTML = '<option value="">-- Seleccionar proyecto --</option>';
// Add new options
if (selectedAulario && availableProjects[selectedAulario]) {
availableProjects[selectedAulario].forEach(proj => {
const option = document.createElement('option');
option.value = proj.id;
option.textContent = proj.name;
projectSelect.appendChild(option);
});
}
}
// Attach event listeners to existing selects
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.linked-aulario-select').forEach(select => {
select.addEventListener('change', updateProjectOptions);
});
});
</script>
<input type="hidden" name="aulario_id" value="<?php echo htmlspecialchars($aulario_id); ?>">
<input type="hidden" name="centro_id" value="<?php echo htmlspecialchars($centro_id); ?>">
<button type="submit" class="btn btn-primary">Guardar Cambios</button>
</form>
<form method="post" action="?form=delete" style="display: inline;">
<input type="hidden" name="aulario_id" value="<?php echo htmlspecialchars($aulario_id); ?>">
<input type="hidden" name="centro_id" value="<?php echo htmlspecialchars($centro_id); ?>">
<button type="submit" class="btn btn-danger" onclick="return confirm('¿Estás seguro de que deseas eliminar este aulario? Esta acción no se puede deshacer.')">Eliminar Aulario</button>
<hr>
<form method="post" action="?form=delete" onsubmit="return confirm('¿Eliminar este aulario? Se borrarán todos sus datos.')">
<input type="hidden" name="aulario_id" value="<?= htmlspecialchars($aulario_id) ?>">
<input type="hidden" name="centro_id" value="<?= htmlspecialchars($centro_id) ?>">
<button type="submit" class="btn btn-danger">Eliminar Aulario</button>
</form>
</div>
</div>
<?php
require_once "_incl/post-body.php";
break;
case "index":
default:
require_once "_incl/pre-body.php";
$all_centros = db_get_centros();
?>
<div class="card pad">
<div>
<h1 class="card-title">Gestión de Aularios</h1>
<span>
Desde esta sección puedes administrar los aularios asociados al centro que estás administrando.
</span>
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Icono</th>
<th>Nombre</th>
<th>
<a href="?action=new" class="btn btn-success">+ Nuevo</a>
</th>
</tr>
</thead>
<tbody>
<?php
$user_data = $_SESSION["auth_data"];
$centro_filter = safe_path_segment(Sf($_GET['centro'] ?? ""));
if ($centro_filter !== "") {
$aulas_filelist = glob("/DATA/entreaulas/Centros/$centro_filter/Aularios/*.json") ?: [];
} else {
$aulas_filelist = glob("/DATA/entreaulas/Centros/*/Aularios/*.json") ?: [];
}
foreach ($aulas_filelist as $aula_file) {
$aula_data = json_decode(file_get_contents($aula_file), true);
$centro_id = basename(dirname(dirname($aula_file)));
echo '<tr>';
echo '<td><img src="' . htmlspecialchars($aula_data['icon'] ?? '/static/logo-entreaulas.png') . '" alt="Icono" style="height: 50px;"></td>';
echo '<td>' . htmlspecialchars($aula_data['name'] ?? 'Sin Nombre') . '<br><small>' . $centro_id . '</small></td>';
echo '<td><a href="?action=edit&aulario=' . urlencode(basename($aula_file, ".json")) . '&centro=' . urlencode($centro_id) . '" class="btn btn-primary">Gestionar</a></td>';
echo '</tr>';
}
?>
</tbody>
</table>
<h1>Gestión de Aularios</h1>
<?php foreach ($all_centros as $c): ?>
<?php $aularios = db_get_aularios($c['centro_id']); ?>
<h2><?= htmlspecialchars($c['centro_id']) ?></h2>
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Icono</th><th>Nombre</th>
<th><a href="?action=new&centro=<?= urlencode($c['centro_id']) ?>" class="btn btn-success">+ Nuevo</a></th>
</tr>
</thead>
<tbody>
<?php foreach ($aularios as $aid => $adata): ?>
<tr>
<td><img src="<?= htmlspecialchars($adata['icon'] ?: '/static/logo-entreaulas.png') ?>" style="height: 50px;"></td>
<td><?= htmlspecialchars($adata['name'] ?: $aid) ?></td>
<td><a href="?action=edit&aulario=<?= urlencode($aid) ?>&centro=<?= urlencode($c['centro_id']) ?>" class="btn btn-primary">Editar</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endforeach; ?>
</div>
</div>
<?php
require_once "_incl/post-body.php";
break;
}
require_once "_incl/post-body.php"; ?>

View File

@@ -1,10 +1,11 @@
<?php
require_once "_incl/auth_redir.php";
require_once "../_incl/tools.security.php";
require_once "../_incl/db.php";
function safe_path_segment($value)
{
$value = trim((string)$value);
$value = trim((string) $value);
$value = str_replace(["\0", "/", "\\"], "", $value);
$value = str_replace("..", "", $value);
$value = basename($value);
@@ -21,25 +22,34 @@ switch ($form_action) {
if (empty($centro_id)) {
die("Nombre del centro no proporcionado.");
}
$centro_path = "/DATA/entreaulas/Centros/$centro_id";
if (is_dir($centro_path)) {
// Check uniqueness in DB
$existing = db()->prepare("SELECT id FROM centros WHERE centro_id = ?");
$existing->execute([$centro_id]);
if ($existing->fetch()) {
die("El centro ya existe.");
}
mkdir($centro_path, 0777, true);
// Create DB record
db()->prepare("INSERT INTO centros (centro_id) VALUES (?)")->execute([$centro_id]);
// Keep filesystem directory for activity photos (Panel/Actividades)
$centro_path = "/DATA/entreaulas/Centros/$centro_id";
if (!is_dir($centro_path)) {
mkdir($centro_path, 0755, true);
}
header("Location: ?action=index");
exit();
break;
case "create_activity":
ini_set('memory_limit', '512M');
ini_set("display_errors", 1);
ini_set('upload_max_filesize', '256M');
ini_set('post_max_size', '256M');
$centro_id = safe_path_segment(Sf($_GET['centro'] ?? ''));
$centro_path = "/DATA/entreaulas/Centros/$centro_id";
if (!is_dir($centro_path)) {
// Validate centro exists in DB
$stmt = db()->prepare("SELECT id FROM centros WHERE centro_id = ?");
$stmt->execute([$centro_id]);
if (!$stmt->fetch()) {
die("Centro no válido.");
}
$activity_name = safe_path_segment(Sf($_POST["name"] ?? ''));
$activity_name = safe_path_segment(Sf($_POST["name"] ?? ''));
if (empty($activity_name)) {
die("Nombre de la actividad no proporcionado.");
}
@@ -47,22 +57,20 @@ switch ($form_action) {
if ($activity_photo === null || $activity_photo["error"] !== UPLOAD_ERR_OK) {
die("Error al subir la foto.");
}
$activity_path = "$centro_path/Panel/Actividades/$activity_name";
$activity_path = "/DATA/entreaulas/Centros/$centro_id/Panel/Actividades/$activity_name";
if (is_dir($activity_path)) {
die("La actividad ya existe.");
}
mkdir($activity_path, 0777, true);
$photo_path = "$activity_path/photo.jpg";
move_uploaded_file($activity_photo["tmp_name"], $photo_path);
mkdir($activity_path, 0755, true);
move_uploaded_file($activity_photo["tmp_name"], "$activity_path/photo.jpg");
header("Location: ?action=edit&centro=" . urlencode($centro_id));
exit();
break;
case "edit_activity":
ini_set('memory_limit', '512M');
ini_set("display_errors", 1);
ini_set('upload_max_filesize', '256M');
ini_set('post_max_size', '256M');
$centro_id = safe_path_segment(Sf($_GET['centro'] ?? ''));
$centro_id = safe_path_segment(Sf($_GET['centro'] ?? ''));
$activity_name = safe_path_segment(Sf($_GET['activity'] ?? ''));
$activity_path = "/DATA/entreaulas/Centros/$centro_id/Panel/Actividades/$activity_name";
if (!is_dir($activity_path)) {
@@ -70,18 +78,17 @@ switch ($form_action) {
}
$activity_photo = $_FILES["file"] ?? null;
if ($activity_photo !== null && $activity_photo["error"] === UPLOAD_ERR_OK) {
$photo_path = "$activity_path/photo.jpg";
move_uploaded_file($activity_photo["tmp_name"], $photo_path);
move_uploaded_file($activity_photo["tmp_name"], "$activity_path/photo.jpg");
}
if (safe_path_segment(Sf($_POST['nombre'] ?? '')) != $activity_name) {
$new_activity_name = safe_path_segment(Sf($_POST['nombre'] ?? ''));
$new_activity_path = "/DATA/entreaulas/Centros/$centro_id/Panel/Actividades/$new_activity_name";
if (is_dir($new_activity_path)) {
$new_name = safe_path_segment(Sf($_POST['nombre'] ?? ''));
if ($new_name !== $activity_name && $new_name !== '') {
$new_path = "/DATA/entreaulas/Centros/$centro_id/Panel/Actividades/$new_name";
if (is_dir($new_path)) {
die("Ya existe una actividad con ese nombre.");
}
rename($activity_path, $new_activity_path);
rename($activity_path, $new_path);
}
header("Location: ?action=edit&centro=" . urlencode($centro_id));;
header("Location: ?action=edit&centro=" . urlencode($centro_id));
exit();
break;
}
@@ -90,7 +97,7 @@ require_once "_incl/pre-body.php";
$view_action = $_GET["action"] ?? "index";
switch ($view_action) {
case "edit_activity":
$centro_id = safe_path_segment(Sf($_GET['centro'] ?? ''));
$centro_id = safe_path_segment(Sf($_GET['centro'] ?? ''));
$activity_name = safe_path_segment(Sf($_GET['activity'] ?? ''));
$activity_path = "/DATA/entreaulas/Centros/$centro_id/Panel/Actividades/$activity_name";
if (!is_dir($activity_path)) {
@@ -99,22 +106,20 @@ switch ($view_action) {
?>
<div class="card pad">
<div>
<h1 class="card-title">Gestión de la Actividad: <?php echo htmlspecialchars($activity_name); ?></h1>
<span>
Desde esta sección puedes administrar la actividad seleccionada del panel del centro <?php echo htmlspecialchars($centro_id); ?>.
</span>
<form method="post" action="?form=edit_activity&centro=<?php echo urlencode($centro_id); ?>&activity=<?php echo urlencode($activity_name); ?>" enctype="multipart/form-data">
<h1 class="card-title">Gestión de la Actividad: <?= htmlspecialchars($activity_name) ?></h1>
<form method="post" action="?form=edit_activity&centro=<?= urlencode($centro_id) ?>&activity=<?= urlencode($activity_name) ?>" enctype="multipart/form-data">
<div class="mb-3">
<label for="nombre" class="form-label">Nombre de la actividad:</label>
<input required type="text" id="nombre" name="nombre" class="form-control" value="<?php echo htmlspecialchars($activity_name); ?>">
<input required type="text" id="nombre" name="nombre" class="form-control" value="<?= htmlspecialchars($activity_name) ?>">
</div>
<div class="mb-3">
<label class="form-label">Foto (pulsa para cambiarla):</label><br>
<div style="width: 200px;">
<label class="dropimage" style="background-image: url('<?php
$image_path = "$activity_path/photo.jpg";
$image_fetchpath = file_exists($image_path) ? "/entreaulas/_filefetch.php?type=panel_actividades&centro=" . urlencode($centro_id) . "&activity=" . urlencode($activity_name) : '/static/logo-entreaulas.png';
echo htmlspecialchars($image_fetchpath);
$img = file_exists("$activity_path/photo.jpg")
? "/entreaulas/_filefetch.php?type=panel_actividades&centro=" . urlencode($centro_id) . "&activity=" . urlencode($activity_name)
: '/static/logo-entreaulas.png';
echo htmlspecialchars($img);
?>');">
<input title="Drop image or click me" type="file" name="file" accept="image/*">
</label>
@@ -128,18 +133,16 @@ switch ($view_action) {
break;
case "new_activity":
$centro_id = safe_path_segment(Sf($_GET['centro'] ?? ''));
$centro_path = "/DATA/entreaulas/Centros/$centro_id";
if (!is_dir($centro_path)) {
$stmt = db()->prepare("SELECT id FROM centros WHERE centro_id = ?");
$stmt->execute([$centro_id]);
if (!$stmt->fetch()) {
die("Centro no válido.");
}
?>
<div class="card pad">
<div>
<h1 class="card-title">Nueva Actividad del Panel</h1>
<span>
Aquí puedes crear una nueva actividad para el panel del centro <?php echo htmlspecialchars($centro_id); ?>.
</span>
<form method="post" action="?form=create_activity&centro=<?php echo urlencode($centro_id); ?>" enctype="multipart/form-data">
<form method="post" action="?form=create_activity&centro=<?= urlencode($centro_id) ?>" enctype="multipart/form-data">
<div class="mb-3">
<label for="name" class="form-label">Nombre de la actividad:</label>
<input required type="text" id="name" name="name" class="form-control" placeholder="Ej: Biblioteca">
@@ -155,13 +158,10 @@ switch ($view_action) {
<?php
break;
case "new":
?>
?>
<div class="card pad">
<div>
<h1 class="card-title">Nuevo Centro</h1>
<span>
Aquí puedes crear un nuevo centro para el sistema.
</span>
<form method="post" action="?form=create">
<div class="mb-3">
<label for="name" class="form-label">ID del centro:</label>
@@ -171,22 +171,21 @@ switch ($view_action) {
</form>
</div>
</div>
<?php
break;
case "edit":
$centro_id = safe_path_segment(Sf($_GET['centro'] ?? ''));
$centro_path = "/DATA/entreaulas/Centros/$centro_id";
if (!is_dir($centro_path)) {
$stmt = db()->prepare("SELECT id FROM centros WHERE centro_id = ?");
$stmt->execute([$centro_id]);
if (!$stmt->fetch()) {
die("Centro no válido.");
}
$aularios = db_get_aularios($centro_id);
$activities = glob("/DATA/entreaulas/Centros/$centro_id/Panel/Actividades/*", GLOB_ONLYDIR) ?: [];
?>
<div class="card pad">
<div>
<h1 class="card-title">Gestión del Centro: <?php echo htmlspecialchars($centro_id); ?></h1>
<span>
Desde esta sección puedes administrar el centro seleccionado.
</span>
<h1 class="card-title">Gestión del Centro: <?= htmlspecialchars($centro_id) ?></h1>
</div>
</div>
<div class="card pad">
@@ -195,25 +194,18 @@ switch ($view_action) {
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Icono</th>
<th>Nombre</th>
<th>
<a href="/sysadmin/aularios.php?action=new&centro=<?php echo urlencode($centro_id); ?>" class="btn btn-success">+ Nuevo</a>
</th>
<th>Icono</th><th>Nombre</th>
<th><a href="/sysadmin/aularios.php?action=new&centro=<?= urlencode($centro_id) ?>" class="btn btn-success">+ Nuevo</a></th>
</tr>
</thead>
<tbody>
<?php
$aulas_filelist = glob("/DATA/entreaulas/Centros/$centro_id/Aularios/*.json");
foreach ($aulas_filelist as $aula_file) {
$aula_data = json_decode(file_get_contents($aula_file), true);
echo '<tr>';
echo '<td><img src="' . htmlspecialchars($aula_data['icon'] ?? '/static/logo-entreaulas.png') . '" alt="Icono" style="height: 50px;"></td>';
echo '<td>' . htmlspecialchars($aula_data['name'] ?? 'Sin Nombre') . '</td>';
echo '<td><a href="/sysadmin/aularios.php?action=edit&aulario=' . urlencode(basename($aula_file, ".json")) . '&centro=' . urlencode($centro_id) . '" class="btn btn-primary">Gestionar</a></td>';
echo '</tr>';
}
?>
<?php foreach ($aularios as $aula_id => $aula): ?>
<tr>
<td><img src="<?= htmlspecialchars($aula['icon'] ?: '/static/logo-entreaulas.png') ?>" alt="Icono" style="height: 50px;"></td>
<td><?= htmlspecialchars($aula['name'] ?: $aula_id) ?></td>
<td><a href="/sysadmin/aularios.php?action=edit&aulario=<?= urlencode($aula_id) ?>&centro=<?= urlencode($centro_id) ?>" class="btn btn-primary">Gestionar</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
@@ -224,64 +216,46 @@ switch ($view_action) {
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Foto</th>
<th>Nombre</th>
<th>
<a href="?action=new_activity&centro=<?php echo urlencode($centro_id); ?>" class="btn btn-success">+ Nuevo</a>
</th>
<th>Foto</th><th>Nombre</th>
<th><a href="?action=new_activity&centro=<?= urlencode($centro_id) ?>" class="btn btn-success">+ Nuevo</a></th>
</tr>
</thead>
<tbody>
<?php
$activities = glob("/DATA/entreaulas/Centros/$centro_id/Panel/Actividades/*", GLOB_ONLYDIR);
foreach ($activities as $activity_path) {
$activity_name = basename($activity_path);
$image_path = "/DATA/entreaulas/Centros/$centro_id/Panel/Actividades/" . basename($activity_name) . "/photo.jpg";
$image_fetchpath = file_exists($image_path) ? "/entreaulas/_filefetch.php?type=panel_actividades&centro=" . urlencode($centro_id) . "&activity=" . urlencode($activity_name) : '/static/logo-entreaulas.png';
echo '<tr>';
echo '<td><img src="' . htmlspecialchars($image_fetchpath) . '" alt="Foto" style="height: 50px;"></td>';
echo '<td>' . htmlspecialchars($activity_name) . '</td>';
echo '<td><a href="?action=edit_activity&centro=' . urlencode($centro_id) . '&activity=' . urlencode($activity_name) . '" class="btn btn-primary">Gestionar</a></td>';
echo '</tr>';
}
?>
<?php foreach ($activities as $ap): ?>
<?php $an = basename($ap); $img_path = "$ap/photo.jpg"; ?>
<tr>
<td><img src="<?= file_exists($img_path) ? htmlspecialchars("/entreaulas/_filefetch.php?type=panel_actividades&centro=" . urlencode($centro_id) . "&activity=" . urlencode($an)) : '/static/logo-entreaulas.png' ?>" style="height: 50px;"></td>
<td><?= htmlspecialchars($an) ?></td>
<td><a href="?action=edit_activity&centro=<?= urlencode($centro_id) ?>&activity=<?= urlencode($an) ?>" class="btn btn-primary">Gestionar</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php
<?php
break;
case "index":
default:
$all_centros = db_get_centros();
?>
<div class="card pad">
<div>
<h1 class="card-title">Gestión de Centros</h1>
<span>
Desde esta sección puedes administrar los centros asociados al sistema.
</span>
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Nombre</th>
<th>
<a href="?action=new" class="btn btn-success">+ Nuevo</a>
</th>
<th>Centro</th>
<th><a href="?action=new" class="btn btn-success">+ Nuevo</a></th>
</tr>
</thead>
<tbody>
<?php
$user_data = $_SESSION["auth_data"];
$centros_filelist = glob("/DATA/entreaulas/Centros/*");
foreach ($centros_filelist as $centro_folder) {
$centro_id = basename($centro_folder);
echo '<tr>';
echo '<td>' . htmlspecialchars($centro_id) . '</td>';
echo '<td><a href="?action=edit&centro=' . urlencode($centro_id) . '" class="btn btn-primary">Gestionar</a></td>';
echo '</tr>';
}
?>
<?php foreach ($all_centros as $c): ?>
<tr>
<td><?= htmlspecialchars($c['centro_id']) ?></td>
<td><a href="?action=edit&centro=<?= urlencode($c['centro_id']) ?>" class="btn btn-primary">Gestionar</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
@@ -290,4 +264,4 @@ switch ($view_action) {
break;
}
require_once "_incl/post-body.php"; ?>
require_once "_incl/post-body.php";

View File

@@ -1,99 +1,90 @@
<?php
require_once "_incl/auth_redir.php";
require_once "../_incl/db.php";
switch ($_GET['form']) {
switch ($_GET['form'] ?? '') {
case "create":
// Handle creation logic here
$invitations = json_decode(file_get_contents("/DATA/Invitaciones_de_usuarios.json"), true) ?? [];
$invitation_code = strtoupper($_POST['invitation_code'] ?? '');
$single_use = isset($_POST['single_use']) ? true : false;
if (isset($invitations[$invitation_code])) {
$code = strtoupper(trim($_POST['invitation_code'] ?? ''));
$single_use = isset($_POST['single_use']);
if (empty($code)) {
header("Location: /sysadmin/invitations.php?action=new&_resultcolor=red&_result=" . urlencode("Código de invitación vacío."));
exit;
}
if (db_get_invitation($code)) {
header("Location: /sysadmin/invitations.php?action=new&_resultcolor=red&_result=" . urlencode("El código de invitación ya existe."));
exit;
}
$invitations[$invitation_code] = [
"active" => true,
"single_use" => $single_use
];
file_put_contents("/DATA/Invitaciones_de_usuarios.json", json_encode($invitations, JSON_PRETTY_PRINT));
header("Location: /sysadmin/invitations.php?_result=" . urlencode("Código $invitation_code creado correctamente."));
db_upsert_invitation($code, true, $single_use);
header("Location: /sysadmin/invitations.php?_result=" . urlencode("Código $code creado correctamente."));
exit;
break;
case "delete":
// Handle deletion logic here
$invitations = json_decode(file_get_contents("/DATA/Invitaciones_de_usuarios.json"), true) ?? [];
$invitation_code = strtoupper($_POST['invitation_code'] ?? '');
if (isset($invitations[$invitation_code])) {
unset($invitations[$invitation_code]);
file_put_contents("/DATA/Invitaciones_de_usuarios.json", json_encode($invitations, JSON_PRETTY_PRINT));
}
header("Location: /sysadmin/invitations.php?_result=" . urlencode("Codigo $invitation_code borrado"));
$code = strtoupper(trim($_POST['invitation_code'] ?? ''));
db_delete_invitation($code);
header("Location: /sysadmin/invitations.php?_result=" . urlencode("Código $code borrado."));
exit;
break;
}
require_once "_incl/pre-body.php";
switch ($_GET['action']) {
switch ($_GET['action'] ?? 'index') {
case "new":
?>
<div class="card pad">
<div>
<h1 class="card-title">Nueva invitación de usuario</h1>
<form method="post" action="?form=create">
<div class="card pad" style="max-width: 500px;">
<div>
<div class="mb-3">
<label for="invitation_code" class="form-label"><b>Código de invitación:</b></label>
<input type="text" id="invitation_code" name="invitation_code" class="form-control" required />
<small>Formato: 123456-ABCDEF</small>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="single_use" id="single_use">
<label class="form-check-label" for="single_use">
Uso único
</label>
</div>
<button type="submit" class="btn btn-primary">Crear invitación</button>
</div>
?>
<div class="card pad">
<div>
<h1 class="card-title">Nueva invitación de usuario</h1>
<form method="post" action="?form=create">
<div class="card pad" style="max-width: 500px;">
<div>
<div class="mb-3">
<label for="invitation_code" class="form-label"><b>Código de invitación:</b></label>
<input type="text" id="invitation_code" name="invitation_code" class="form-control" required />
<small>Formato: 123456-ABCDEF</small>
</div>
</form>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="single_use" id="single_use">
<label class="form-check-label" for="single_use">Uso único</label>
</div>
<button type="submit" class="btn btn-primary">Crear invitación</button>
</div>
</div>
</div>
<?php
</form>
</div>
</div>
<?php
break;
default:
case "index":
?>
<div class="card pad">
<div>
<h1>Invitaciones de usuarios</h1>
<span>Desde aquí puedes gestionar las invitaciones de usuarios.</span>
<table class="table table-striped table-hover">
<thead class="table-dark">
<th>Codigo de invitación</th>
<th>
<a href="?action=new" class="btn btn-success">+ Nuevo</a>
</th>
</thead>
<tbody>
<?php
$invitations = json_decode(file_get_contents("/DATA/Invitaciones_de_usuarios.json"), true);
foreach ($invitations as $inv_key => $inv_data) {
echo "<tr>";
echo "<td>" . htmlspecialchars($inv_key) . "</td>";
echo "<td>";
echo '<form method="post" action="?form=delete" style="display:inline;">';
echo '<input type="hidden" name="invitation_code" value="' . htmlspecialchars($inv_key) . '"/>';
echo '<button type="submit" class="btn btn-danger" onclick="return confirm(\'¿Estás seguro de que deseas eliminar esta invitación?\');">Eliminar</button>';
echo '</form>';
echo "</td>";
echo "</tr>";
}
?>
</tbody>
</table>
</div>
</div>
$invitations = db_get_all_invitations();
?>
<div class="card pad">
<div>
<h1>Invitaciones de usuarios</h1>
<table class="table table-striped table-hover">
<thead class="table-dark">
<th>Código</th>
<th>Activo</th>
<th>Uso único</th>
<th><a href="?action=new" class="btn btn-success">+ Nuevo</a></th>
</thead>
<tbody>
<?php foreach ($invitations as $inv): ?>
<tr>
<td><?= htmlspecialchars($inv['code']) ?></td>
<td><?= $inv['active'] ? 'Sí' : 'No' ?></td>
<td><?= $inv['single_use'] ? 'Sí' : 'No' ?></td>
<td>
<form method="post" action="?form=delete" style="display:inline">
<input type="hidden" name="invitation_code" value="<?= htmlspecialchars($inv['code']) ?>">
<button type="submit" class="btn btn-danger btn-sm">Borrar</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php
break;
}

View File

@@ -1,93 +1,75 @@
<?php
require_once "_incl/auth_redir.php";
require_once "../_incl/tools.security.php";
require_once "../_incl/db.php";
function safe_username($value)
{
$value = basename((string)$value);
$value = preg_replace('/[^a-zA-Z0-9._-]/', '', $value);
if (strpos($value, '..') !== false) {
return '';
}
return $value;
$value = strtolower(basename((string) $value));
$value = preg_replace('/[^a-zA-Z0-9._@-]/', '', $value);
if (strpos($value, '..') !== false) {
return '';
}
return $value;
}
switch ($_GET['form'] ?? '') {
case 'save_password':
$username = safe_username($_POST['username'] ?? '');
$new_password = $_POST['new_password'] ?? '';
$confirm_password = $_POST['confirm_password'] ?? '';
case 'save_password':
$username = safe_username($_POST['username'] ?? '');
$new_password = $_POST['new_password'] ?? '';
$confirm_password = $_POST['confirm_password'] ?? '';
if (empty($username)) {
die("Nombre de usuario no proporcionado.");
}
if (empty($username)) {
die("Nombre de usuario no proporcionado.");
}
if (empty($new_password)) {
die("La contraseña no puede estar vacía.");
}
if ($new_password !== $confirm_password) {
die("Las contraseñas no coinciden.");
}
if (strlen($new_password) < 6) {
die("La contraseña debe tener al menos 6 caracteres.");
}
$row = db_get_user($username);
if (!$row) {
die("Usuario no encontrado.");
}
db()->prepare("UPDATE users SET password_hash = ?, updated_at = datetime('now') WHERE id = ?")
->execute([password_hash($new_password, PASSWORD_DEFAULT), $row['id']]);
if (empty($new_password)) {
die("La contraseña no puede estar vacía.");
}
if ($new_password !== $confirm_password) {
die("Las contraseñas no coinciden.");
}
if (strlen($new_password) < 6) {
die("La contraseña debe tener al menos 6 caracteres.");
}
$userfile = "/DATA/Usuarios/$username.json";
if (!file_exists($userfile)) {
die("Usuario no encontrado.");
}
$userdata = json_decode(file_get_contents($userfile), true);
$userdata['password_hash'] = password_hash($new_password, PASSWORD_DEFAULT);
file_put_contents($userfile, json_encode($userdata, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
header("Location: users.php?action=edit&user=" . urlencode($username) . "&_result=" . urlencode("Contraseña restablecida correctamente a las " . date("H:i:s") . " (hora servidor)."));
exit;
break;
header("Location: users.php?action=edit&user=" . urlencode($username) . "&_result=" . urlencode("Contraseña restablecida correctamente a las " . date("H:i:s") . " (hora servidor)."));
exit;
break;
}
require_once "_incl/pre-body.php";
$username = safe_username($_GET['user'] ?? '');
if (empty($username)) {
die("Usuario no especificado.");
die("Usuario no especificado.");
}
$userfile = "/DATA/Usuarios/$username.json";
if (!file_exists($userfile)) {
die("Usuario no encontrado.");
$row = db_get_user($username);
if (!$row) {
die("Usuario no encontrado.");
}
$userdata = json_decode(file_get_contents($userfile), true);
?>
<form method="post" action="?form=save_password">
<div class="card pad">
<div>
<h1>Restablecer Contraseña: <?php echo htmlspecialchars($username); ?></h1>
<h1>Restablecer Contraseña: <?= htmlspecialchars($username) ?></h1>
<div class="mb-3">
<label for="new_password" class="form-label">Nueva Contraseña:</label>
<input type="password" id="new_password" name="new_password" class="form-control" required minlength="6">
<small class="form-text text-muted">Mínimo 6 caracteres</small>
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label">Confirmar Contraseña:</label>
<input type="password" id="confirm_password" name="confirm_password" class="form-control" required minlength="6">
</div>
<input type="hidden" name="username" value="<?php echo htmlspecialchars($username); ?>">
<input type="hidden" name="username" value="<?= htmlspecialchars($username) ?>">
<button type="submit" class="btn btn-primary">Restablecer Contraseña</button>
<a href="users.php?action=edit&user=<?php echo urlencode($username); ?>" class="btn btn-secondary">Cancelar</a>
<a href="users.php?action=edit&user=<?= urlencode($username) ?>" class="btn btn-secondary">Cancelar</a>
</div>
</div>
</form>
<?php
require_once "_incl/post-body.php";
?>

View File

@@ -1,89 +1,54 @@
<?php
require_once "_incl/auth_redir.php";
require_once "../_incl/tools.security.php";
require_once "../_incl/db.php";
function safe_username($value)
{
$value = basename((string)$value);
$value = preg_replace('/[^a-zA-Z0-9._-]/', '', $value);
if (strpos($value, '..') !== false) {
return '';
}
return $value;
$value = strtolower(basename((string) $value));
$value = preg_replace('/[^a-zA-Z0-9._@-]/', '', $value);
if (strpos($value, '..') !== false) {
return '';
}
return $value;
}
define('USERS_DIR', '/DATA/Usuarios/');
switch ($_GET['form'] ?? '') {
case 'save_edit':
$username = safe_username($_POST['username'] ?? '');
if (empty($username)) {
die("Nombre de usuario no proporcionado.");
}
$user_file = get_user_file_path($username);
$userdata_old = [];
if (is_readable($user_file)) {
$file_contents = file_get_contents($user_file);
if ($file_contents !== false) {
$decoded = json_decode($file_contents, true);
if (is_array($decoded)) {
$userdata_old = $decoded;
case 'save_edit':
$username = safe_username($_POST['username'] ?? '');
if (empty($username)) {
die("Nombre de usuario no proporcionado.");
}
}
}
$permissions = $_POST['permissions'] ?? [];
if (!is_array($permissions)) {
$permissions = [];
}
$permissions = $_POST['permissions'] ?? [];
if (!is_array($permissions)) {
$permissions = [];
}
$aulas = $_POST['aulas'] ?? [];
if (!is_array($aulas)) {
$aulas = [];
}
$aulas = array_values(array_filter(array_map('safe_aulario_id', $aulas)));
$aulas = $_POST['aulas'] ?? [];
if (!is_array($aulas)) {
$aulas = [];
}
$aulas = array_values(array_filter(array_map('safe_aulario_id', $aulas)));
$userdata_new = [
'display_name' => $_POST['display_name'] ?? '',
'email' => $_POST['email'] ?? '',
'permissions' => $permissions,
'entreaulas' => [
'centro' => safe_centro_id($_POST['centro'] ?? ''),
'role' => $_POST['role'] ?? '',
'aulas' => $aulas
]
];
// Merge old and new data to preserve any other fields, like password hashes or custom metadata.
$userdata = array_merge($userdata_old, $userdata_new);
$user_dir = rtrim(USERS_DIR, '/');
$user_file = get_user_file_path($username);
if (!is_dir($user_dir) || !is_writable($user_dir)) {
die("No se puede guardar el usuario: directorio de datos no disponible.");
}
$json_data = json_encode($userdata, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
if ($json_data === false) {
die("No se puede guardar el usuario: error al codificar los datos.");
}
$tmp_file = tempnam($user_dir, 'user_');
if ($tmp_file === false) {
die("No se puede guardar el usuario: no se pudo crear un archivo temporal.");
}
$bytes_written = file_put_contents($tmp_file, $json_data, LOCK_EX);
if ($bytes_written === false) {
@unlink($tmp_file);
die("No se puede guardar el usuario: error al escribir en el disco.");
}
if (!rename($tmp_file, $user_file)) {
@unlink($tmp_file);
die("No se puede guardar el usuario: no se pudo finalizar la grabación del archivo.");
}
header("Location: ?action=edit&user=" . urlencode($username) . "&_result=" . urlencode("Cambios guardados correctamente a las ".date("H:i:s")." (hora servidor)."));
exit;
break;
db_upsert_user([
'username' => $username,
'display_name' => $_POST['display_name'] ?? '',
'email' => $_POST['email'] ?? '',
'permissions' => $permissions,
'entreaulas' => [
'centro' => safe_centro_id($_POST['centro'] ?? ''),
'role' => $_POST['role'] ?? '',
'aulas' => $aulas,
],
]);
header("Location: ?action=edit&user=" . urlencode($username) . "&_result=" . urlencode("Cambios guardados correctamente a las " . date("H:i:s") . " (hora servidor)."));
exit;
break;
}
switch ($_GET['action'] ?? '') {
case 'add':
require_once "_incl/pre-body.php";
case 'add':
require_once "_incl/pre-body.php";
$all_centros = db_get_centro_ids();
?>
<form method="post" action="?form=save_edit">
<div class="card pad">
@@ -104,70 +69,52 @@ switch ($_GET['action'] ?? '') {
<b>Permisos:</b>
<div class="accordion mt-3" id="permissionsAccordion">
<div class="accordion-item">
<h2 class="accordion-header" id="headingSysadmin">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseSysadmin" aria-expanded="true" aria-controls="collapseSysadmin">
Administración del sistema
</button>
<h2 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseSysadmin">Administración del sistema</button>
</h2>
<div id="collapseSysadmin" class="accordion-collapse collapse show" aria-labelledby="headingSysadmin" data-bs-parent="#permissionsAccordion">
<div id="collapseSysadmin" class="accordion-collapse collapse show" data-bs-parent="#permissionsAccordion">
<div class="accordion-body">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="permissions[]" value="sysadmin:access" id="sysadmin-access">
<label class="form-check-label" for="sysadmin-access">
Acceso
</label>
<label class="form-check-label" for="sysadmin-access">Acceso</label>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingEntreaulas">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseEntreaulas" aria-expanded="false" aria-controls="collapseEntreaulas">
EntreAulas
</button>
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseEntreaulas">EntreAulas</button>
</h2>
<div id="collapseEntreaulas" class="accordion-collapse collapse" aria-labelledby="headingEntreaulas" data-bs-parent="#permissionsAccordion">
<div id="collapseEntreaulas" class="accordion-collapse collapse" data-bs-parent="#permissionsAccordion">
<div class="accordion-body">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="permissions[]" value="entreaulas:access" id="entreaulas-access">
<label class="form-check-label" for="entreaulas-access">
Acceso
</label>
<label class="form-check-label" for="entreaulas-access">Acceso</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="permissions[]" value="entreaulas:docente" id="entreaulas-docente">
<label class="form-check-label" for="entreaulas-docente">
Docente
</label>
<label class="form-check-label" for="entreaulas-docente">Docente</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="permissions[]" value="entreaulas:proyectos:delete" id="entreaulas-proyectos-delete">
<label class="form-check-label" for="entreaulas-proyectos-delete">
Eliminar Proyectos
</label>
<label class="form-check-label" for="entreaulas-proyectos-delete">Eliminar Proyectos</label>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingSupercafe">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseSupercafe" aria-expanded="false" aria-controls="collapseSupercafe">
SuperCafe
</button>
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseSupercafe">SuperCafe</button>
</h2>
<div id="collapseSupercafe" class="accordion-collapse collapse" aria-labelledby="headingSupercafe" data-bs-parent="#permissionsAccordion">
<div id="collapseSupercafe" class="accordion-collapse collapse" data-bs-parent="#permissionsAccordion">
<div class="accordion-body">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="permissions[]" value="supercafe:access" id="supercafe-access">
<label class="form-check-label" for="supercafe-access">
Acceso
</label>
<label class="form-check-label" for="supercafe-access">Acceso</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="permissions[]" value="supercafe:edit" id="supercafe-edit">
<label class="form-check-label" for="supercafe-edit">
Editar comandas
</label>
<label class="form-check-label" for="supercafe-edit">Editar comandas</label>
</div>
</div>
</div>
@@ -183,13 +130,9 @@ switch ($_GET['action'] ?? '') {
<label for="centro" class="form-label">Centro asociado:</label>
<select id="centro" name="centro" class="form-select">
<option value="">-- Selecciona un centro --</option>
<?php
$centros_folders_add = glob("/DATA/entreaulas/Centros/*", GLOB_ONLYDIR) ?: [];
foreach ($centros_folders_add as $centro_folder) {
$centro_id_opt = basename($centro_folder);
echo '<option value="' . htmlspecialchars($centro_id_opt) . '">' . htmlspecialchars($centro_id_opt) . '</option>';
}
?>
<?php foreach ($all_centros as $cid): ?>
<option value="<?= htmlspecialchars($cid) ?>"><?= htmlspecialchars($cid) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
@@ -205,112 +148,91 @@ switch ($_GET['action'] ?? '') {
</div>
</form>
<?php
require_once "_incl/post-body.php";
break;
case 'edit':
require_once "_incl/pre-body.php";
$username = safe_username($_GET['user'] ?? '');
if (empty($username)) {
die("Nombre de usuario inválido.");
}
$user_file = get_user_file_path($username);
if (!file_exists($user_file) || !is_readable($user_file)) {
die("Usuario no encontrado o datos no disponibles.");
}
$jsonContent = file_get_contents($user_file);
if ($jsonContent === false) {
die("Error al leer los datos del usuario.");
}
$userdata = json_decode($jsonContent, true);
if (!is_array($userdata) || json_last_error() !== JSON_ERROR_NONE) {
die("Datos de usuario corruptos o con formato inválido.");
}
require_once "_incl/post-body.php";
break;
case 'edit':
require_once "_incl/pre-body.php";
$username = safe_username($_GET['user'] ?? '');
if (empty($username)) {
die("Nombre de usuario inválido.");
}
$row = db_get_user($username);
if (!$row) {
die("Usuario no encontrado.");
}
$userdata = db_build_auth_data($row);
$all_centros = db_get_centro_ids();
$user_centro = safe_centro_id($userdata['entreaulas']['centro'] ?? '');
$aularios = $user_centro !== '' ? db_get_aularios($user_centro) : [];
?>
<form method="post" action="?form=save_edit">
<div class="card pad">
<div>
<h1>Editar Usuario: <?php echo htmlspecialchars($username); ?></h1>
<h1>Editar Usuario: <?= htmlspecialchars($username) ?></h1>
<div class="mb-3">
<label for="display_name" class="form-label">Nombre para mostrar:</label>
<input type="text" id="display_name" name="display_name" value="<?php echo htmlspecialchars($userdata['display_name'] ?? ''); ?>" class="form-control" required>
<input type="text" id="display_name" name="display_name" value="<?= htmlspecialchars($userdata['display_name'] ?? '') ?>" class="form-control" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Correo electrónico:</label>
<input type="email" id="email" name="email" value="<?php echo htmlspecialchars($userdata['email'] ?? ''); ?>" class="form-control" required>
<input type="email" id="email" name="email" value="<?= htmlspecialchars($userdata['email'] ?? '') ?>" class="form-control" required>
</div>
<b>Permisos:</b>
<div class="accordion mt-3" id="permissionsAccordion">
<div class="accordion-item">
<h2 class="accordion-header" id="headingSysadmin">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseSysadmin" aria-expanded="true" aria-controls="collapseSysadmin">
Administración del sistema
</button>
<h2 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseSysadmin">Administración del sistema</button>
</h2>
<div id="collapseSysadmin" class="accordion-collapse collapse show" aria-labelledby="headingSysadmin" data-bs-parent="#permissionsAccordion">
<div id="collapseSysadmin" class="accordion-collapse collapse show" data-bs-parent="#permissionsAccordion">
<div class="accordion-body">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="permissions[]" value="sysadmin:access" id="sysadmin-access" <?php if (in_array('sysadmin:access', $userdata['permissions'] ?? [])) echo 'checked'; ?>>
<label class="form-check-label" for="sysadmin-access">
Acceso
</label>
<input class="form-check-input" type="checkbox" name="permissions[]" value="sysadmin:access" id="sysadmin-access" <?= in_array('sysadmin:access', $userdata['permissions'] ?? []) ? 'checked' : '' ?>>
<label class="form-check-label" for="sysadmin-access">Acceso</label>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingEntreaulas">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseEntreaulas" aria-expanded="false" aria-controls="collapseEntreaulas">
EntreAulas
</button>
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseEntreaulas">EntreAulas</button>
</h2>
<div id="collapseEntreaulas" class="accordion-collapse collapse" aria-labelledby="headingEntreaulas" data-bs-parent="#permissionsAccordion">
<div id="collapseEntreaulas" class="accordion-collapse collapse" data-bs-parent="#permissionsAccordion">
<div class="accordion-body">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="permissions[]" value="entreaulas:access" id="entreaulas-access" <?php if (in_array('entreaulas:access', $userdata['permissions'] ?? [])) echo 'checked'; ?>>
<label class="form-check-label" for="entreaulas-access">
Acceso
</label>
<input class="form-check-input" type="checkbox" name="permissions[]" value="entreaulas:access" id="entreaulas-access" <?= in_array('entreaulas:access', $userdata['permissions'] ?? []) ? 'checked' : '' ?>>
<label class="form-check-label" for="entreaulas-access">Acceso</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="permissions[]" value="entreaulas:docente" id="entreaulas-docente" <?php if (in_array('entreaulas:docente', $userdata['permissions'] ?? [])) echo 'checked'; ?>>
<label class="form-check-label" for="entreaulas-docente">
Docente
</label>
<input class="form-check-input" type="checkbox" name="permissions[]" value="entreaulas:docente" id="entreaulas-docente" <?= in_array('entreaulas:docente', $userdata['permissions'] ?? []) ? 'checked' : '' ?>>
<label class="form-check-label" for="entreaulas-docente">Docente</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="permissions[]" value="entreaulas:proyectos:delete" id="entreaulas-proyectos-delete" <?php if (in_array('entreaulas:proyectos:delete', $userdata['permissions'] ?? [])) echo 'checked'; ?>>
<label class="form-check-label" for="entreaulas-proyectos-delete">
Eliminar Proyectos
</label>
<input class="form-check-input" type="checkbox" name="permissions[]" value="entreaulas:proyectos:delete" id="entreaulas-proyectos-delete" <?= in_array('entreaulas:proyectos:delete', $userdata['permissions'] ?? []) ? 'checked' : '' ?>>
<label class="form-check-label" for="entreaulas-proyectos-delete">Eliminar Proyectos</label>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingSupercafe">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseSupercafe" aria-expanded="false" aria-controls="collapseSupercafe">
SuperCafe
</button>
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseSupercafe">SuperCafe</button>
</h2>
<div id="collapseSupercafe" class="accordion-collapse collapse" aria-labelledby="headingSupercafe" data-bs-parent="#permissionsAccordion">
<div id="collapseSupercafe" class="accordion-collapse collapse" data-bs-parent="#permissionsAccordion">
<div class="accordion-body">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="permissions[]" value="supercafe:access" id="supercafe-access" <?php if (in_array('supercafe:access', $userdata['permissions'] ?? [])) echo 'checked'; ?>>
<label class="form-check-label" for="supercafe-access">
Acceso
</label>
<input class="form-check-input" type="checkbox" name="permissions[]" value="supercafe:access" id="supercafe-access" <?= in_array('supercafe:access', $userdata['permissions'] ?? []) ? 'checked' : '' ?>>
<label class="form-check-label" for="supercafe-access">Acceso</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="permissions[]" value="supercafe:edit" id="supercafe-edit" <?php if (in_array('supercafe:edit', $userdata['permissions'] ?? [])) echo 'checked'; ?>>
<label class="form-check-label" for="supercafe-edit">
Editar comandas
</label>
<input class="form-check-input" type="checkbox" name="permissions[]" value="supercafe:edit" id="supercafe-edit" <?= in_array('supercafe:edit', $userdata['permissions'] ?? []) ? 'checked' : '' ?>>
<label class="form-check-label" for="supercafe-edit">Editar comandas</label>
</div>
</div>
</div>
</div>
</div>
<input type="hidden" name="username" value="<?php echo htmlspecialchars($username); ?>">
<input type="hidden" name="username" value="<?= htmlspecialchars($username) ?>">
<button type="submit" class="btn btn-primary mt-3">Guardar Cambios</button>
</div>
</div>
@@ -320,104 +242,81 @@ switch ($_GET['action'] ?? '') {
<div class="mb-3">
<label for="centro" class="form-label">Centro asociado:</label>
<select id="centro" name="centro" class="form-select" required>
<option value="" <?php if (empty($userdata["entreaulas"]['centro'] ?? '')) echo 'selected'; ?>>-- Selecciona un centro --</option>
<?php
$centros_folders = glob("/DATA/entreaulas/Centros/*", GLOB_ONLYDIR);
foreach ($centros_folders as $centro_folder) {
$centro_id = basename($centro_folder);
echo '<option value="' . htmlspecialchars($centro_id) . '"';
if (($userdata["entreaulas"]['centro'] ?? '') === $centro_id) {
echo ' selected';
}
echo '>' . htmlspecialchars($centro_id) . '</option>';
}
?>
<option value="" <?= empty($user_centro) ? 'selected' : '' ?>>-- Selecciona un centro --</option>
<?php foreach ($all_centros as $cid): ?>
<option value="<?= htmlspecialchars($cid) ?>" <?= $user_centro === $cid ? 'selected' : '' ?>><?= htmlspecialchars($cid) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="role" class="form-label">Rol en EntreAulas:</label>
<select id="role" name="role" class="form-select" required>
<option value="" <?php if (empty($userdata["entreaulas"]['role'] ?? '')) echo 'selected'; ?>>-- Selecciona un rol --</option>
<option value="teacher" <?php if (($userdata["entreaulas"]['role'] ?? '') === 'teacher') echo 'selected'; ?>>Profesor</option>
<option value="student" <?php if (($userdata["entreaulas"]['role'] ?? '') === 'student') echo 'selected'; ?>>Estudiante</option>
<option value="" <?= empty($userdata['entreaulas']['role'] ?? '') ? 'selected' : '' ?>>-- Selecciona un rol --</option>
<option value="teacher" <?= ($userdata['entreaulas']['role'] ?? '') === 'teacher' ? 'selected' : '' ?>>Profesor</option>
<option value="student" <?= ($userdata['entreaulas']['role'] ?? '') === 'student' ? 'selected' : '' ?>>Estudiante</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Aulas asignadas: <small>(Guarda primero para actualizar la lista)</small></label><br>
<?php
$user_centro = safe_centro_id($userdata["entreaulas"]['centro'] ?? '');
$aulas_filelist = $user_centro !== '' ? (glob("/DATA/entreaulas/Centros/" . $user_centro . "/Aularios/*.json") ?: []) : [];
foreach ($aulas_filelist as $aula_file) {
$aula_data = json_decode(file_get_contents($aula_file), true);
$aula_id = safe_aulario_id(basename($aula_file, ".json"));
$is_assigned = in_array($aula_id, $userdata["entreaulas"]['aulas'] ?? []);
echo '<div class="form-check form-check-inline">';
echo '<input class="form-check-input" type="checkbox" name="aulas[]" value="' . htmlspecialchars($aula_id) . '" id="aula-' . htmlspecialchars($aula_id) . '" ' . ($is_assigned ? 'checked' : '') . '>';
echo '<label class="form-check-label" for="aula-' . htmlspecialchars($aula_id) . '">' . htmlspecialchars($aula_data['name'] ?? $aula_id) . '</label>';
echo '</div>';
}
?>
<?php foreach ($aularios as $aula_id => $aula_data): ?>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="aulas[]"
value="<?= htmlspecialchars($aula_id) ?>"
id="aula-<?= htmlspecialchars($aula_id) ?>"
<?= in_array($aula_id, $userdata['entreaulas']['aulas'] ?? []) ? 'checked' : '' ?>>
<label class="form-check-label" for="aula-<?= htmlspecialchars($aula_id) ?>">
<?= htmlspecialchars($aula_data['name'] ?? $aula_id) ?>
</label>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<div class="card pad">
<div>
<h2>Cambiar contraseña</h2>
<p>Para cambiar la contraseña de este usuario, utiliza la herramienta de restablecimiento de contraseñas disponible en el siguiente enlace:</p>
<a href="/sysadmin/reset_password.php?user=<?php echo urlencode($username); ?>" class="btn btn-secondary">Restablecer Contraseña</a>
<a href="/sysadmin/reset_password.php?user=<?= urlencode($username) ?>" class="btn btn-secondary">Restablecer Contraseña</a>
</div>
</div>
</form>
<?php
require_once "_incl/post-body.php";
break;
case "index":
default:
require_once "_incl/pre-body.php";
?>
<div class="card pad">
<div>
<h1>Gestión de Usuarios</h1>
<p>Desde esta sección puedes gestionar los usuarios del sistema. Puedes agregar, editar o eliminar usuarios según sea necesario.</p>
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Usuario</th>
<th>Nombre</th>
<th>Correo</th>
<th>
<a href="?action=add" class="btn btn-success">+ Nuevo</a>
</th>
</tr>
</thead>
<tbody>
<?php
$users_filelist = glob(USERS_DIR . '*.json') ?: [];
foreach ($users_filelist as $user_file) {
$userdata = json_decode(file_get_contents($user_file), true);
if (!is_array($userdata)) {
error_log("users.php: corrupted or unreadable user file: $user_file");
$userdata = [];
}
// Username is the filename without path and extension
$username = basename($user_file, ".json");
echo "<tr>";
echo "<td>" . htmlspecialchars($username) . "</td>";
echo "<td>" . htmlspecialchars($userdata['display_name'] ?? 'N/A') . "</td>";
echo "<td>" . htmlspecialchars($userdata['email'] ?? 'N/A') . "</td>";
echo "<td>";
echo '<a href="?action=edit&user=' . urlencode($username) . '" class="btn btn-primary">Editar</a> ';
echo '<a href="?action=delete&user=' . urlencode($username) . '" class="btn btn-danger">Eliminar</a>';
echo "</td>";
echo "</tr>";
}
?>
</tbody>
</table>
</div>
</div>
<?php
require_once "_incl/post-body.php";
break;
}
require_once "_incl/post-body.php";
break;
case 'index':
default:
require_once "_incl/pre-body.php";
$all_users = db_get_all_users();
?>
<div class="card pad">
<div>
<h1>Gestión de Usuarios</h1>
<p>Desde esta sección puedes gestionar los usuarios del sistema.</p>
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Usuario</th>
<th>Nombre</th>
<th>Correo</th>
<th><a href="?action=add" class="btn btn-success">+ Nuevo</a></th>
</tr>
</thead>
<tbody>
<?php foreach ($all_users as $u): ?>
<tr>
<td><?= htmlspecialchars($u['username']) ?></td>
<td><?= htmlspecialchars($u['display_name'] ?: 'N/A') ?></td>
<td><?= htmlspecialchars($u['email'] ?: 'N/A') ?></td>
<td>
<a href="?action=edit&user=<?= urlencode($u['username']) ?>" class="btn btn-primary">Editar</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php
require_once "_incl/post-body.php";
break;
}