Merge pull request #16 from Axia4/copilot/make-app-feel-integrated

Redesign UI to match Google Workspace integrated feel + SQLite DB with migrations, multi-tenant account management
This commit is contained in:
Naiel
2026-03-07 13:34:58 +01:00
committed by GitHub
47 changed files with 3717 additions and 2601 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

View File

@@ -1,6 +1,6 @@
# Axia4
Axia4 is a unified platform for EuskadiTech and Sketaria, providing various services including EntreAulas (connected classroom management system).
Axia4 is a unified platform for EuskadiTech and Sketaria, providing various services including AulaTek (connected classroom management system).
## Quick Start with Docker
@@ -13,7 +13,7 @@ cd Axia4
# 2. Create the data directory structure
mkdir -p DATA/entreaulas/Usuarios
mkdir -p DATA/entreaulas/Centros
mkdir -p DATA/entreaulas/Organizaciones
# 3. Start the application
docker compose up -d
@@ -29,7 +29,7 @@ docker compose up -d
## Features
- **EntreAulas**: Management system for connected classrooms
- **AulaTek**: Management system for connected classrooms
- **Aularios**: Centralized access to classroom resources
- Integration with multiple external services

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

@@ -0,0 +1,765 @@
<?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.sqlite.
*
* Usage: db() → returns the shared PDO instance (auto-migrates on first call).
*/
define('DB_PATH', '/DATA/axia4.sqlite');
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.active_organizations auth_data.organizations.
*/
function db_build_auth_data(array $row): array
{
$permissions = json_decode($row['permissions'] ?? '[]', true) ?: [];
$meta = json_decode($row['meta'] ?? '{}', true) ?: [];
$ea = [
'organization' => '',
'organizations' => [],
'role' => '',
'aulas' => [],
'organizations_data' => [],
];
// Fetch all organization assignments for this user
$stmt = db()->prepare(
'SELECT org_id, role, ea_aulas
FROM user_orgs
WHERE user_id = ?
ORDER BY org_id'
);
$stmt->execute([$row['id']]);
$org_rows = $stmt->fetchAll();
$orgs = [];
if (!empty($org_rows)) {
$first = $org_rows[0];
foreach ($org_rows as $r) {
$orgs[] = $r['org_id'];
}
$ea['organization'] = $first['org_id'];
$ea['role'] = $first['role'];
$ea['aulas'] = json_decode($first['ea_aulas'] ?? '[]', true) ?: [];
$ea['organizations'] = $orgs;
$ea['organizations_data'] = $org_rows;
}
$active_org = $ea['organization'] ?? '';
$aulatek = [
'organizacion' => $active_org,
'organizaciones' => $orgs,
'organization' => $active_org,
'organizations' => $orgs,
'centro' => $active_org,
'centros' => $orgs,
'role' => $ea['role'] ?? '',
'aulas' => $ea['aulas'] ?? [],
];
return array_merge($meta, [
'display_name' => $row['display_name'],
'email' => $row['email'],
'password_hash' => $row['password_hash'],
'permissions' => $permissions,
'orgs' => $orgs,
'organizations' => $orgs,
'active_organization' => $active_org,
'active_organizations' => $ea,
'aulatek' => $aulatek,
'entreaulas' => $aulatek,
'google_auth' => (bool) $row['google_auth'],
]);
}
/**
* Create or update a user.
* $data keys: username, display_name, email, password_hash, permissions[],
* google_auth, entreaulas{organizacion,organizaciones[],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',
'orgs', 'organizations', 'organization', 'organizacion',
'role', 'aulas'];
$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 organization assignments if tenant data is provided.
$has_org_payload = array_key_exists('entreaulas', $data)
|| array_key_exists('orgs', $data)
|| array_key_exists('organizations', $data)
|| array_key_exists('organization', $data)
|| array_key_exists('organizacion', $data);
if ($has_org_payload) {
$ea = $data['entreaulas'] ?? [];
$organizations = [];
$candidate_lists = [
$data['organizations'] ?? null,
$data['orgs'] ?? null,
$ea['organizaciones'] ?? null,
$ea['organizations'] ?? null,
$ea['centros'] ?? null,
];
foreach ($candidate_lists as $list) {
if (is_array($list) && !empty($list)) {
$organizations = $list;
break;
}
}
if (empty($organizations)) {
foreach ([
$data['organization'] ?? null,
$data['organizacion'] ?? null,
$ea['organizacion'] ?? null,
$ea['organization'] ?? null,
$ea['centro'] ?? null,
] as $single) {
if (!empty($single)) {
$organizations = [$single];
break;
}
}
}
$organizations = array_values(array_unique(array_filter(array_map(
static function ($value): string {
return preg_replace('/[^a-zA-Z0-9._-]/', '', (string) $value);
},
$organizations
))));
$role = (string) ($data['role'] ?? $ea['role'] ?? '');
$aulas_payload = $data['aulas'] ?? $ea['aulas'] ?? [];
if (!is_array($aulas_payload)) {
$aulas_payload = [];
}
$aulas = json_encode($aulas_payload, JSON_UNESCAPED_UNICODE);
$pdo->prepare('DELETE FROM user_orgs WHERE user_id = ?')->execute([$user_id]);
$ins_org = $pdo->prepare('INSERT OR IGNORE INTO organizaciones (org_id, org_name) VALUES (?, ?)');
$ins_uo = $pdo->prepare(
'INSERT OR REPLACE INTO user_orgs (user_id, org_id, role, ea_aulas) VALUES (?, ?, ?, ?)'
);
foreach ($organizations as $org_id) {
if ($org_id === '') {
continue;
}
$ins_org->execute([$org_id, $org_id]);
$ins_uo->execute([$user_id, $org_id, $role, $aulas]);
}
}
return $user_id;
}
/** Delete a user and their organization assignments. */
function db_delete_user(string $username): void
{
db()->prepare('DELETE FROM users WHERE username = ?')->execute([strtolower($username)]);
}
// ── Organization helpers ─────────────────────────────────────────────────────
function db_get_organizations(): array
{
return db()->query('SELECT org_id, org_name FROM organizaciones ORDER BY org_id')->fetchAll();
}
function db_get_organization_ids(): array
{
return db()->query('SELECT org_id FROM organizaciones ORDER BY org_id')->fetchAll(PDO::FETCH_COLUMN);
}
function db_get_organizaciones(): array
{
return db_get_organizations();
}
function get_organizations(): array
{
return db_get_organizations();
}
function db_get_centros(): array
{
$rows = db_get_organizations();
return array_map(static function (array $row): array {
return [
'centro_id' => $row['org_id'],
'name' => $row['org_name'],
];
}, $rows);
}
function db_get_centro_ids(): array
{
return db_get_organization_ids();
}
// ── 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 org_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 org_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 org_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 (org_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 org_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 org_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 (org_id, order_ref, fecha, persona, comanda, notas, estado)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(org_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 org_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 org_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 org_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 (org_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 org_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 (org_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 org_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 (org_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 org_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 (org_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 organization IDs the authenticated user belongs to. */
function get_user_organizations(?array $auth_data = null): array
{
$data = $auth_data ?? $_SESSION['auth_data'] ?? [];
$orgs = $data['organizations']
?? $data['orgs']
?? $data['aulatek']['organizaciones']
?? $data['aulatek']['organizations']
?? $data['aulatek']['centros']
?? $data['entreaulas']['organizaciones']
?? $data['entreaulas']['organizations']
?? $data['entreaulas']['centros']
?? [];
if (!empty($orgs) && is_array($orgs)) {
return array_values(array_unique(array_filter($orgs, static function ($value): bool {
return is_string($value) && $value !== '';
})));
}
if (!empty($orgs)) {
return [(string) $orgs];
}
foreach ([
$data['active_organization'] ?? null,
$data['aulatek']['organizacion'] ?? null,
$data['aulatek']['organization'] ?? null,
$data['aulatek']['centro'] ?? null,
$data['entreaulas']['organizacion'] ?? null,
$data['entreaulas']['organization'] ?? null,
$data['entreaulas']['centro'] ?? null,
] as $single) {
if (is_string($single) && $single !== '') {
return [$single];
}
}
return [];
}
/** Spanish alias used by pre-body.php menu rendering. */
function get_user_organizaciones(?array $auth_data = null): array
{
$org_ids = get_user_organizations($auth_data);
if (empty($org_ids)) {
return [];
}
$name_by_id = [];
foreach (db_get_organizations() as $org_row) {
$name_by_id[$org_row['org_id']] = $org_row['org_name'];
}
$result = [];
foreach ($org_ids as $org_id) {
$result[$org_id] = $name_by_id[$org_id] ?? $org_id;
}
return $result;
}
function get_user_centros(?array $auth_data = null): array
{
return get_user_organizations($auth_data);
}
/** Ensure active organization session keys are set and mirrored for legacy code. */
function init_active_org(?array $auth_data = null): void
{
$organizations = get_user_organizations($auth_data);
if (empty($organizations)) {
$_SESSION['active_organization'] = null;
$_SESSION['active_organizacion'] = null;
$_SESSION['active_centro'] = null;
return;
}
$current = $_SESSION['active_organization']
?? $_SESSION['active_organizacion']
?? $_SESSION['active_centro']
?? null;
if (!is_string($current) || !in_array($current, $organizations, true)) {
$current = $organizations[0];
}
$_SESSION['active_organization'] = $current;
$_SESSION['active_organizacion'] = $current;
$_SESSION['active_centro'] = $current;
if (!isset($_SESSION['auth_data']) || !is_array($_SESSION['auth_data'])) {
$_SESSION['auth_data'] = [];
}
$_SESSION['auth_data']['active_organization'] = $current;
if (!isset($_SESSION['auth_data']['aulatek']) || !is_array($_SESSION['auth_data']['aulatek'])) {
$_SESSION['auth_data']['aulatek'] = [];
}
$_SESSION['auth_data']['aulatek']['organizacion'] = $current;
$_SESSION['auth_data']['aulatek']['organization'] = $current;
$_SESSION['auth_data']['aulatek']['centro'] = $current;
if (!isset($_SESSION['auth_data']['entreaulas']) || !is_array($_SESSION['auth_data']['entreaulas'])) {
$_SESSION['auth_data']['entreaulas'] = [];
}
$_SESSION['auth_data']['entreaulas']['organizacion'] = $current;
$_SESSION['auth_data']['entreaulas']['organization'] = $current;
$_SESSION['auth_data']['entreaulas']['centro'] = $current;
}
function init_active_centro(?array $auth_data = null): void
{
init_active_org($auth_data);
}

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,256 @@
<?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
$stmt2 = $db->prepare("SELECT id FROM users WHERE username = ?");
$stmt2->execute([$username]);
$user_id = (int) $stmt2->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

@@ -0,0 +1,163 @@
-- filepath: /workspaces/Axia4/public_html/_incl/migrations/003_organizaciones.sql
-- Axia4 Migration 003: Rename centros to organizaciones
-- Migrates the centros table to organizaciones with org_id and org_name columns.
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
-- ── Create new organizaciones table ──────────────────────────────────────────
CREATE TABLE IF NOT EXISTS organizaciones (
id INTEGER PRIMARY KEY AUTOINCREMENT,
org_id TEXT UNIQUE NOT NULL,
org_name TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- ── Migrate data from centros to organizaciones ──────────────────────────────
INSERT INTO organizaciones (org_id, org_name, created_at)
SELECT centro_id, COALESCE(name, centro_id), created_at
FROM centros
WHERE NOT EXISTS (
SELECT 1 FROM organizaciones WHERE org_id = centros.centro_id
);
-- ── Update foreign key references in user_centros ──────────────────────────────
-- user_centros.centro_id → user_centros.org_id (rename column if needed via recreation)
-- For SQLite, we need to recreate the table due to FK constraint changes
CREATE TABLE user_centros_new (
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
org_id TEXT NOT NULL REFERENCES organizaciones(org_id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT '',
ea_aulas TEXT NOT NULL DEFAULT '[]',
PRIMARY KEY (user_id, org_id)
);
INSERT INTO user_centros_new (user_id, org_id, role, ea_aulas)
SELECT user_id, centro_id, role, aulas FROM user_centros;
DROP TABLE user_centros;
ALTER TABLE user_centros_new RENAME TO user_orgs;
-- ── Update foreign key references in aularios ──────────────────────────────────
CREATE TABLE aularios_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
org_id TEXT NOT NULL REFERENCES organizaciones(org_id) ON DELETE CASCADE,
aulario_id TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
icon TEXT NOT NULL DEFAULT '',
extra TEXT NOT NULL DEFAULT '{}',
UNIQUE (org_id, aulario_id)
);
INSERT INTO aularios_new (id, org_id, aulario_id, name, icon, extra)
SELECT id, centro_id, aulario_id, name, icon, extra FROM aularios;
DROP TABLE aularios;
ALTER TABLE aularios_new RENAME TO aularios;
-- ── Update foreign key references in remaining tables ──────────────────────────
-- supercafe_menu
CREATE TABLE supercafe_menu_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
org_id TEXT NOT NULL REFERENCES organizaciones(org_id) ON DELETE CASCADE,
data TEXT NOT NULL DEFAULT '{}',
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (org_id)
);
INSERT INTO supercafe_menu_new (id, org_id, data, updated_at)
SELECT id, centro_id, data, updated_at FROM supercafe_menu;
DROP TABLE supercafe_menu;
ALTER TABLE supercafe_menu_new RENAME TO supercafe_menu;
-- supercafe_orders
CREATE TABLE supercafe_orders_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
org_id TEXT NOT NULL REFERENCES organizaciones(org_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 (org_id, order_ref)
);
INSERT INTO supercafe_orders_new (id, org_id, order_ref, fecha, persona, comanda, notas, estado, created_at)
SELECT id, centro_id, order_ref, fecha, persona, comanda, notas, estado, created_at FROM supercafe_orders;
DROP TABLE supercafe_orders;
ALTER TABLE supercafe_orders_new RENAME TO supercafe_orders;
-- comedor_menu_types
CREATE TABLE comedor_menu_types_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
org_id TEXT NOT NULL REFERENCES organizaciones(org_id) ON DELETE CASCADE,
aulario_id TEXT NOT NULL,
data TEXT NOT NULL DEFAULT '[]',
UNIQUE (org_id, aulario_id)
);
INSERT INTO comedor_menu_types_new (id, org_id, aulario_id, data)
SELECT id, centro_id, aulario_id, data FROM comedor_menu_types;
DROP TABLE comedor_menu_types;
ALTER TABLE comedor_menu_types_new RENAME TO comedor_menu_types;
-- comedor_entries
CREATE TABLE comedor_entries_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
org_id TEXT NOT NULL REFERENCES organizaciones(org_id) ON DELETE CASCADE,
aulario_id TEXT NOT NULL,
year_month TEXT NOT NULL,
day TEXT NOT NULL,
data TEXT NOT NULL DEFAULT '{}',
UNIQUE (org_id, aulario_id, year_month, day)
);
INSERT INTO comedor_entries_new (id, org_id, aulario_id, year_month, day, data)
SELECT id, centro_id, aulario_id, year_month, day, data FROM comedor_entries;
DROP TABLE comedor_entries;
ALTER TABLE comedor_entries_new RENAME TO comedor_entries;
-- diario_entries
CREATE TABLE diario_entries_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
org_id TEXT NOT NULL REFERENCES organizaciones(org_id) ON DELETE CASCADE,
aulario_id TEXT NOT NULL,
entry_date TEXT NOT NULL,
data TEXT NOT NULL DEFAULT '{}',
UNIQUE (org_id, aulario_id, entry_date)
);
INSERT INTO diario_entries_new (id, org_id, aulario_id, entry_date, data)
SELECT id, centro_id, aulario_id, entry_date, data FROM diario_entries;
DROP TABLE diario_entries;
ALTER TABLE diario_entries_new RENAME TO diario_entries;
-- panel_alumno
CREATE TABLE panel_alumno_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
org_id TEXT NOT NULL REFERENCES organizaciones(org_id) ON DELETE CASCADE,
aulario_id TEXT NOT NULL,
alumno TEXT NOT NULL,
data TEXT NOT NULL DEFAULT '{}',
UNIQUE (org_id, aulario_id, alumno)
);
INSERT INTO panel_alumno_new (id, org_id, aulario_id, alumno, data)
SELECT id, centro_id, aulario_id, alumno, data FROM panel_alumno;
DROP TABLE panel_alumno;
ALTER TABLE panel_alumno_new RENAME TO panel_alumno;
-- ── Drop old centros table ─────────────────────────────────────────────────────
DROP TABLE IF EXISTS centros;
-- ── Verify migration ───────────────────────────────────────────────────────────
-- SELECT COUNT(*) as total_organizaciones FROM organizaciones;

View File

@@ -27,6 +27,12 @@ if (!empty($displayName)) {
$initials = mb_strtoupper($first . $last);
}
// Tenant (organización) management
$userOrganizaciones = get_user_organizaciones($_SESSION["auth_data"] ?? []);
$activeOrganizacionId = $_SESSION['active_organizacion']
?? ($_SESSION["auth_data"]["aulatek"]["organizacion"] ?? ($_SESSION["auth_data"]["entreaulas"]["organizacion"] ?? ''));
$activeOrganizacionName = $userOrganizaciones[$activeOrganizacionId] ?? '';
?>
<!DOCTYPE html>
<html lang="es">
@@ -38,412 +44,362 @@ if (!empty($displayName)) {
<link rel="stylesheet" href="/static/bootstrap.min.css" />
<link rel="icon" type="image/png" href="/static/<?php echo $APP_ICON ?? "logo.png"; ?>" />
<link rel="manifest" href="/static/manifest.json">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&family=Google+Sans:wght@400;500;700&display=swap" rel="stylesheet">
</head>
<body>
<style>
/* fieldset label {
margin-bottom: 15px;
/* ─── Google Workspace Design System ─────────────────────────────── */
:root {
--gw-font: 'Google Sans', 'Roboto', 'Arial', sans-serif;
--gw-blue: #1a73e8;
--gw-blue-hover: #1765cc;
--gw-blue-light: #e8f0fe;
--gw-text-primary: #202124;
--gw-text-secondary: #5f6368;
--gw-bg: #f0f4f9;
--gw-surface: #ffffff;
--gw-border: #dadce0;
--gw-hover: #f1f3f4;
--gw-header-h: 64px;
--gw-sidebar-w: 256px;
--gw-brand: #9013FE;
--bs-btn-font-family: 'Google Sans', 'Roboto', Arial, sans-serif;
--bs-body-font-family: 'Google Sans', 'Roboto', Arial, sans-serif;
--bs-font-sans-serif: 'Google Sans', 'Roboto', Arial, sans-serif;
--bs-link-color: var(--gw-blue);
--bs-link-hover-color: var(--gw-blue-hover);
}
.actbutton,
.actbutton-half {
padding: 5px 10px;
padding-left: 5px;
width: 200px;
text-align: right;
vertical-align: top;
}
*, *::before, *::after { box-sizing: border-box; }
.actbutton-half {
width: 167.5px;
}
.actbutton img,
.actbutton-half img {
float: left;
body {
font-family: var(--gw-font);
background: var(--gw-bg);
color: var(--gw-text-primary);
margin: 0;
height: 55px;
width: 55px;
margin-right: 10px;
}
td,
th {
padding: 0.3em 0.6em;
border-left: 1px solid #ccc;
border-right: 1px solid #ccc;
/* ─── Print ───────────────────────────────────────────────────────── */
@media print { .no-print { display: none; } }
/* ─── Form helpers ────────────────────────────────────────────────── */
input[readonly], textarea[readonly], .select select[readonly] {
background-color: #f1f3f4;
}
th {
text-align: center;
} */
@media print {
.no-print {
display: none;
}
}
input[readonly],
textarea[readonly],
.select select[readonly] {
background-color: lightgray;
}
fieldset input,
fieldset textarea,
fieldset .select select {
width: calc(100% - 1s5px);
fieldset input, fieldset textarea, fieldset .select select {
width: 100%;
box-sizing: border-box;
}
input.nonumscroll::-webkit-outer-spin-button,
input.nonumscroll::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input.nonumscroll::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
input.nonumscroll[type=number] { appearance: textfield; -moz-appearance: textfield; }
input.nonumscroll[type=number] {
appearance: textfield;
-moz-appearance: textfield;
}
/*
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0;
padding: 0;
}
a.grid-item {
margin-bottom: 10px !important;
padding: 15px;
width: 250px;
text-align: center;
}
a.grid-item img {
margin: 0 auto;
} */
.card.pad {
padding: 10px;
margin-bottom: 10px;
}
details summary {
cursor: pointer;
display: list-item;
}
.text-black {
color: black !important;
}
.btn {
margin-bottom: 5px;
}
.navbar-nav>a.btn {
margin-right: 10px;
}
.bg-custom {
background-color: #9013FE;
}
/* ─── Utility ─────────────────────────────────────────────────────── */
.card.pad { padding: 12px; margin-bottom: 12px; border: 1px solid var(--gw-border); border-radius: 8px; box-shadow: none; }
details summary { cursor: pointer; display: list-item; }
.text-black { color: black !important; }
.bg-custom { background-color: var(--gw-brand); }
.btn { margin-bottom: 4px; border-radius: 4px; font-family: var(--gw-font); font-weight: 500; letter-spacing: 0.01em; }
.btn-primary { background-color: var(--gw-blue); border-color: var(--gw-blue); }
.btn-primary:hover { background-color: var(--gw-blue-hover); border-color: var(--gw-blue-hover); }
.navbar-nav > a.btn { margin-right: 10px; }
/* ─── App shell ───────────────────────────────────────────────────── */
.app-shell {
display: flex;
min-height: 100vh;
background: #f5f5f5;
min-height: calc(100vh - var(--gw-header-h));
background: var(--gw-bg);
}
.sidebar-toggle-input {
position: absolute;
opacity: 0;
pointer-events: none;
}
/* ─── Sidebar toggle (hidden checkbox) ───────────────────────────── */
.sidebar-toggle-input { position: absolute; opacity: 0; pointer-events: none; }
/* ─── Sidebar ─────────────────────────────────────────────────────── */
.sidebar {
width: 260px;
background: #ffffff;
border-right: 1px solid #e5e7eb;
padding: 20px 16px;
width: var(--gw-sidebar-w);
background: var(--gw-surface);
padding: 8px 0;
position: sticky;
top: 0;
height: 100vh;
top: var(--gw-header-h);
height: calc(100vh - var(--gw-header-h));
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
transition: width 0.25s ease, transform 0.25s ease, padding 0.25s ease, opacity 0.2s ease;
gap: 0;
transition: width 0.3s cubic-bezier(0.4,0,0.2,1),
padding 0.3s cubic-bezier(0.4,0,0.2,1),
opacity 0.2s ease;
flex-shrink: 0;
}
.sidebar-toggle-input:not(:checked)~.app-shell .sidebar {
.sidebar-toggle-input:not(:checked) ~ .app-shell .sidebar {
width: 0;
padding-left: 0;
padding-right: 0;
border-right: none;
overflow: hidden;
opacity: 0;
pointer-events: none;
}
.sidebar-brand {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
color: #202124;
text-decoration: none;
}
.sidebar-brand img {
height: 34px;
.sidebar-section-label {
font-size: 11px;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--gw-text-secondary);
padding: 16px 16px 4px;
white-space: nowrap;
}
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 8px;
gap: 2px;
padding: 0 8px;
}
.sidebar-link {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: 12px;
gap: 16px;
padding: 0 16px;
height: 48px;
border-radius: 24px;
text-decoration: none;
color: #202124;
background: #f8f9fa;
outline: 1px solid grey;
color: var(--gw-text-primary);
font-size: 14px;
font-weight: 400;
white-space: nowrap;
transition: background 0.15s ease;
border: 1px solid grey;
}
.sidebar-link:hover { background: var(--gw-hover); color: var(--gw-text-primary); text-decoration: none; }
.sidebar-link.active, .sidebar-link:focus-visible { background: var(--gw-blue-light); color: var(--gw-blue); font-weight: 500; }
.sidebar-link img {
height: 26px;
}
.sidebar-link img { height: 20px; width: 20px; object-fit: contain; flex-shrink: 0; }
.sidebar-note {
font-size: 12px;
color: #5f6368;
}
.sidebar-divider { height: 1px; background: var(--gw-border); margin: 8px 16px; }
.sidebar-backdrop {
display: none;
}
.sidebar-backdrop { display: none; }
.app-content {
flex: 1;
min-width: 0;
}
.axia-home {
max-width: 1200px;
margin: 0 auto;
padding: 10px 16px 40px;
}
/* ─── App content ─────────────────────────────────────────────────── */
.app-content { flex: 1; min-width: 0; }
/* ─── Top header ──────────────────────────────────────────────────── */
.axia-header {
display: flex;
align-items: center;
gap: 18px;
background: #ffffff;
border-radius: 999px;
padding: 10px 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
gap: 4px;
background: var(--gw-surface);
border-bottom: 1px solid var(--gw-border);
padding: 0 8px;
height: var(--gw-header-h);
position: sticky;
top: 10px;
z-index: 5;
top: 0;
z-index: 100;
box-shadow: 0 1px 2px 0 rgba(60,64,67,0.1);
}
.logo-area {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
color: #202124;
text-decoration: none;
}
.brand-logo {
height: 32px;
}
.brand-text {
gap: 6px;
font-size: 18px;
font-weight: 400;
color: var(--gw-text-secondary);
text-decoration: none;
padding: 0 8px;
white-space: nowrap;
}
.logo-area:hover { color: var(--gw-text-primary); text-decoration: none; }
.brand-logo { height: 30px; }
.brand-text { font-size: 18px; font-weight: 400; letter-spacing: -0.01em; }
/* Sidebar toggle button */
.sidebar-toggle {
width: 36px;
height: 36px;
width: 40px;
height: 40px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
background: #f1f3f4;
color: #5f6368;
background: transparent;
color: var(--gw-text-secondary);
border: none;
transition: background 0.15s ease;
flex-shrink: 0;
}
.sidebar-toggle:hover { background: var(--gw-hover); }
.search-bar {
flex: 1;
}
/* Search bar */
.search-bar { flex: 1; max-width: 720px; margin: 0 auto; }
.search-bar form,
.search-bar > form { display: flex; }
.search-bar input {
width: 100%;
border: none;
background: #f1f3f4;
padding: 10px 16px;
border-radius: 999px;
border: 1px solid var(--gw-border);
background: var(--gw-hover);
padding: 8px 20px;
border-radius: 24px;
outline: none;
font-size: 15px;
font-size: 16px;
font-family: var(--gw-font);
color: var(--gw-text-primary);
transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
.header-actions {
display: flex;
align-items: center;
gap: 10px;
.search-bar input:focus {
background: var(--gw-surface);
border-color: var(--gw-blue);
box-shadow: 0 1px 6px rgba(32,33,36,0.28);
}
.search-bar input::placeholder { color: var(--gw-text-secondary); }
.axia-header summary {
list-style: none;
}
/* Header action area */
.header-actions { display: flex; align-items: center; gap: 4px; margin-left: auto; }
.axia-header summary::-webkit-details-marker {
display: none;
}
.axia-header summary { list-style: none; }
.axia-header summary::-webkit-details-marker { display: none; }
/* Icon button (for waffle, etc.) */
.icon-button {
list-style: none;
background: transparent;
border: none;
padding: 8px;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--gw-text-secondary);
transition: background 0.15s ease;
}
.icon-button:hover { background: var(--gw-hover); }
/* Waffle 3×3 dot grid */
.dot-grid {
display: grid;
grid-template-columns: repeat(3, 4px);
gap: 4px;
padding: 2px;
grid-template-columns: repeat(3, 5px);
gap: 3px;
}
.dot-grid span {
width: 4px;
height: 4px;
background: #5f6368;
width: 5px;
height: 5px;
background: var(--gw-text-secondary);
border-radius: 50%;
}
details {
position: relative;
}
/* ─── Dropdown cards ──────────────────────────────────────────────── */
details { position: relative; }
header .menu-card {
position: absolute;
right: 0;
margin-top: 10px;
background: #fff;
border-radius: 16px;
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12);
padding: 16px;
min-width: 240px;
z-index: 10;
top: calc(100% + 4px);
background: var(--gw-surface);
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2), 0 0 0 1px rgba(0,0,0,0.06);
padding: 12px 4px;
min-width: 280px;
z-index: 200;
}
header .menu-card-title {
font-size: 13px;
font-weight: 500;
color: var(--gw-text-secondary);
padding: 4px 16px 12px;
letter-spacing: 0.01em;
}
header .menu-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 12px;
grid-template-columns: repeat(3, 1fr);
gap: 0;
}
header .menu-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: 12px;
gap: 6px;
padding: 12px 8px;
border-radius: 8px;
text-decoration: none;
color: #202124;
background: #f8f9fa;
outline: 1px solid grey;
color: var(--gw-text-primary);
font-size: 12px;
text-align: center;
margin: 2px;
transition: background 0.15s ease;
}
header .menu-item:hover { background: var(--gw-hover); text-decoration: none; color: var(--gw-text-primary); }
header .menu-item img {
height: 28px;
height: 40px;
width: 40px;
border-radius: 8px;
object-fit: contain;
}
header .menu-item span { line-height: 1.3; }
/* ─── Avatar ──────────────────────────────────────────────────────── */
.avatar {
width: 36px;
height: 36px;
width: 32px;
height: 32px;
border-radius: 50%;
background: #1a73e8;
background: var(--gw-blue);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-weight: 500;
font-size: 13px;
cursor: pointer;
letter-spacing: 0.03em;
flex-shrink: 0;
}
.avatar.big { width: 56px; height: 56px; font-size: 22px; }
.avatar.big {
width: 48px;
height: 48px;
font-size: 24px;
}
.account-card {
min-width: 280px;
}
/* ─── Account card ────────────────────────────────────────────────── */
.account-card { min-width: 300px; }
.account-head {
display: flex;
gap: 12px;
flex-direction: column;
align-items: center;
margin-bottom: 12px;
}
.account-name {
font-weight: 600;
}
.account-email {
font-size: 13px;
color: #5f6368;
}
.account-actions .btn {
gap: 8px;
padding: 16px 16px 12px;
border-bottom: 1px solid var(--gw-border);
margin-bottom: 8px;
text-align: center;
}
.account-name { font-weight: 500; font-size: 16px; }
.account-email { font-size: 13px; color: var(--gw-text-secondary); }
.account-actions { padding: 0 12px 8px; }
.account-actions .btn { margin-bottom: 6px; border-radius: 20px; font-size: 14px; }
/* ─── Main content ────────────────────────────────────────────────── */
.axia-home {
max-width: 1200px;
margin: 0 auto;
padding: 24px 24px 48px;
}
/* ─── Mobile ──────────────────────────────────────────────────────── */
@media (max-width: 768px) {
.sidebar-toggle {
display: inline-flex;
/* make it more to the left */
margin-left: -8px;
}
.axia-home { padding: 16px 12px 48px; }
.logo-area {
gap: 6px;
margin-left: -8px;
margin-right: -8px;
}
.axia-home {
padding: 10px 8px 40px;
}
.app-shell {
display: block;
}
.app-shell { display: block; }
.sidebar {
position: fixed;
@@ -451,19 +407,16 @@ if (!empty($displayName)) {
top: 0;
height: 100vh;
transform: translateX(-100%);
transition: transform 0.25s ease;
z-index: 20;
width: 260px;
padding: 20px 16px;
border-right: 1px solid #e5e7eb;
transition: transform 0.25s cubic-bezier(0.4,0,0.2,1);
z-index: 200;
width: var(--gw-sidebar-w);
padding: 8px 0;
opacity: 1;
pointer-events: auto;
}
.sidebar-toggle-input:not(:checked)~.app-shell .sidebar {
width: 260px;
padding: 20px 16px;
border-right: 1px solid #e5e7eb;
.sidebar-toggle-input:not(:checked) ~ .app-shell .sidebar {
width: var(--gw-sidebar-w);
opacity: 1;
pointer-events: auto;
}
@@ -471,52 +424,22 @@ if (!empty($displayName)) {
.sidebar-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.35);
background: rgba(0,0,0,0.32);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease;
z-index: 15;
z-index: 150;
display: block;
}
.sidebar-toggle-input:checked~.app-shell .sidebar {
transform: translateX(0);
}
.sidebar-toggle-input:checked ~ .app-shell .sidebar { transform: translateX(0); }
.sidebar-toggle-input:checked ~ .app-shell .sidebar-backdrop { opacity: 1; pointer-events: auto; }
.sidebar-toggle-input:checked~.app-shell .sidebar-backdrop {
opacity: 1;
pointer-events: auto;
}
.search-bar { display: none; }
.header-actions { gap: 2px; }
.hide-small { display: none; }
.axia-header {
flex-wrap: wrap;
border-radius: 20px;
}
.search-bar {
width: 100%;
order: 3;
display: none;
}
/* make other buttons alinged to the right */
.header-actions {
margin-left: auto;
margin-right: -8px;
gap: 6px;
}
.hide-small {
display: none;
}
}
:root {
--bs-btn-font-family: Arial, Helvetica, sans-serif;
--bs-body-font-family: Arial, Helvetica, sans-serif;
--bs-font-sans-serif: Arial, Helvetica, sans-serif;
--bs-font-family-base: Arial, Helvetica, sans-serif;
--bs-heading-font-family: Arial, Helvetica, sans-serif;
.logo-area { padding: 0 4px; }
}
</style>
@@ -548,96 +471,116 @@ if (!empty($displayName)) {
});
})();
</script>
<!-- ── Google Workspace-style top header ──────────────────── -->
<header class="axia-header">
<label for="sidebarToggle" class="sidebar-toggle" aria-label="Abrir menú">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
</svg>
</label>
<a class="logo-area" href="<?= $APP_ROOT ?>">
<img src="/static/<?= $APP_ICON ?>" alt="<?= htmlspecialchars($APP_NAME) ?>" class="brand-logo">
<span class="brand-text hide-small"><?= $APP_NAME ?></span>
</a>
<div class="search-bar">
<form action="https://search.tech.eus/s/" method="get">
<input type="text" name="q" placeholder="Búsqueda global" aria-label="Buscar">
</form>
</div>
<div class="header-actions">
<details class="app-menu">
<summary class="icon-button" aria-label="Menú de aplicaciones">
<span class="dot-grid" aria-hidden="true">
<span></span><span></span><span></span>
<span></span><span></span><span></span>
<span></span><span></span><span></span>
</span>
</summary>
<div class="menu-card">
<div class="menu-card-title">Aplicaciones de Axia4</div>
<div class="menu-grid">
<a class="menu-item" href="/">
<img src="/static/logo.png" alt="">
<span>Axia4</span>
</a>
<a class="menu-item" href="/club/">
<img src="/static/logo-club.png" alt="">
<span>Club</span>
</a>
<a class="menu-item" href="/aulatek/">
<img src="/static/logo-entreaulas.png" alt="">
<span>AulaTek</span>
</a>
<a class="menu-item" href="/account/">
<img src="/static/logo-account.png" alt="">
<span>Cuenta</span>
</a>
<a class="menu-item" href="/sysadmin/">
<img src="/static/logo-sysadmin.png" alt="">
<span>SysAdmin</span>
</a>
</div>
</div>
</details>
<details class="account-menu">
<summary class="avatar" aria-label="Cuenta">
<?php echo htmlspecialchars($initials); ?>
</summary>
<div class="menu-card account-card">
<div class="account-head">
<div class="avatar big"><?php echo htmlspecialchars($initials); ?></div>
<div class="account-name"><?php echo htmlspecialchars($displayName); ?></div>
<div class="account-email"><?php echo htmlspecialchars($email); ?></div>
</div>
<?php if (!empty($userOrganizaciones) && $_SESSION["auth_ok"]): ?>
<div style="padding: 8px 16px;">
<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($userOrganizaciones) > 1 ? '8px' : '0' ?>;">
<?= htmlspecialchars($activeOrganizacionName ?: '') ?>
</div>
<?php if (count($userOrganizaciones) > 1): ?>
<div style="font-size:.75rem;color:#5f6368;margin-bottom:4px;">Cambiar organización:</div>
<?php foreach ($userOrganizaciones as $oid => $orgName): if ($oid === $activeOrganizacionId) 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="organization" value="<?= htmlspecialchars($oid) ?>"
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($orgName) ?>
</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-dark w-100">Gestionar cuenta</a>
<a href="/_login.php?logout=1&redir=/" class="btn btn-outline-danger w-100">Cerrar sesión</a>
<?php } else { ?>
<a href="/_login.php?redir=/" class="btn btn-primary w-100">Iniciar sesión</a>
<a href="/account/register.php" class="btn btn-outline-dark w-100">Crear cuenta</a>
<?php } ?>
</div>
</div>
</details>
</div>
</header>
<!-- ── App shell (sidebar + content) ──────────────────────── -->
<div class="app-shell">
<aside class="sidebar">
<b>Esta app</b>
<nav class="sidebar-nav">
<?php
if (file_exists(__DIR__ . "/../$APP_CODE/__menu.php")) {
include __DIR__ . "/../$APP_CODE/__menu.php";
}
?>
</nav>
<b>Axia4</b>
<nav class="sidebar-nav">
<a class="sidebar-link" href="/">
<img src="/static/logo.png" alt="">
<span>Inicio</span>
</a>
</nav>
<?php
if (file_exists(__DIR__ . "/../$APP_CODE/__menu.php")) {
include __DIR__ . "/../$APP_CODE/__menu.php";
}
?>
</aside>
<label for="sidebarToggle" class="sidebar-backdrop" aria-hidden="true"></label>
<div class="app-content">
<main class="axia-home">
<header class="axia-header">
<label for="sidebarToggle" class="sidebar-toggle" aria-label="Abrir menú">☰</label>
<a class="logo-area" href="<?= $APP_ROOT ?>">
<img src="/static/<?= $APP_ICON ?>" alt="<?= htmlspecialchars($APP_NAME) ?>" class="brand-logo">
<span class="brand-text"><?= $APP_NAME ?></span>
</a>
<form class="search-bar" action="https://search.tech.eus/s/" method="get">
<input type="text" name="q" placeholder="Busqueda Global" aria-label="Buscar">
</form>
<div class="header-actions">
<details class="app-menu">
<summary class="icon-button" aria-label="Menú de aplicaciones">
<span class="dot-grid" aria-hidden="true">
<span></span><span></span><span></span>
<span></span><span></span><span></span>
<span></span><span></span><span></span>
</span>
</summary>
<div class="menu-card">
<div class="menu-grid">
<a class="menu-item" href="/">
<img src="/static/logo.png" alt="">
<span>Axia4</span>
</a>
<a class="menu-item" href="/club/">
<img src="/static/logo-club.png" alt="">
<span>Club</span>
</a>
<a class="menu-item" href="/entreaulas/">
<img src="/static/logo-entreaulas.png" alt="">
<span>EntreAulas</span>
</a>
<a class="menu-item" href="/account/">
<img src="/static/logo-account.png" alt="">
<span>Cuenta</span>
</a>
<a class="menu-item" href="/sysadmin/">
<img src="/static/logo-sysadmin.png" alt="">
<span>SysAdmin</span>
</a>
</div>
</div>
</details>
<details class="account-menu">
<summary class="avatar" aria-label="Cuenta">
<?php echo htmlspecialchars($initials); ?>
</summary>
<div class="menu-card account-card">
<div class="account-head">
<div class="avatar big"><?php echo htmlspecialchars($initials); ?></div>
<div>
<div class="account-name"><?php echo htmlspecialchars($displayName); ?></div>
<div class="account-email"><?php echo htmlspecialchars($email); ?></div>
</div>
</div>
<div class="account-actions">
<?php if ($_SESSION["auth_ok"]) { ?>
<a href="/account/" class="btn btn-primary w-100">Gestionar cuenta</a>
<a href="/_login.php?logout=1&redir=/" class="btn btn-outline-secondary w-100">Cerrar sesión</a>
<?php } else { ?>
<a href="/_login.php?redir=/" class="btn btn-primary w-100">Iniciar sesión</a>
<a href="/account/register.php" class="btn btn-outline-primary w-100">Crear cuenta</a>
<?php } ?>
</div>
</div>
</details>
</div>
</header>
<div style="margin-top: 20px;">
<?php } ?>
<?php if (isset($_GET["_result"])) { ?>
<div class="card pad"

View File

@@ -0,0 +1,42 @@
<?php
/**
* switch_organization.php
* POST endpoint to switch the active organization for the current user session.
* Validates the requested organization against the user's allowed organizations 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 = safe_organization_id(
$_POST['organization']
?? $_POST['organizacion']
?? $_POST['org']
?? $_POST['centro']
?? ''
);
$redir = safe_redir($_POST['redir'] ?? '/');
$organizations = get_user_organizations($_SESSION['auth_data'] ?? []);
if ($requested !== '' && in_array($requested, $organizations, true)) {
$_SESSION['active_organization'] = $requested;
$_SESSION['active_organizacion'] = $requested;
$_SESSION['active_centro'] = $requested;
// Also update session auth_data so it reflects immediately
$_SESSION['auth_data']['active_organization'] = $requested;
$_SESSION['auth_data']['aulatek']['organizacion'] = $requested;
$_SESSION['auth_data']['aulatek']['organization'] = $requested;
$_SESSION['auth_data']['aulatek']['centro'] = $requested;
$_SESSION['auth_data']['entreaulas']['organizacion'] = $requested;
$_SESSION['auth_data']['entreaulas']['organization'] = $requested;
$_SESSION['auth_data']['entreaulas']['centro'] = $requested;
}
header("Location: $redir");
exit;

View File

@@ -1,87 +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_org($_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_org($_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_org($_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_org($_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)
function user_has_permission(string $perm): bool
{
return in_array($perm, $_SESSION["auth_data"]["permissions"] ?? []);
return in_array($perm, $_SESSION["auth_data"]["permissions"] ?? [], true);
}

View File

@@ -108,12 +108,23 @@ function Sb($input) {
}
function get_user_file_path($username)
{
return USERS_DIR . $username . '.json';
$users_dir = defined('USERS_DIR') ? USERS_DIR : '/DATA/Usuarios/';
return rtrim($users_dir, '/') . '/' . $username . '.json';
}
function safe_organization_id($value)
{
return preg_replace('/[^a-zA-Z0-9._-]/', '', (string)$value);
}
function safe_organizacion_id($value)
{
return safe_organization_id($value);
}
function safe_centro_id($value)
{
return preg_replace('/[^a-zA-Z0-9._-]/', '', (string)$value);
return safe_organization_id($value);
}
function safe_aulario_id($value)
@@ -156,12 +167,42 @@ function path_is_within($real_base, $real_path)
return strpos($real_path, $base_prefix) === 0 || $real_path === rtrim($real_base, DIRECTORY_SEPARATOR);
}
function aulatek_orgs_base_path()
{
$orgs_path = '/DATA/entreaulas/Organizaciones';
$legacy_path = '/DATA/entreaulas/Centros';
if (is_dir($orgs_path)) {
return $orgs_path;
}
if (is_dir($legacy_path)) {
return $legacy_path;
}
return $orgs_path;
}
function entreaulas_orgs_base_path()
{
return aulatek_orgs_base_path();
}
function safe_aulario_config_path($centro_id, $aulario_id)
{
$centro = safe_centro_id($centro_id);
$centro = safe_organization_id($centro_id);
$aulario = safe_id_segment($aulario_id);
if ($centro === '' || $aulario === '') {
return null;
}
return "/DATA/entreaulas/Centros/$centro/Aularios/$aulario.json";
return aulatek_orgs_base_path() . "/$centro/Aularios/$aulario.json";
}
function safe_redir($url, $default = "/")
{
if (empty($url) || !is_string($url)) {
return $default;
}
// Only allow relative URLs that start with /
if (str_starts_with($url, "/") && !str_contains($url, "\0")) {
return $url;
}
return $default;
}

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', 'aulatek:access'],
'password_hash' => password_hash($admin_password, PASSWORD_DEFAULT),
]);
db_set_config('installed', '1');
header("Location: /_login.php");
exit;
break;

View File

@@ -1,38 +1,27 @@
<?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"];
/**
* Return a safe redirect URL: only allow relative paths starting with a single slash.
* Falls back to "/" for any external, protocol-relative, or otherwise unsafe URLs.
*/
function safe_redir($url) {
$url = (string)$url;
// Must start with a single "/" but not "//" (protocol-relative)
if (preg_match('#^/[^/]#', $url) || $url === '/') {
// Strip newlines to prevent header injection
return preg_replace('/[\r\n]/', '', $url);
}
return '/';
}
// safe_redir() is provided by _incl/tools.security.php.
if ($_GET["reload_user"] == "1") {
$user_filename = safe_username_to_filename($_SESSION["auth_user"] ?? "");
if ($user_filename === "") {
if (($_GET["reload_user"] ?? "") === "1") {
$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_org($_SESSION['auth_data']);
$redir = safe_redir($_GET["redir"] ?? "/");
header("Location: $redir");
die();
}
if ($_GET["google_callback"] == "1") {
if (($_GET["google_callback"] ?? "") === "1") {
if (!isset($AuthConfig["google_client_id"]) || !isset($AuthConfig["google_client_secret"])) {
die("Error: La autenticación de Google no está configurada.");
}
@@ -83,41 +72,43 @@ 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_org($_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"] ?? "/");
header("Location: $redir");
die();
}
if ($_GET["google"] == "1") {
if (($_GET["google"] ?? "") === "1") {
if (!isset($AuthConfig["google_client_id"]) || !isset($AuthConfig["google_client_secret"])) {
die("Error: La autenticación de Google no está configurada.");
}
@@ -145,7 +136,7 @@ if ($_GET["google"] == "1") {
header("Location: " . $request_to);
die();
}
if ($_GET["logout"] == "1") {
if (($_GET["logout"] ?? "") === "1") {
$redir = safe_redir($_GET["redir"] ?? "/");
$cookie_options_expired = ["expires" => time() - 3600, "path" => "/", "httponly" => true, "secure" => true, "samesite" => "Lax"];
setcookie("auth_user", "", $cookie_options_expired);
@@ -154,27 +145,26 @@ if ($_GET["logout"] == "1") {
header("Location: $redir");
die();
}
if ($_GET["clear_session"] == "1") {
if (($_GET["clear_session"] ?? "") === "1") {
session_destroy();
$redir = safe_redir($_GET["redir"] ?? "/");
header("Location: $redir");
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_org($_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 +172,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,132 @@
<?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 / organization management
$userOrganizations = get_user_organizations($authData);
$activeOrganization = $_SESSION['active_organization']
?? ($authData['aulatek']['organizacion'] ?? ($authData['aulatek']['centro'] ?? ($authData['entreaulas']['organizacion'] ?? ($authData['entreaulas']['centro'] ?? ''))));
$aularios = ($activeOrganization !== '') ? db_get_aularios($activeOrganization) : [];
$userAulas = $authData['aulatek']['aulas'] ?? ($authData['entreaulas']['aulas'] ?? []);
$role = $authData['aulatek']['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>
<?php require_once "_incl/post-body.php"; ?>
<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($userOrganizations)): ?>
<div class="account-card">
<h2>Organizaciones</h2>
<?php foreach ($userOrganizations as $orgId): ?>
<form method="post" action="/_incl/switch_tenant.php" style="margin:0;">
<input type="hidden" name="redir" value="/account/">
<button type="submit" name="organization" value="<?= htmlspecialchars($orgId) ?>"
class="tenant-btn <?= ($activeOrganization === $orgId) ? 'active-tenant' : '' ?>">
<?php if ($activeOrganization === $orgId): ?>
<span style="color:var(--gw-blue,#1a73e8);">✓ </span>
<?php endif; ?>
<?= htmlspecialchars($orgId) ?>
<?php if ($activeOrganization === $orgId): ?>
<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($activeOrganization) ?>)</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($activeOrganization ?: '') ?></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

@@ -1,11 +1,10 @@
<!-- <a href="/_login.php?reload_user=1" class="btn btn-secondary">Recargar Cuenta</a>
<a href="/_login.php?logout=1" class="btn btn-secondary">Cerrar sesión</a> -->
<a class="sidebar-link" href="/entreaulas/">
<span>Mi aula</span>
</a>
<a class="sidebar-link" href="/entreaulas/supercafe.php">
<img src="/static/iconexperience/purchase_order_cart.png" alt="">
<span>SuperCafe</span>
</a>
<div class="sidebar-section-label">Atajos</div>
<nav class="sidebar-nav">
<a class="sidebar-link" href="/aulatek/">
<span>Mi aula</span>
</a>
<a class="sidebar-link" href="/aulatek/supercafe.php">
<img src="/static/iconexperience/purchase_order_cart.png" alt="">
<span>SuperCafe</span>
</a>
</nav>

View File

@@ -10,9 +10,10 @@ header("Access-Control-Allow-Origin: *");
$type = $_GET["type"] ?? "";
$orgs_base_dir = basename(aulatek_orgs_base_path());
switch ($type) {
case "alumno_photo":
$centro = safe_centro_id($_GET["centro"] ?? '');
$centro = safe_organization_id($_GET["organization"] ?? $_GET["organizacion"] ?? $_GET["org"] ?? $_GET["centro"] ?? '');
$aulario = safe_id_segment($_GET["aulario"] ?? '');
$alumno = safe_id_segment($_GET["alumno"] ?? '');
// Additional validation to prevent empty names
@@ -20,19 +21,19 @@ switch ($type) {
header("HTTP/1.1 403 Forbidden");
die("Invalid parameters");
}
$relpath = "entreaulas/Centros/$centro/Aularios/$aulario/Alumnos/$alumno/photo.jpg";
$relpath = "entreaulas/$orgs_base_dir/$centro/Aularios/$aulario/Alumnos/$alumno/photo.jpg";
break;
case "panel_actividades":
$centro = safe_centro_id($_GET["centro"] ?? '');
$centro = safe_organization_id($_GET["organization"] ?? $_GET["organizacion"] ?? $_GET["org"] ?? $_GET["centro"] ?? '');
$activity = safe_id_segment($_GET["activity"] ?? '');
if (empty($centro) || empty($activity)) {
header("HTTP/1.1 400 Bad Request");
die("Invalid parameters");
}
$relpath = "entreaulas/Centros/$centro/Panel/Actividades/$activity/photo.jpg";
$relpath = "entreaulas/$orgs_base_dir/$centro/Panel/Actividades/$activity/photo.jpg";
break;
case "comedor_image":
$centro = safe_centro_id($_GET["centro"] ?? '');
$centro = safe_organization_id($_GET["organization"] ?? $_GET["organizacion"] ?? $_GET["org"] ?? $_GET["centro"] ?? '');
$aulario = safe_id_segment($_GET["aulario"] ?? '');
$date = preg_replace('/[^0-9-]/', '', $_GET["date"] ?? '');
$file = safe_filename($_GET["file"] ?? '');
@@ -46,10 +47,10 @@ switch ($type) {
}
$ym = substr($date, 0, 7);
$day = substr($date, 8, 2);
$relpath = "entreaulas/Centros/$centro/Aularios/$aulario/Comedor/$ym/$day/$file";
$relpath = "entreaulas/$orgs_base_dir/$centro/Aularios/$aulario/Comedor/$ym/$day/$file";
break;
case "proyecto_file":
$centro = safe_centro_id($_GET["centro"] ?? '');
$centro = safe_organization_id($_GET["organization"] ?? $_GET["organizacion"] ?? $_GET["org"] ?? $_GET["centro"] ?? '');
$project = safe_id_segment($_GET["project"] ?? '');
$file = safe_filename($_GET["file"] ?? '');
if (empty($centro) || empty($project) || empty($file)) {
@@ -61,7 +62,7 @@ switch ($type) {
header("HTTP/1.1 400 Bad Request");
die("Invalid file name");
}
$projects_base = "/DATA/entreaulas/Centros/$centro/Proyectos";
$projects_base = aulatek_orgs_base_path() . "/$centro/Proyectos";
$project_dir = null;
if (is_dir($projects_base)) {
$iterator = new RecursiveIteratorIterator(

View File

@@ -1,5 +1,5 @@
<?php
$APP_CODE = "entreaulas";
$APP_NAME = "EntreAulas";
$APP_TITLE = "EntreAulas";
$APP_CODE = "aulatek";
$APP_NAME = "AulaTek";
$APP_TITLE = "AulaTek";
require_once __DIR__ . "/../../_incl/auth_redir.php";

View File

@@ -1,5 +1,5 @@
<?php
$APP_CODE = "entreaulas";
$APP_NAME = "EntreAulas";
$APP_TITLE = "EntreAulas";
$APP_CODE = "aulatek";
$APP_NAME = "AulaTek";
$APP_TITLE = "AulaTek";
require_once __DIR__ . "/../../_incl/pre-body.php";

View File

@@ -2,14 +2,16 @@
require_once "_incl/auth_redir.php";
require_once "../_incl/tools.security.php";
// Check if user has docente permission
if (!in_array("entreaulas:docente", $_SESSION["auth_data"]["permissions"] ?? [])) {
$permissions = $_SESSION["auth_data"]["permissions"] ?? [];
if (!in_array("aulatek:docente", $permissions, true) && !in_array("entreaulas:docente", $permissions, true)) {
header("HTTP/1.1 403 Forbidden");
die("Access denied");
}
$aulario_id = safe_id_segment($_GET["aulario"] ?? "");
$centro_id = safe_centro_id($_SESSION["auth_data"]["entreaulas"]["centro"] ?? "");
$tenant_data = $_SESSION["auth_data"]["aulatek"] ?? ($_SESSION["auth_data"]["entreaulas"] ?? []);
$centro_id = safe_organization_id($tenant_data["organizacion"] ?? ($tenant_data["centro"] ?? ""));
if (empty($aulario_id) || empty($centro_id)) {
require_once "_incl/pre-body.php";
@@ -24,7 +26,7 @@ if (empty($aulario_id) || empty($centro_id)) {
}
// Validate paths with realpath
$base_path = "/DATA/entreaulas/Centros";
$base_path = aulatek_orgs_base_path();
$real_base = realpath($base_path);
$alumnos_base_path = "$base_path/$centro_id/Aularios/$aulario_id/Alumnos";
@@ -250,7 +252,7 @@ switch ($_GET["action"] ?? '') {
<label class="form-label">Foto actual:</label>
<?php if ($photo_exists): ?>
<div class="mb-2">
<img src="_filefetch.php?type=alumno_photo&alumno=<?= urlencode($nombre) ?>&centro=<?= urlencode($centro_id) ?>&aulario=<?= urlencode($aulario_id) ?>"
<img src="_filefetch.php?type=alumno_photo&alumno=<?= urlencode($nombre) ?>&org=<?= urlencode($centro_id) ?>&aulario=<?= urlencode($aulario_id) ?>"
alt="Foto de <?= htmlspecialchars($nombre) ?>"
style="max-width: 200px; max-height: 200px; border: 2px solid #ddd; border-radius: 10px;">
</div>
@@ -351,7 +353,7 @@ switch ($_GET["action"] ?? '') {
<tr>
<td>
<?php if ($photo_exists): ?>
<img src="_filefetch.php?type=alumno_photo&alumno=<?= urlencode($nombre) ?>&centro=<?= urlencode($centro_id) ?>&aulario=<?= urlencode($aulario_id) ?>"
<img src="_filefetch.php?type=alumno_photo&alumno=<?= urlencode($nombre) ?>&org=<?= urlencode($centro_id) ?>&aulario=<?= urlencode($aulario_id) ?>"
alt="Foto de <?= htmlspecialchars($nombre) ?>"
style="width: 50px; height: 50px; object-fit: cover; border-radius: 5px;">
<?php else: ?>
@@ -379,7 +381,7 @@ switch ($_GET["action"] ?? '') {
</table>
<?php endif; ?>
<a href="/entreaulas/aulario.php?id=<?= urlencode($aulario_id) ?>" class="btn btn-secondary mt-3">Volver al Aulario</a>
<a href="/aulatek/aulario.php?id=<?= urlencode($aulario_id) ?>" class="btn btn-secondary mt-3">Volver al Aulario</a>
</div>
<?php
require_once "_incl/post-body.php";

View File

@@ -15,7 +15,7 @@ La API utiliza el mismo sistema de autenticación que el resto de la aplicación
### 1. Obtener tipos de menú
**GET** `/entreaulas/api/comedor.php?action=get_menu_types&aulario={aulario_id}`
**GET** `/aulatek/api/comedor.php?action=get_menu_types&aulario={aulario_id}`
Devuelve todos los tipos de menú disponibles para un aulario.
@@ -89,7 +89,7 @@ Obtiene el menú de un día específico y tipo de menú.
### 3. Guardar menú
**POST** `/entreaulas/api/comedor.php?action=save_menu&aulario={aulario_id}`
**POST** `/aulatek/api/comedor.php?action=save_menu&aulario={aulario_id}`
Guarda o actualiza un menú para un día específico.
@@ -114,7 +114,7 @@ Guarda o actualiza un menú para un día específico.
**Ejemplo de uso con curl:**
```bash
curl -X POST "http://localhost/entreaulas/api/comedor.php?action=save_menu&aulario=aulario_id" \
curl -X POST "http://localhost/aulatek/api/comedor.php?action=save_menu&aulario=aulario_id" \
-H "Content-Type: application/json" \
-d '{
"date": "2026-02-18",
@@ -131,7 +131,7 @@ curl -X POST "http://localhost/entreaulas/api/comedor.php?action=save_menu&aular
### 4. Añadir nuevo tipo de menú
**POST** `/entreaulas/api/comedor.php?action=add_menu_type&aulario={aulario_id}`
**POST** `/aulatek/api/comedor.php?action=add_menu_type&aulario={aulario_id}`
Crea un nuevo tipo de menú.
@@ -146,7 +146,7 @@ Crea un nuevo tipo de menú.
**Ejemplo de uso con curl:**
```bash
curl -X POST "http://localhost/entreaulas/api/comedor.php?action=add_menu_type&aulario=aulario_id" \
curl -X POST "http://localhost/aulatek/api/comedor.php?action=add_menu_type&aulario=aulario_id" \
-H "Content-Type: application/json" \
-d '{
"id": "celiaco",
@@ -159,7 +159,7 @@ curl -X POST "http://localhost/entreaulas/api/comedor.php?action=add_menu_type&a
### 5. Renombrar tipo de menú
**POST** `/entreaulas/api/comedor.php?action=rename_menu_type&aulario={aulario_id}`
**POST** `/aulatek/api/comedor.php?action=rename_menu_type&aulario={aulario_id}`
Cambia el nombre o color de un tipo de menú existente.
@@ -176,7 +176,7 @@ Cambia el nombre o color de un tipo de menú existente.
### 6. Eliminar tipo de menú
**POST** `/entreaulas/api/comedor.php?action=delete_menu_type&aulario={aulario_id}`
**POST** `/aulatek/api/comedor.php?action=delete_menu_type&aulario={aulario_id}`
Elimina un tipo de menú.
@@ -211,7 +211,7 @@ Elimina un tipo de menú.
```javascript
async function obtenerMenu(aularioId) {
const response = await fetch(
`/entreaulas/api/comedor.php?action=get_menu&aulario=${aularioId}`
`/aulatek/api/comedor.php?action=get_menu&aulario=${aularioId}`
);
const data = await response.json();
return data.menu;
@@ -223,7 +223,7 @@ async function obtenerMenu(aularioId) {
```javascript
async function guardarMenu(aularioId, fecha, tipoMenu, platos) {
const response = await fetch(
`/entreaulas/api/comedor.php?action=save_menu&aulario=${aularioId}`,
`/aulatek/api/comedor.php?action=save_menu&aulario=${aularioId}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -243,7 +243,7 @@ async function guardarMenu(aularioId, fecha, tipoMenu, platos) {
```javascript
async function obtenerTiposMenu(aularioId) {
const response = await fetch(
`/entreaulas/api/comedor.php?action=get_menu_types&aulario=${aularioId}`
`/aulatek/api/comedor.php?action=get_menu_types&aulario=${aularioId}`
);
const data = await response.json();
return data.menu_types;

View File

@@ -2,95 +2,64 @@
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"] ?? [])) {
$permissions = $_SESSION["auth_data"]["permissions"] ?? [];
if (!in_array("aulatek:docente", $permissions, true) && !in_array("entreaulas:docente", $permissions, true)) {
http_response_code(403);
die(json_encode(["error" => "Access denied", "code" => "FORBIDDEN"]));
}
$centro_id = safe_centro_id($_SESSION["auth_data"]["entreaulas"]["centro"] ?? "");
$tenant_data = $_SESSION["auth_data"]["aulatek"] ?? ($_SESSION["auth_data"]["entreaulas"] ?? []);
$centro_id = safe_organization_id($tenant_data["organizacion"] ?? ($tenant_data["centro"] ?? ""));
if ($centro_id === "") {
http_response_code(400);
die(json_encode(["error" => "Centro not found in session", "code" => "INVALID_SESSION"]));
die(json_encode(["error" => "Organizacion 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', $tenant_data["aulas"] ?? [])));
if (!in_array($aulario_id, $userAulas, true)) {
http_response_code(403);
die(json_encode(["error" => "Access denied to this aulario", "code" => "FORBIDDEN"]));
}
$aulario_path = safe_aulario_config_path($centro_id, $aulario_id);
$aulario = ($aulario_path && file_exists($aulario_path)) ? json_decode(file_get_contents($aulario_path), true) : null;
$aulario = db_get_aulario($centro_id, $aulario_id);
// Handle shared comedor data
$source_aulario_id = $aulario_id;
$is_shared = false;
if ($aulario && !empty($aulario["shared_comedor_from"])) {
$shared_from = safe_id_segment($aulario["shared_comedor_from"]);
$shared_aulario_path = safe_aulario_config_path($centro_id, $shared_from);
if ($shared_aulario_path && file_exists($shared_aulario_path)) {
if (db_get_aulario($centro_id, $shared_from)) {
$source_aulario_id = $shared_from;
$is_shared = true;
}
}
// Check edit permissions (must be sysadmin and not shared)
$canEdit = in_array("sysadmin:access", $_SESSION["auth_data"]["permissions"] ?? []) && !$is_shared;
// Helper functions
function get_menu_types($centro_id, $source_aulario_id) {
$menuTypesPath = menu_types_path($centro_id, $source_aulario_id);
$defaultMenuTypes = [
["id" => "basal", "label" => "Menú basal", "color" => "#0d6efd"],
["id" => "vegetariano", "label" => "Menú vegetariano", "color" => "#198754"],
["id" => "alergias", "label" => "Menú alergias", "color" => "#dc3545"],
];
$defaultMenuTypes = [
["id" => "basal", "label" => "Menú basal", "color" => "#0d6efd"],
["id" => "vegetariano", "label" => "Menú vegetariano", "color" => "#198754"],
["id" => "alergias", "label" => "Menú alergias", "color" => "#dc3545"],
];
if ($menuTypesPath === null) {
function get_menu_types($centro_id, $source_aulario_id) {
global $defaultMenuTypes;
$types = db_get_comedor_menu_types($centro_id, $source_aulario_id);
if (empty($types)) {
db_set_comedor_menu_types($centro_id, $source_aulario_id, $defaultMenuTypes);
return $defaultMenuTypes;
}
if (!file_exists($menuTypesPath)) {
if (!is_dir(dirname($menuTypesPath))) {
mkdir(dirname($menuTypesPath), 0777, true);
}
file_put_contents($menuTypesPath, json_encode($defaultMenuTypes, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
return $defaultMenuTypes;
}
$menuTypes = json_decode(@file_get_contents($menuTypesPath), true);
return (is_array($menuTypes) && count($menuTypes) > 0) ? $menuTypes : $defaultMenuTypes;
return $types;
}
function blank_menu() {
@@ -98,7 +67,7 @@ function blank_menu() {
"plates" => [
"primero" => ["name" => "", "pictogram" => ""],
"segundo" => ["name" => "", "pictogram" => ""],
"postre" => ["name" => "", "pictogram" => ""],
"postre" => ["name" => "", "pictogram" => ""],
]
];
}
@@ -113,43 +82,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 +108,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,12 @@
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;
$tenant_data = $_SESSION["auth_data"]["aulatek"] ?? ($_SESSION["auth_data"]["entreaulas"] ?? []);
$centro_id = safe_organization_id($tenant_data["organizacion"] ?? ($tenant_data["centro"] ?? ""));
$aulario = db_get_aulario($centro_id, $aulario_id);
if (!$aulario || !is_array($aulario)) {
?>
@@ -26,13 +27,14 @@ if (!$aulario || !is_array($aulario)) {
</div>
<div id="grid">
<a href="/entreaulas/paneldiario.php?aulario=<?= urlencode($aulario_id) ?>" class="btn btn-primary grid-item">
<a href="/aulatek/paneldiario.php?aulario=<?= urlencode($aulario_id) ?>" class="btn btn-primary grid-item">
<img src="/static/arasaac/pdi.png" height="125" style="background: white; padding: 5px; border-radius: 10px;">
</br>
Panel Diario
</a>
<?php if (in_array("entreaulas:docente", $_SESSION["auth_data"]["permissions"] ?? [])): ?>
<a href="/entreaulas/alumnos.php?aulario=<?= urlencode($aulario_id) ?>" class="btn btn-info grid-item">
<?php $permissions = $_SESSION["auth_data"]["permissions"] ?? []; ?>
<?php if (in_array("aulatek:docente", $permissions, true) || in_array("entreaulas:docente", $permissions, true)): ?>
<a href="/aulatek/alumnos.php?aulario=<?= urlencode($aulario_id) ?>" class="btn btn-info grid-item">
<img src="/static/arasaac/alumnos.png" height="125" style="background: white; padding: 5px; border-radius: 10px;" alt="Icono de gestión de alumnos">
<br>
Gestión de Alumnos
@@ -46,13 +48,13 @@ if (!$aulario || !is_array($aulario)) {
</a>
<?php endif; ?>
<!-- Menú del comedor -->
<a href="/entreaulas/comedor.php?aulario=<?= urlencode($aulario_id) ?>" class="btn btn-success grid-item">
<a href="/aulatek/comedor.php?aulario=<?= urlencode($aulario_id) ?>" class="btn btn-success grid-item">
<img src="/static/arasaac/comedor.png" height="125" style="background: white; padding: 5px; border-radius: 10px;">
<br>
Menú del Comedor
</a>
<!-- Proyectos -->
<a href="/entreaulas/proyectos.php?aulario=<?= urlencode($aulario_id) ?>" class="btn btn-warning grid-item">
<a href="/aulatek/proyectos.php?aulario=<?= urlencode($aulario_id) ?>" class="btn btn-warning grid-item">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="125" fill="currentColor">
<title>folder-multiple</title>
<path d="M22,4H14L12,2H6A2,2 0 0,0 4,4V16A2,2 0 0,0 6,18H22A2,2 0 0,0 24,16V6A2,2 0 0,0 22,4M2,6H0V11H0V20A2,2 0 0,0 2,22H20V20H2V6Z" />

View File

@@ -1,14 +1,17 @@
<?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) {
$permissions = $_SESSION["auth_data"]["permissions"] ?? [];
if (!in_array("aulatek:docente", $permissions, true) && !in_array("entreaulas:docente", $permissions, true)) {
header("HTTP/1.1 403 Forbidden");
die("Access denied");
}
$aulario_id = safe_id_segment($_GET["aulario"] ?? "");
$centro_id = safe_centro_id($_SESSION["auth_data"]["entreaulas"]["centro"] ?? "");
$tenant_data = $_SESSION["auth_data"]["aulatek"] ?? ($_SESSION["auth_data"]["entreaulas"] ?? []);
$centro_id = safe_organization_id($tenant_data["organizacion"] ?? ($tenant_data["centro"] ?? ""));
if ($aulario_id === "" || $centro_id === "") {
require_once "_incl/pre-body.php";
@@ -22,38 +25,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 +67,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 +86,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,15 +244,12 @@ 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));
header("Location: /entreaulas/comedor.php?aulario=" . urlencode($aulario_id) . "&date=" . urlencode($date) . "&menu=" . urlencode($newId));
db_set_comedor_menu_types($centro_id, $source_aulario_id, $menuTypes);
header("Location: /aulatek/comedor.php?aulario=" . urlencode($aulario_id) . "&date=" . urlencode($date) . "&menu=" . urlencode($newId));
exit;
}
}
@@ -268,21 +258,12 @@ 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));
header("Location: /aulatek/comedor.php?aulario=" . urlencode($aulario_id) . "&date=" . urlencode($date) . "&menu=" . urlencode($redirectMenuId));
exit;
}
}
@@ -296,16 +277,13 @@ 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));
header("Location: /entreaulas/comedor.php?aulario=" . urlencode($aulario_id) . "&date=" . urlencode($date) . "&menu=" . urlencode($renameId));
db_set_comedor_menu_types($centro_id, $source_aulario_id, $menuTypes);
header("Location: /aulatek/comedor.php?aulario=" . urlencode($aulario_id) . "&date=" . urlencode($date) . "&menu=" . urlencode($renameId));
exit;
}
}
@@ -316,23 +294,19 @@ if ($_SERVER["REQUEST_METHOD"] === "POST" && $canEdit) {
$menuData["menus"][$menuTypeId] = blank_menu();
}
// Pictogram images still stored on filesystem in Comedor dir
$baseDir = aulatek_orgs_base_path() . "/$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.";
}
}
@@ -346,24 +320,23 @@ function image_src($value, $centro_id, $source_aulario_id, $date)
if (filter_var($value, FILTER_VALIDATE_URL)) {
return $value;
}
return "/entreaulas/_filefetch.php?type=comedor_image&centro=" . urlencode($centro_id) . "&aulario=" . urlencode($source_aulario_id) . "&date=" . urlencode($date) . "&file=" . urlencode($value);
return "/aulatek/_filefetch.php?type=comedor_image&org=" . urlencode($centro_id) . "&aulario=" . urlencode($source_aulario_id) . "&date=" . urlencode($date) . "&file=" . urlencode($value);
}
$prevDate = (clone $dateObj)->modify("-1 day")->format("Y-m-d");
$nextDate = (clone $dateObj)->modify("+1 day")->format("Y-m-d");
$userAulas = $_SESSION["auth_data"]["entreaulas"]["aulas"] ?? [];
$userAulas = $tenant_data["aulas"] ?? [];
$aulaOptions = [];
foreach ($userAulas as $aulaId) {
$aulaIdSafe = safe_id_segment($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";
@@ -394,9 +367,9 @@ require_once "_incl/pre-body.php";
<!-- Navigation Buttons - Single row -->
<div class="card pad">
<div style="display: flex; gap: 10px; flex-wrap: wrap; align-items: center; justify-content: center; flex-direction: row;">
<a class="btn btn-outline-dark" href="/entreaulas/comedor.php?aulario=<?= urlencode($aulario_id) ?>&date=<?= urlencode($prevDate) ?>&menu=<?= urlencode($menuTypeId) ?>">⟵ Día anterior</a>
<a class="btn btn-outline-dark" href="/aulatek/comedor.php?aulario=<?= urlencode($aulario_id) ?>&date=<?= urlencode($prevDate) ?>&menu=<?= urlencode($menuTypeId) ?>">⟵ Día anterior</a>
<input type="date" id="datePicker" class="form-control form-control-lg" value="<?= htmlspecialchars($date) ?>" style="max-width: 200px;">
<a class="btn btn-outline-dark" href="/entreaulas/comedor.php?aulario=<?= urlencode($aulario_id) ?>&date=<?= urlencode($nextDate) ?>&menu=<?= urlencode($menuTypeId) ?>">Día siguiente ⟶</a>
<a class="btn btn-outline-dark" href="/aulatek/comedor.php?aulario=<?= urlencode($aulario_id) ?>&date=<?= urlencode($nextDate) ?>&menu=<?= urlencode($menuTypeId) ?>">Día siguiente ⟶</a>
</div>
<div style="margin-top: 10px; text-align: center;">
<label for="aularioPicker" class="form-label" style="margin-right: 10px;">Aulario:</label>
@@ -419,7 +392,7 @@ require_once "_incl/pre-body.php";
$isActive = ($type["id"] ?? "") === $menuTypeId;
$color = $type["color"] ?? "#0d6efd";
?>
<a href="/entreaulas/comedor.php?aulario=<?= urlencode($aulario_id) ?>&date=<?= urlencode($date) ?>&menu=<?= urlencode($type["id"]) ?>"
<a href="/aulatek/comedor.php?aulario=<?= urlencode($aulario_id) ?>&date=<?= urlencode($date) ?>&menu=<?= urlencode($type["id"]) ?>"
class="btn btn-lg" style="background: <?= htmlspecialchars($color) ?>; color: white; border: 3px solid <?= $isActive ? "#000" : "transparent" ?>;">
<?= htmlspecialchars($type["label"] ?? $type["id"]) ?>
</a>
@@ -644,7 +617,7 @@ require_once "_incl/pre-body.php";
params.set("date", dateValue);
params.set("aulario", aularioValue);
params.set("menu", "<?= htmlspecialchars($menuTypeId) ?>");
window.location.href = "/entreaulas/comedor.php?" + params.toString();
window.location.href = "/aulatek/comedor.php?" + params.toString();
}
if (datePicker) {
datePicker.addEventListener("change", goToSelection);

View File

@@ -3,13 +3,15 @@ require_once "_incl/auth_redir.php";
require_once "../_incl/tools.security.php";
// Check if user has docente permission
if (!in_array("entreaulas:docente", $_SESSION["auth_data"]["permissions"] ?? [])) {
$permissions = $_SESSION["auth_data"]["permissions"] ?? [];
if (!in_array("aulatek:docente", $permissions, true) && !in_array("entreaulas:docente", $permissions, true)) {
header("HTTP/1.1 403 Forbidden");
die("Acceso denegado");
}
$aulario_id = safe_id_segment($_GET["aulario"] ?? "");
$centro_id = safe_centro_id($_SESSION["auth_data"]["entreaulas"]["centro"] ?? "");
$tenant_data = $_SESSION["auth_data"]["aulatek"] ?? ($_SESSION["auth_data"]["entreaulas"] ?? []);
$centro_id = safe_organization_id($tenant_data["organizacion"] ?? ($tenant_data["centro"] ?? ""));
$alumno = safe_id_segment($_GET["alumno"] ?? "");
if (empty($aulario_id) || empty($centro_id)) {
@@ -25,7 +27,7 @@ if (empty($aulario_id) || empty($centro_id)) {
}
// Validate paths with realpath
$base_path = "/DATA/entreaulas/Centros";
$base_path = aulatek_orgs_base_path();
$real_base = realpath($base_path);
if ($real_base === false) {
@@ -77,7 +79,7 @@ if (empty($alumno)) {
<div class="card" style="padding: 0; overflow: hidden;">
<div style="background: #f8f9fa; padding: 15px; text-align: center;">
<?php if ($photo_exists): ?>
<img src="_filefetch.php?type=alumno_photo&alumno=<?= urlencode($alumno_name) ?>&centro=<?= urlencode($centro_id) ?>&aulario=<?= urlencode($aulario_id) ?>"
<img src="_filefetch.php?type=alumno_photo&alumno=<?= urlencode($alumno_name) ?>&org=<?= urlencode($centro_id) ?>&aulario=<?= urlencode($aulario_id) ?>"
alt="Foto de <?= htmlspecialchars($alumno_name) ?>"
style="width: 120px; height: 120px; object-fit: cover; border-radius: 10px; border: 3px solid #ddd;">
<?php else: ?>

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

@@ -2,42 +2,42 @@
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"];
$centro_id = safe_centro_id($user_data["entreaulas"]["centro"] ?? "");
foreach ($user_data["entreaulas"]["aulas"] as $aulario_id) {
<?php
$user_data = $_SESSION["auth_data"];
$tenant_data = $user_data["aulatek"] ?? ($user_data["entreaulas"] ?? []);
$centro_id = safe_organization_id($tenant_data["organizacion"] ?? ($tenant_data["centro"] ?? ""));
$user_aulas = $tenant_data["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;
$aulario_icon = $aulario["icon"] ?? "/static/arasaac/aulario.png";
echo '<a href="/entreaulas/aulario.php?id=' . urlencode($aulario_id) . '" class="btn btn-primary grid-item">
echo '<a href="/aulatek/aulario.php?id=' . urlencode($aulario_id) . '" class="btn btn-primary grid-item">
<img style="height: 125px;" src="' . htmlspecialchars($aulario_icon, ENT_QUOTES) . '" alt="' . htmlspecialchars($aulario_name) . ' Icono">
<br>
' . htmlspecialchars($aulario_name) . '
</a>';
} ?>
<?php if (in_array('supercafe:access', $_SESSION['auth_data']['permissions'] ?? [])): ?>
<a href="/entreaulas/supercafe.php" class="btn btn-warning grid-item">
<a href="/aulatek/supercafe.php" class="btn btn-warning grid-item">
<img src="/static/iconexperience/purchase_order_cart.png" height="125"
style="background: white; padding: 5px; border-radius: 10px;"
alt="Icono SuperCafe">
@@ -61,7 +61,6 @@ require_once "../_incl/tools.security.php";
}
</style>
<script>
var msnry = new Masonry('#grid', {
"columnWidth": 250,
@@ -69,10 +68,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
@@ -8,8 +9,8 @@ function getDiarioPath($alumno, $centro_id, $aulario_id) {
// Validate path components to avoid directory traversal or illegal characters
// Allow only alphanumeric, underscore and dash for alumno and aulario_id
$idPattern = '/^[A-Za-z0-9_-]+$/';
// Typically centro_id is numeric; restrict it accordingly
$centroPattern = '/^[0-9]+$/';
// Organization id may include letters, numbers, dots, underscores and dashes.
$centroPattern = '/^[A-Za-z0-9._-]+$/';
if (!preg_match($idPattern, (string)$alumno) ||
!preg_match($idPattern, (string)$aulario_id) ||
@@ -23,10 +24,12 @@ function getDiarioPath($alumno, $centro_id, $aulario_id) {
$centro_safe = basename($centro_id);
$aulario_safe = basename($aulario_id);
$base_path = "/DATA/entreaulas/Centros/$centro_safe/Aularios/$aulario_safe/Alumnos/$alumno_safe";
$base_path = aulatek_orgs_base_path() . "/$centro_safe/Aularios/$aulario_safe/Alumnos/$alumno_safe";
return $base_path . "/Diario/" . date("Y-m-d");
}
$tenant_data = $_SESSION["auth_data"]["aulatek"] ?? ($_SESSION["auth_data"]["entreaulas"] ?? []);
function initDiario($alumno, $centro_id, $aulario_id) {
$diario_path = getDiarioPath($alumno, $centro_id, $aulario_id);
if ($diario_path) {
@@ -72,14 +75,14 @@ function guardarPanelDiario($panel_name, $data, $alumno, $centro_id, $aulario_id
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_GET['api'])) {
header('Content-Type: application/json');
$api_action = $_GET['api'];
$alumno = $_SESSION["entreaulas_selected_alumno"] ?? '';
$centro_id = $_SESSION["auth_data"]["entreaulas"]["centro"] ?? '';
$alumno = $_SESSION["aulatek_selected_alumno"] ?? ($_SESSION["entreaulas_selected_alumno"] ?? '');
$centro_id = $tenant_data["organizacion"] ?? ($tenant_data["centro"] ?? '');
if ($api_action === 'guardar_panel' && $alumno && $centro_id) {
$input = json_decode(file_get_contents('php://input'), true);
$panel_name = $input['panel'] ?? '';
$panel_data = $input['data'] ?? [];
$aulario_id = $_SESSION["entreaulas_selected_aulario"] ?? '';
$aulario_id = $_SESSION["aulatek_selected_aulario"] ?? ($_SESSION["entreaulas_selected_aulario"] ?? '');
guardarPanelDiario($panel_name, $panel_data, $alumno, $centro_id, $aulario_id);
echo json_encode(['success' => true]);
die();
@@ -90,10 +93,12 @@ $form_action = $_GET["form"] ?? "";
switch ($form_action) {
case "alumno_selected":
$alumno = safe_id_segment($_GET["alumno"] ?? "");
$centro_id = safe_centro_id($_SESSION["auth_data"]["entreaulas"]["centro"] ?? "");
$centro_id = safe_organization_id($tenant_data["organizacion"] ?? ($tenant_data["centro"] ?? ""));
$aulario_id = safe_id_segment($_GET["aulario"] ?? '');
$photo_url = $_GET["photo"] ?? '';
if ($alumno !== "" && $centro_id !== "" && $aulario_id !== "") {
$_SESSION["aulatek_selected_alumno"] = $alumno;
$_SESSION["aulatek_selected_aulario"] = $aulario_id;
$_SESSION["entreaulas_selected_alumno"] = $alumno;
$_SESSION["entreaulas_selected_aulario"] = $aulario_id;
initDiario($alumno, $centro_id, $aulario_id);
@@ -208,8 +213,8 @@ ini_set("display_errors", "0");
</script>
<?php
// Verificar si hay un alumno seleccionado y cargar su progreso
$alumno_actual = $_SESSION["entreaulas_selected_alumno"] ?? '';
$centro_id = safe_centro_id($_SESSION["auth_data"]["entreaulas"]["centro"] ?? '');
$alumno_actual = $_SESSION["aulatek_selected_alumno"] ?? ($_SESSION["entreaulas_selected_alumno"] ?? '');
$centro_id = safe_organization_id($tenant_data["organizacion"] ?? ($tenant_data["centro"] ?? ''));
$aulario_id = safe_id_segment($_GET["aulario"] ?? '');
$diario_data = null;
@@ -603,7 +608,7 @@ switch ($view_action) {
case "quien_soy":
// ¿Quién soy? - Identificación del alumno
$aulario_id = safe_id_segment($_GET["aulario"] ?? '');
$centro_id = safe_centro_id($_SESSION["auth_data"]["entreaulas"]["centro"] ?? '');
$centro_id = safe_organization_id($tenant_data["organizacion"] ?? ($tenant_data["centro"] ?? ''));
// Validate parameters
if (empty($aulario_id) || empty($centro_id)) {
@@ -611,7 +616,7 @@ switch ($view_action) {
break;
}
$base_path = "/DATA/entreaulas/Centros";
$base_path = aulatek_orgs_base_path();
$alumnos_path = "$base_path/$centro_id/Aularios/$aulario_id/Alumnos";
// Validate the path is within the expected directory
@@ -628,12 +633,12 @@ switch ($view_action) {
element.style.backgroundColor = "#9cff9f"; // Verde
let photoUrl = '';
if (hasPhoto) {
photoUrl = '/entreaulas/_filefetch.php?type=alumno_photo&alumno=' + encodeURIComponent(nombre) +
photoUrl = '/aulatek/_filefetch.php?type=alumno_photo&alumno=' + encodeURIComponent(nombre) +
'&centro=' + encodeURIComponent(centro) + '&aulario=' + encodeURIComponent(aulario);
}
announceAndMaybeRedirect(
"¡Hola " + nombre + "!",
"/entreaulas/paneldiario.php?aulario=" + encodeURIComponent(aulario) + "&form=alumno_selected&alumno=" + encodeURIComponent(nombre) +
"/aulatek/paneldiario.php?aulario=" + encodeURIComponent(aulario) + "&form=alumno_selected&alumno=" + encodeURIComponent(nombre) +
(photoUrl ? "&photo=" + encodeURIComponent(photoUrl) : ''),
true
);
@@ -661,7 +666,7 @@ switch ($view_action) {
?>
<a href="#" class="card grid-item" style="color: black;" onclick='seleccionarAlumno(this, "<?php echo htmlspecialchars($alumno_name, ENT_QUOTES); ?>", <?php echo $photo_exists ? 'true' : 'false'; ?>, "<?php echo htmlspecialchars($centro_id, ENT_QUOTES); ?>", "<?php echo htmlspecialchars($aulario_id, ENT_QUOTES); ?>");' aria-label="Seleccionar alumno <?php echo htmlspecialchars($alumno_name); ?>">
<?php if ($photo_exists): ?>
<img src="_filefetch.php?type=alumno_photo&alumno=<?php echo urlencode($alumno_name); ?>&centro=<?php echo urlencode($centro_id); ?>&aulario=<?php echo urlencode($aulario_id); ?>" height="150" class="bg-white" alt="Foto de <?php echo htmlspecialchars($alumno_name); ?>">
<img src="_filefetch.php?type=alumno_photo&alumno=<?php echo urlencode($alumno_name); ?>&org=<?php echo urlencode($centro_id); ?>&aulario=<?php echo urlencode($aulario_id); ?>" height="150" class="bg-white" alt="Foto de <?php echo htmlspecialchars($alumno_name); ?>">
<?php else: ?>
<div style="width: 150px; height: 150px; background: #f0f0f0; display: flex; align-items: center; justify-content: center; border-radius: 10px; border: 2px dashed #ccc;">
<span style="font-size: 48px;">?</span>
@@ -704,10 +709,10 @@ switch ($view_action) {
<?php
break;
case "actividades":
$centro_actividades = safe_centro_id($_SESSION["auth_data"]["entreaulas"]["centro"] ?? '');
$centro_actividades = safe_organization_id($tenant_data["organizacion"] ?? ($tenant_data["centro"] ?? ''));
$actividades = [];
if ($centro_actividades !== '') {
$actividades_path = "/DATA/entreaulas/Centros/$centro_actividades/Panel/Actividades";
$actividades_path = aulatek_orgs_base_path() . "/$centro_actividades/Panel/Actividades";
if (is_dir($actividades_path)) {
$actividades = glob($actividades_path . "/*", GLOB_ONLYDIR) ?: [];
}
@@ -718,7 +723,7 @@ switch ($view_action) {
element.style.backgroundColor = "#9cff9f"; // Verde
// Guardar al diario antes de redirigir
fetch('/entreaulas/paneldiario.php?api=guardar_panel', {
fetch('/aulatek/paneldiario.php?api=guardar_panel', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -731,7 +736,7 @@ switch ($view_action) {
}).finally(() => {
announceAndMaybeRedirect(
actividad + ", Actividad seleccionada",
"/entreaulas/paneldiario.php?aulario=<?php echo urlencode($_GET['aulario'] ?? ''); ?>",
"/aulatek/paneldiario.php?aulario=<?php echo urlencode($_GET['aulario'] ?? ''); ?>",
true
);
});
@@ -745,7 +750,7 @@ switch ($view_action) {
<div class="grid">
<?php foreach ($actividades as $actividad_path) {
$actividad_name = basename($actividad_path);
$pictogram_url = '/entreaulas/_filefetch.php?type=panel_actividades&activity=' . urlencode($actividad_name) . '&centro=' . urlencode($centro_actividades);
$pictogram_url = '/aulatek/_filefetch.php?type=panel_actividades&activity=' . urlencode($actividad_name) . '&org=' . urlencode($centro_actividades);
?>
<a class="card grid-item" style="color: black;" onclick="seleccionarActividad(this, '<?php echo htmlspecialchars($actividad_name); ?>', '<?php echo htmlspecialchars($pictogram_url); ?>');">
<img src="<?php echo htmlspecialchars($pictogram_url); ?>" height="125" class="bg-white">
@@ -779,17 +784,15 @@ switch ($view_action) {
case "menu":
// Menú del comedor (nuevo sistema, vista simplificada)
$aulario_id = safe_id_segment($_GET["aulario"] ?? '');
$centro_id = safe_centro_id($_SESSION["auth_data"]["entreaulas"]["centro"] ?? "");
$centro_id = safe_organization_id($tenant_data["organizacion"] ?? ($tenant_data["centro"] ?? ""));
$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 +803,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 +824,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);
}
}
@@ -843,7 +841,7 @@ switch ($view_action) {
if (!$value) {
return "";
}
return "/entreaulas/_filefetch.php?type=comedor_image&centro=" . urlencode($centro_id) . "&aulario=" . urlencode($aulario_id) . "&date=" . urlencode($date) . "&file=" . urlencode($value);
return "/aulatek/_filefetch.php?type=comedor_image&org=" . urlencode($centro_id) . "&aulario=" . urlencode($aulario_id) . "&date=" . urlencode($date) . "&file=" . urlencode($value);
}
?>
<script>
@@ -851,7 +849,7 @@ switch ($view_action) {
element.style.backgroundColor = "#9cff9f"; // Verde
// Guardar al diario antes de redirigir
fetch('/entreaulas/paneldiario.php?api=guardar_panel', {
fetch('/aulatek/paneldiario.php?api=guardar_panel', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -861,7 +859,7 @@ switch ($view_action) {
}).finally(() => {
announceAndMaybeRedirect(
menu_ty + ", Menú seleccionado",
"/entreaulas/paneldiario.php?aulario=<?php echo urlencode($_GET['aulario'] ?? ''); ?>",
"/aulatek/paneldiario.php?aulario=<?php echo urlencode($_GET['aulario'] ?? ''); ?>",
true
);
});
@@ -1034,7 +1032,7 @@ switch ($view_action) {
element.style.backgroundColor = "#9cff9f"; // Verde
// Guardar al diario antes de redirigir
fetch('/entreaulas/paneldiario.php?api=guardar_panel', {
fetch('/aulatek/paneldiario.php?api=guardar_panel', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -1142,7 +1140,7 @@ switch ($view_action) {
element.style.backgroundColor = "#9cff9f"; // Verde
// Guardar al diario antes de redirigir
fetch('/entreaulas/paneldiario.php?api=guardar_panel', {
fetch('/aulatek/paneldiario.php?api=guardar_panel', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -1152,7 +1150,7 @@ switch ($view_action) {
}).finally(() => {
announceAndMaybeRedirect(
dow[ds] + ", Correcto",
"/entreaulas/paneldiario.php?action=calendario_mes&aulario=<?php echo urlencode($_GET['aulario'] ?? ''); ?>",
"/aulatek/paneldiario.php?action=calendario_mes&aulario=<?php echo urlencode($_GET['aulario'] ?? ''); ?>",
true
);
});
@@ -1260,7 +1258,7 @@ switch ($view_action) {
element.style.backgroundColor = "#9cff9f"; // Verde
// Guardar al diario antes de redirigir
fetch('/entreaulas/paneldiario.php?api=guardar_panel', {
fetch('/aulatek/paneldiario.php?api=guardar_panel', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -1270,7 +1268,7 @@ switch ($view_action) {
}).finally(() => {
announceAndMaybeRedirect(
meses[mes] + ", Correcto",
"/entreaulas/paneldiario.php?aulario=<?php echo urlencode($_GET['aulario'] ?? ''); ?>",
"/aulatek/paneldiario.php?aulario=<?php echo urlencode($_GET['aulario'] ?? ''); ?>",
true
);
});

View File

@@ -8,7 +8,7 @@ $is_valid = false;
if ($file !== "") {
$parsed = parse_url($file);
if (!isset($parsed["scheme"]) && !isset($parsed["host"])) {
if (strpos($file, "/entreaulas/_filefetch.php") === 0) {
if (strpos($file, "/aulatek/_filefetch.php") === 0) {
$is_valid = true;
}
}

View File

@@ -1,14 +1,17 @@
<?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) {
$permissions = $_SESSION["auth_data"]["permissions"] ?? [];
if (!in_array("aulatek:docente", $permissions, true) && !in_array("entreaulas:docente", $permissions, true)) {
header("HTTP/1.1 403 Forbidden");
die("Access denied");
}
$aulario_id = safe_path_segment($_GET["aulario"] ?? "");
$centro_id = safe_path_segment($_SESSION["auth_data"]["entreaulas"]["centro"] ?? "");
$tenant_data = $_SESSION["auth_data"]["aulatek"] ?? ($_SESSION["auth_data"]["entreaulas"] ?? []);
$centro_id = safe_path_segment($tenant_data["organizacion"] ?? ($tenant_data["centro"] ?? ""));
// Validate centro_id and aulario_id to prevent directory traversal
if ($aulario_id === "" || $centro_id === "") {
@@ -23,10 +26,9 @@ 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";
$proyectos_dir = aulatek_orgs_base_path() . "/$centro_id/Proyectos";
if (!is_dir($proyectos_dir)) {
mkdir($proyectos_dir, 0755, true);
}
@@ -71,16 +73,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 ?? "");
@@ -367,7 +359,7 @@ function get_linked_projects($aulario, $centro_id)
continue;
}
$projects_base = "/DATA/entreaulas/Centros/$centro_id/Proyectos";
$projects_base = aulatek_orgs_base_path() . "/$centro_id/Proyectos";
$project = load_project($projects_base, $project_id);
if ($project && ($project["parent_id"] ?? null) === null) {
// Mark as linked and add source info
@@ -450,7 +442,7 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
}
}
header("Location: /entreaulas/proyectos.php?aulario=" . urlencode($aulario_id) . "&project=" . urlencode($project_id));
header("Location: /aulatek/proyectos.php?aulario=" . urlencode($aulario_id) . "&project=" . urlencode($project_id));
exit;
}
} else {
@@ -459,23 +451,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,20 +481,30 @@ 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 org_id = ? AND aulario_id = ?"
)->execute([json_encode($extra, JSON_UNESCAPED_UNICODE), $centro_id, $target_aulario]);
}
}
}
}
if ($action === "delete_project") {
if (in_array("entreaulas:proyectos:delete", $_SESSION["auth_data"]["permissions"] ?? []) === false) {
if (!in_array("aulatek:proyectos:delete", $permissions, true) && !in_array("entreaulas:proyectos:delete", $permissions, true)) {
$error = "No tienes permisos para borrar proyectos.";
} else {
$project_id = safe_path_segment($_POST["project_id"] ?? "");
@@ -766,7 +763,7 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
if (!empty($source_aulario_param)) {
$redirect_params .= "&source=" . urlencode($source_aulario_param);
}
header("Location: /entreaulas/proyectos.php?" . $redirect_params);
header("Location: /aulatek/proyectos.php?" . $redirect_params);
exit;
}
}
@@ -1030,7 +1027,7 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
if (!empty($source_aulario_param)) {
$redirect_params .= "&source=" . urlencode($source_aulario_param);
}
header("Location: /entreaulas/proyectos.php?" . $redirect_params);
header("Location: /aulatek/proyectos.php?" . $redirect_params);
exit;
}
}
@@ -1060,7 +1057,7 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
if (!empty($source_aulario_param)) {
$redirect_params .= "&source=" . urlencode($source_aulario_param);
}
header("Location: /entreaulas/proyectos.php?" . $redirect_params);
header("Location: /aulatek/proyectos.php?" . $redirect_params);
exit;
}
}
@@ -1180,7 +1177,7 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
if (!empty($source_aulario_param)) {
$redirect_params .= "&source=" . urlencode($source_aulario_param);
}
header("Location: /entreaulas/proyectos.php?" . $redirect_params);
header("Location: /aulatek/proyectos.php?" . $redirect_params);
exit;
}
}
@@ -1382,11 +1379,11 @@ $view = $current_project ? "project" : "list";
</p>
<div class="d-flex gap-2">
<?php if ($is_linked): ?>
<a href="/entreaulas/proyectos.php?aulario=<?= urlencode($aulario_id) ?>&project=<?= urlencode($project["id"]) ?>&source=<?= urlencode($source_aulario) ?>" class="btn btn-primary">
<a href="/aulatek/proyectos.php?aulario=<?= urlencode($aulario_id) ?>&project=<?= urlencode($project["id"]) ?>&source=<?= urlencode($source_aulario) ?>" class="btn btn-primary">
Abrir
</a>
<?php else: ?>
<a href="/entreaulas/proyectos.php?aulario=<?= urlencode($aulario_id) ?>&project=<?= urlencode($project["id"]) ?>" class="btn btn-primary">
<a href="/aulatek/proyectos.php?aulario=<?= urlencode($aulario_id) ?>&project=<?= urlencode($project["id"]) ?>" class="btn btn-primary">
Abrir
</a>
<!-- Delete -->
@@ -1476,7 +1473,7 @@ $view = $current_project ? "project" : "list";
$linked_permission = $link["permission"] ?? "read_only";
break;
}
$project_source_dir = "/DATA/entreaulas/Centros/$centro_id/Proyectos";
$project_source_dir = aulatek_orgs_base_path() . "/$centro_id/Proyectos";
$breadcrumb_check = get_project_breadcrumb($project_source_dir, $current_project);
foreach ($breadcrumb_check as $crumb) {
if (($crumb["id"] ?? "") === $link_root_id) {
@@ -1489,7 +1486,7 @@ $view = $current_project ? "project" : "list";
if ($valid_link) {
$is_linked_project = true;
$project_source_dir = "/DATA/entreaulas/Centros/$centro_id/Proyectos";
$project_source_dir = aulatek_orgs_base_path() . "/$centro_id/Proyectos";
$project = load_project($project_source_dir, $current_project);
} else {
// Invalid link configuration, treat as local project
@@ -1513,7 +1510,7 @@ $view = $current_project ? "project" : "list";
<div class="card pad">
<h1>Error</h1>
<p>Proyecto no encontrado.</p>
<a href="/entreaulas/proyectos.php?aulario=<?= urlencode($aulario_id) ?>" class="btn btn-primary">Volver a Proyectos</a>
<a href="/aulatek/proyectos.php?aulario=<?= urlencode($aulario_id) ?>" class="btn btn-primary">Volver a Proyectos</a>
</div>
<?php
require_once "_incl/post-body.php";
@@ -1533,12 +1530,12 @@ $view = $current_project ? "project" : "list";
<div class="card pad">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="/entreaulas/proyectos.php?aulario=<?= urlencode($aulario_id) ?>">Proyectos</a>
<a href="/aulatek/proyectos.php?aulario=<?= urlencode($aulario_id) ?>">Proyectos</a>
</li>
<?php foreach ($breadcrumb as $idx => $crumb): ?>
<?php if ($idx < count($breadcrumb) - 1): ?>
<li class="breadcrumb-item">
<a href="/entreaulas/proyectos.php?aulario=<?= urlencode($aulario_id) ?>&project=<?= urlencode($crumb["id"]) ?><?= $is_linked_project ? "&source=" . urlencode($source_aulario_for_project) : "" ?>">
<a href="/aulatek/proyectos.php?aulario=<?= urlencode($aulario_id) ?>&project=<?= urlencode($crumb["id"]) ?><?= $is_linked_project ? "&source=" . urlencode($source_aulario_for_project) : "" ?>">
<?= htmlspecialchars($crumb["name"]) ?>
</a>
</li>
@@ -1570,10 +1567,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"] ?? "";
}
?>
@@ -1587,11 +1583,11 @@ $view = $current_project ? "project" : "list";
</div>
<div class="d-flex gap-2">
<?php if (!empty($project["parent_id"])): ?>
<a href="/entreaulas/proyectos.php?aulario=<?= urlencode($aulario_id) ?>&project=<?= urlencode($project["parent_id"]) ?><?= $is_linked_project ? "&source=" . urlencode($source_aulario_for_project) : "" ?>" class="btn btn-secondary">
<a href="/aulatek/proyectos.php?aulario=<?= urlencode($aulario_id) ?>&project=<?= urlencode($project["parent_id"]) ?><?= $is_linked_project ? "&source=" . urlencode($source_aulario_for_project) : "" ?>" class="btn btn-secondary">
← Volver al Proyecto
</a>
<?php else: ?>
<a href="/entreaulas/proyectos.php?aulario=<?= urlencode($aulario_id) ?>" class="btn btn-secondary">
<a href="/aulatek/proyectos.php?aulario=<?= urlencode($aulario_id) ?>" class="btn btn-secondary">
← Volver al Listado
</a>
<?php endif; ?>
@@ -1636,27 +1632,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;
}
@@ -1728,7 +1711,7 @@ $view = $current_project ? "project" : "list";
</small>
</p>
<div class="d-flex gap-2">
<a href="/entreaulas/proyectos.php?aulario=<?= urlencode($aulario_id) ?>&project=<?= urlencode($subproject["id"]) ?><?= $is_linked_project ? "&source=" . urlencode($source_aulario_for_project) : "" ?>" class="btn btn-primary">
<a href="/aulatek/proyectos.php?aulario=<?= urlencode($aulario_id) ?>&project=<?= urlencode($subproject["id"]) ?><?= $is_linked_project ? "&source=" . urlencode($source_aulario_for_project) : "" ?>" class="btn btn-primary">
Abrir
</a>
<?php if (!$is_linked_project || ($is_linked_project && $linked_permission === "full_edit")): ?>
@@ -1833,7 +1816,7 @@ $view = $current_project ? "project" : "list";
<?php elseif ($item["type"] === "pdf_secure"): ?>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#viewPdfSecureModal"
data-item-name="<?= htmlspecialchars($item["name"], ENT_QUOTES) ?>"
data-file-url="/entreaulas/_filefetch.php?type=proyecto_file&centro=<?= urlencode($centro_id) ?>&project=<?= urlencode($current_project) ?>&file=<?= urlencode($item["filename"]) ?>">
data-file-url="/aulatek/_filefetch.php?type=proyecto_file&org=<?= urlencode($centro_id) ?>&project=<?= urlencode($current_project) ?>&file=<?= urlencode($item["filename"]) ?>">
Abrir
</button>
<?php elseif ($item["type"] === "videocall"): ?>
@@ -1847,7 +1830,7 @@ $view = $current_project ? "project" : "list";
Abrir
</button>
<?php else: ?>
<a href="/entreaulas/_filefetch.php?type=proyecto_file&centro=<?= urlencode($centro_id) ?>&project=<?= urlencode($current_project) ?>&file=<?= urlencode($item["filename"]) ?>" target="_blank" class="btn btn-primary">
<a href="/aulatek/_filefetch.php?type=proyecto_file&org=<?= urlencode($centro_id) ?>&project=<?= urlencode($current_project) ?>&file=<?= urlencode($item["filename"]) ?>" target="_blank" class="btn btn-primary">
Abrir
</a>
<?php endif; ?>
@@ -1918,10 +1901,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";
?>
@@ -2579,7 +2560,7 @@ $view = $current_project ? "project" : "list";
var title = button.getAttribute('data-item-name') || 'PDF Seguro';
var url = button.getAttribute('data-file-url') || '';
document.getElementById('viewPdfSecureModalLabel').textContent = title;
document.getElementById('pdf_secure_frame').src = url ? ('/entreaulas/pdf_secure_viewer.php?file=' + encodeURIComponent(url)) : '';
document.getElementById('pdf_secure_frame').src = url ? ('/aulatek/pdf_secure_viewer.php?file=' + encodeURIComponent(url)) : '';
});
viewPdfSecureModal.addEventListener('hidden.bs.modal', function() {
document.getElementById('pdf_secure_frame').src = '';

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 = aulatek_orgs_base_path() . "/$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,
@@ -54,15 +50,15 @@ function sc_persona_label($persona_key, $personas)
return $persona_key;
}
$centro_id = safe_centro_id($_SESSION['auth_data']['entreaulas']['centro'] ?? '');
$tenant_data = $_SESSION['auth_data']['aulatek'] ?? ($_SESSION['auth_data']['entreaulas'] ?? []);
$centro_id = safe_organization_id($tenant_data['organizacion'] ?? ($tenant_data['centro'] ?? ''));
if ($centro_id === '') {
require_once "_incl/pre-body.php";
echo '<div class="card pad"><h1>SuperCafe</h1><p>No tienes un centro asignado.</p></div>';
echo '<div class="card pad"><h1>SuperCafe</h1><p>No tienes una organizacion asignada.</p></div>';
require_once "_incl/post-body.php";
exit;
}
define('SC_DATA_DIR', "/DATA/entreaulas/Centros/$centro_id/SuperCafe/Comandas");
define('SC_MAX_DEBTS', 3);
$estados_colores = [
@@ -81,57 +77,48 @@ 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');
header('Location: /aulatek/supercafe.php');
exit;
}
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 org_id = ? AND order_ref = ?')
->execute([$centro_id, $order_id]);
}
header('Location: /entreaulas/supercafe.php');
header('Location: /aulatek/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,11 +126,12 @@ $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>
<?php if ($can_edit): ?>
<a href="/entreaulas/supercafe_edit.php" class="btn btn-success">+ Nueva comanda</a>
<a href="/aulatek/supercafe_edit.php" class="btn btn-success">+ Nueva comanda</a>
<?php endif; ?>
</div>
</div>
@@ -183,7 +171,7 @@ require_once "_incl/pre-body.php";
<td><strong><?= htmlspecialchars($estado) ?></strong></td>
<?php if ($can_edit): ?>
<td style="white-space: nowrap;">
<a href="/entreaulas/supercafe_edit.php?id=<?= urlencode($order['_id']) ?>"
<a href="/aulatek/supercafe_edit.php?id=<?= urlencode($order['_id']) ?>"
class="btn btn-sm btn-primary">Editar</a>
<form method="post" style="display: inline;">
<input type="hidden" name="action" value="change_status">
@@ -246,7 +234,7 @@ require_once "_incl/pre-body.php";
<td><?= htmlspecialchars($order['Notas'] ?? '') ?></td>
<?php if ($can_edit): ?>
<td style="white-space: nowrap;">
<a href="/entreaulas/supercafe_edit.php?id=<?= urlencode($order['_id']) ?>"
<a href="/aulatek/supercafe_edit.php?id=<?= urlencode($order['_id']) ?>"
class="btn btn-sm btn-primary">Editar</a>
<form method="post" style="display: inline;">
<input type="hidden" name="action" value="change_status">

View File

@@ -1,56 +1,46 @@
<?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');
die('Acceso denegado');
}
$centro_id = safe_centro_id($_SESSION['auth_data']['entreaulas']['centro'] ?? '');
$tenant_data = $_SESSION['auth_data']['aulatek'] ?? ($_SESSION['auth_data']['entreaulas'] ?? []);
$centro_id = safe_organization_id($tenant_data['organizacion'] ?? ($tenant_data['centro'] ?? ''));
if ($centro_id === '') {
require_once "_incl/pre-body.php";
echo '<div class="card pad"><h1>SuperCafe</h1><p>No tienes un centro asignado.</p></div>';
echo '<div class="card pad"><h1>SuperCafe</h1><p>No tienes una organizacion asignada.</p></div>';
require_once "_incl/post-body.php";
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 = aulatek_orgs_base_path() . "/$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 +52,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 +67,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 +100,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,52 +117,231 @@ 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: /aulatek/supercafe.php');
exit;
}
}
}
require_once "_incl/pre-body.php";
?>
<h1>Comanda <code><?= htmlspecialchars($order_id) ?></code></h1>
<a href="/entreaulas/supercafe.php" class="btn btn-secondary">Salir</a>
?>
<style>
.sc-legacy-wrap {
max-width: 320px;
}
.sc-legacy-title {
font-size: 1.9rem;
margin: 0 0 0.2rem;
font-weight: 700;
}
.sc-legacy-subtitle {
font-size: 0.85rem;
font-weight: 600;
margin-bottom: 0.35rem;
}
.sc-legacy-exit {
padding: 2px 8px;
border: 1px solid #111;
border-radius: 2px;
background: #fff;
color: #111;
text-decoration: none;
display: inline-block;
margin-bottom: 0.35rem;
font-size: 0.82rem;
}
.sc-legacy-fieldset {
border: 1px solid #bcbcbc;
border-radius: 0;
padding: 6px;
background: #fff;
}
.sc-legacy-fieldset legend {
font-size: 0.95rem;
font-weight: 700;
margin-bottom: 0;
}
.sc-legacy-persona-photo img {
height: 64px;
border-radius: 10px;
border: 2px solid #777;
background: #fff;
}
.sc-persona-current {
display: flex;
align-items: center;
gap: 7px;
margin-top: 5px;
min-height: 36px;
font-size: 0.8rem;
}
.sc-persona-current img {
width: 32px;
height: 32px;
border-radius: 8px;
border: 1px solid #666;
background: #fff;
object-fit: cover;
}
.sc-persona-group {
margin-top: 5px;
padding-top: 3px;
border-top: 1px dashed #ddd;
}
.sc-persona-group-label {
font-size: 0.72rem;
color: #555;
margin-bottom: 3px;
font-weight: 700;
}
.sc-person-btn {
border: 5px solid transparent;
outline: 2px solid black;
border-radius: 10px;
background: #fff;
color: #111;
min-width: 86px;
max-width: 125px;
min-height: 54px;
padding: 4px 6px;
font-size: 0.75rem;
line-height: 1.05;
text-align: center;
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
}
.sc-person-btn img {
width: 24px;
height: 24px;
border-radius: 7px;
border: 1px solid #777;
object-fit: cover;
background: #fff;
}
.sc-person-btn.active {
background: #d9ff1f;
border: 5px dashed #000;
outline: 2px solid black;
}
.sc-details {
margin: 4px 0;
border: 1px solid #444;
border-radius: 3px;
background: #fff;
width: 100%;
}
.sc-details summary {
list-style: none;
cursor: pointer;
font-size: 0.86rem;
padding: 4px 6px;
background: #f3f3f3;
border-bottom: 1px solid #ddd;
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
}
.sc-details summary::-webkit-details-marker {
display: none;
}
.sc-summary-left {
display: flex;
align-items: center;
gap: 5px;
min-width: 0;
}
.sc-summary-left img {
height: 18px;
width: 18px;
object-fit: contain;
}
.sc-summary-right {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.sc-selected-val {
max-width: 95px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.78rem;
color: #444;
}
.sc-check {
height: 15px;
}
.sc-category-body {
padding: 4px;
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.sc-cat-btn {
border: 5px solid transparent;
outline: 2px solid black;
border-radius: 9px;
background: #d9ff1f;
color: #111;
min-width: 90px;
min-height: 44px;
padding: 3px 6px;
font-size: 0.78rem;
line-height: 1.1;
text-align: center;
}
.sc-cat-btn small {
font-size: 0.68rem;
}
.sc-cat-btn.active {
outline: 2px solid black;
border: 5px dashed #000;
}
.sc-cat-btn.sc-size-btn {
background: #ff3030;
color: #fff;
}
.sc-notes {
width: 100%;
min-height: 78px;
font-size: 0.85rem;
}
.sc-actions {
display: flex;
gap: 6px;
justify-content: flex-start;
margin-top: 6px;
}
.sc-actions .btn {
border-radius: 2px;
padding: 3px 9px;
font-size: 0.82rem;
}
</style>
<div class="sc-legacy-wrap">
<h1 class="sc-legacy-title">Comanda <small style="font-size:.72rem;color:#666;"><?= htmlspecialchars($order_id) ?></small></h1>
<a href="/aulatek/supercafe.php" class="sc-legacy-exit">Salir</a>
<?php if ($error !== ''): ?>
<div class="card pad" style="background: #f8d7da; color: #842029;">
@@ -206,56 +350,84 @@ require_once "_incl/pre-body.php";
<?php endif; ?>
<form method="post">
<fieldset class="card pad" style="text-align: center;">
<legend>Rellenar comanda</legend>
<fieldset class="sc-legacy-fieldset">
<legend>Rellenar comanda</legend>
<label style="display: none;">
Fecha<br>
<input readonly disabled type="text" value="<?= htmlspecialchars($order_data['Fecha']) ?>"><br><br>
</label>
<label>
<label style="display:block; margin-bottom:6px;">
Persona<br>
<?php if (!empty($personas_by_aulario)): ?>
<select name="Persona" class="form-select" required>
<option value="">-- Selecciona una persona --</option>
<?php foreach ($personas_by_aulario as $region_name => $group): ?>
<optgroup label="<?= htmlspecialchars($region_name) ?>">
<?php foreach ($group as $pkey => $pinfo): ?>
<option value="<?= htmlspecialchars($pkey) ?>"
<?= ($order_data['Persona'] === $pkey) ? 'selected' : '' ?>>
<?= htmlspecialchars($pinfo['Nombre']) ?>
</option>
<?php endforeach; ?>
</optgroup>
<?php endforeach; ?>
</select>
<?php
$sel_key = $order_data['Persona'];
$sel_info = $personas[$sel_key] ?? null;
if ($sel_info && $sel_info['HasPhoto']):
?>
<div id="sc-persona-photo" style="margin-top: 8px;">
<?php $photo_url = '/entreaulas/_filefetch.php?type=alumno_photo'
. '&centro=' . urlencode($centro_id)
<details class="sc-details" open>
<summary>
<span class="sc-summary-left">
<span>Persona</span>
</span>
<span class="sc-summary-right">
<span class="sc-selected-val" id="sc-persona-selected-label"><?= htmlspecialchars($sel_info['Nombre'] ?? '') ?></span>
<img class="sc-check" src="static/ico/<?= $sel_info ? 'checkbox.png' : 'checkbox_unchecked.png' ?>" id="sc-persona-check" alt="">
</span>
</summary>
<div class="sc-category-body" id="sc-persona-panel">
<?php foreach ($personas_by_aulario as $region_name => $group): ?>
<div class="sc-persona-group" style="width:100%;">
<div class="sc-persona-group-label"><?= htmlspecialchars($region_name) ?></div>
<div style="display:flex; flex-wrap:wrap; gap:4px;">
<?php foreach ($group as $pkey => $pinfo): ?>
<?php
$p_photo_url = '/aulatek/_filefetch.php?type=alumno_photo'
. '&org=' . urlencode($centro_id)
. '&aulario=' . urlencode($pinfo['AularioID'])
. '&alumno=' . urlencode($pinfo['Nombre']);
$is_person_active = ($order_data['Persona'] === $pkey);
?>
<button type="button"
class="sc-person-btn<?= $is_person_active ? ' active' : '' ?>"
data-person-key="<?= htmlspecialchars($pkey, ENT_QUOTES) ?>"
data-person-name="<?= htmlspecialchars($pinfo['Nombre'], ENT_QUOTES) ?>"
data-person-region="<?= htmlspecialchars($region_name, ENT_QUOTES) ?>"
data-person-photo="<?= htmlspecialchars($pinfo['HasPhoto'] ? $p_photo_url : '/static/arasaac/alumnos.png', ENT_QUOTES) ?>"
onclick="scSelectPersona(this)">
<img src="<?= htmlspecialchars($pinfo['HasPhoto'] ? $p_photo_url : '/static/arasaac/alumnos.png') ?>" alt="">
<span><?= htmlspecialchars($pinfo['Nombre']) ?></span>
</button>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</details>
<input type="hidden" name="Persona" id="sc-persona-input" value="<?= htmlspecialchars($order_data['Persona']) ?>">
<div class="sc-persona-current" id="sc-persona-current" style="display: none;">
<?php if ($sel_info): ?>
<?php $photo_url = '/aulatek/_filefetch.php?type=alumno_photo'
. '&org=' . urlencode($centro_id)
. '&aulario=' . urlencode($sel_info['AularioID'])
. '&alumno=' . urlencode($sel_info['Nombre']); ?>
<img src="<?= htmlspecialchars($photo_url) ?>"
alt="Foto de <?= htmlspecialchars($sel_info['Nombre']) ?>"
style="height: 80px; border-radius: 8px; border: 2px solid #dee2e6;">
</div>
<?php endif; ?>
<img src="<?= htmlspecialchars($sel_info['HasPhoto'] ? $photo_url : '/static/arasaac/alumnos.png') ?>" alt="Foto">
<span><strong><?= htmlspecialchars($sel_info['Nombre']) ?></strong> (<?= htmlspecialchars($sel_info['Region']) ?>)</span>
<?php else: ?>
<span style="color:#666;">No hay persona seleccionada.</span>
<?php endif; ?>
</div>
<?php else: ?>
<input type="text" name="Persona" class="form-control"
value="<?= htmlspecialchars($order_data['Persona']) ?>"
placeholder="Nombre de la persona" required>
<small class="text-muted">
No hay alumnos registrados en los aularios de este centro.
No hay alumnos registrados en los aularios de esta organizacion.
Añade alumnos desde
<a href="/entreaulas/">EntreAulas</a>.
<a href="/aulatek/">AulaTek</a>.
</small>
<?php endif; ?>
<br><br>
<br>
</label>
<label style="display: none;">
@@ -265,15 +437,6 @@ require_once "_incl/pre-body.php";
<div>
<?php if (!empty($menu)): ?>
<style>
.sc-details { text-align: center; margin: 5px; padding: 5px; border: 2px solid black; border-radius: 5px; background: white; cursor: pointer; width: calc(100% - 25px); display: inline-block; }
.sc-details summary { padding: 10px; background-size: contain; background-position: left; background-repeat: no-repeat; text-align: left; padding-left: 55px; font-size: 1.1em; }
.sc-cat-btn { border-radius: 20px; font-size: 1.1em; margin: 6px; padding: 10px 18px; border: 2px solid #bbb; background: #f8f9fa; display: inline-block; min-width: 90px; min-height: 60px; vertical-align: top; transition: background 0.2s, border 0.2s; }
.sc-cat-btn.active { background: #ffe066; border: 2px solid #222; }
.sc-cat-btn img { height: 50px; padding: 5px; background: white; border-radius: 8px; }
.sc-details .sc-summary-right { float: right; display: flex; align-items: center; gap: 6px; }
.sc-details .sc-check { height: 30px; }
</style>
<?php
// Iconos por categoría (puedes ampliar este array según tus iconos)
$sc_actions_icons = [
@@ -296,8 +459,13 @@ require_once "_incl/pre-body.php";
?>
<?php foreach ($menu as $category => $items): ?>
<details class="sc-details">
<summary style="background-image: url('<?= isset($sc_actions_icons[$category]) ? $sc_actions_icons[$category] : '' ?>');">
<?= htmlspecialchars($category) ?>
<summary>
<span class="sc-summary-left">
<?php if (isset($sc_actions_icons[$category])): ?>
<img src="<?= htmlspecialchars($sc_actions_icons[$category]) ?>" alt="">
<?php endif; ?>
<span><?= htmlspecialchars($category) ?></span>
</span>
<span class="sc-summary-right">
<span class="sc-selected-val" id="sc-val-<?= md5($category) ?>">
<?= htmlspecialchars($selected[$category] ?? '') ?>
@@ -305,12 +473,13 @@ require_once "_incl/pre-body.php";
<img class="sc-check" src="static/ico/checkbox_unchecked.png" id="sc-check-<?= md5($category) ?>">
</span>
</summary>
<div>
<div class="sc-category-body">
<?php foreach ($items as $item_name => $item_price):
$btn_id = 'sc-btn-' . md5($category . '_' . $item_name);
$is_active = ($selected[$category] ?? '') === $item_name;
$btn_extra_class = ($category === 'Tamaño') ? ' sc-size-btn' : '';
?>
<button type="button" class="sc-cat-btn<?= $is_active ? ' active' : '' ?>" id="<?= $btn_id ?>" onclick="
<button type="button" class="sc-cat-btn<?= $btn_extra_class ?><?= $is_active ? ' active' : '' ?>" id="<?= $btn_id ?>" onclick="
document.getElementById('sc-val-<?= md5($category) ?>').innerText = '<?= htmlspecialchars($item_name) ?>';
document.getElementById('sc-check-<?= md5($category) ?>').src = 'static/ico/checkbox.png';
var btns = this.parentNode.querySelectorAll('.sc-cat-btn');
@@ -322,7 +491,6 @@ require_once "_incl/pre-body.php";
<?php if ($item_price > 0): ?>
<br><small style="color: #6c757d;">(<?= number_format((float)$item_price, 2) ?>c)</small>
<?php endif; ?>
<!-- Aquí podrías poner una imagen si tienes -->
</button>
<?php endforeach; ?>
<input type="hidden" name="<?= htmlspecialchars($category) ?>" id="input-<?= md5($category) ?>" value="<?= htmlspecialchars($selected[$category] ?? '') ?>">
@@ -337,15 +505,15 @@ require_once "_incl/pre-body.php";
placeholder="Ej. 1x Café, 1x Bocadillo">
<small class="text-muted">
No hay menú configurado en
<code>/DATA/entreaulas/Centros/<?= htmlspecialchars($centro_id) ?>/SuperCafe/Menu.json</code>.
<code><?= htmlspecialchars(aulatek_orgs_base_path() . "/" . $centro_id . "/SuperCafe/Menu.json") ?></code>.
</small>
</div>
<?php endif; ?>
</div>
<label>
<label style="display:block; margin-top:6px;">
Notas<br>
<textarea name="Notas" class="form-control" rows="2"><?= htmlspecialchars($order_data['Notas']) ?></textarea><br><br>
<textarea name="Notas" class="form-control sc-notes" rows="2"><?= htmlspecialchars($order_data['Notas']) ?></textarea>
</label>
<?php if (!$is_new): ?>
@@ -359,14 +527,53 @@ require_once "_incl/pre-body.php";
</option>
<?php endforeach; ?>
</select>
<br>Modificar en el listado de comandas<br>
<br><small>Modificar en el listado de comandas</small><br>
</label>
<?php endif; ?>
<button type="submit" class="btn btn-success">Guardar</button>
<a href="/entreaulas/supercafe.php" class="btn btn-secondary">Cancelar</a>
<div class="sc-actions">
<button type="submit" class="btn btn-success">Guardar</button>
<a href="/aulatek/supercafe.php" class="btn btn-danger">Cancelar</a>
</div>
</fieldset>
</form>
</div>
<script>
function scSelectPersona(button) {
var key = button.getAttribute('data-person-key') || '';
var name = button.getAttribute('data-person-name') || '';
var region = button.getAttribute('data-person-region') || '';
var photo = button.getAttribute('data-person-photo') || '/static/arasaac/alumnos.png';
var hiddenInput = document.getElementById('sc-persona-input');
if (hiddenInput) {
hiddenInput.value = key;
}
var allButtons = document.querySelectorAll('.sc-person-btn');
allButtons.forEach(function(btn) {
btn.classList.remove('active');
});
button.classList.add('active');
var label = document.getElementById('sc-persona-selected-label');
if (label) {
label.innerText = name;
}
var check = document.getElementById('sc-persona-check');
if (check) {
check.src = 'static/ico/checkbox.png';
}
var current = document.getElementById('sc-persona-current');
if (current) {
current.innerHTML = '<img src="' + photo + '" alt="Foto">' +
'<span><strong>' + name + '</strong>' + (region ? ' (' + region + ')' : '') + '</span>';
}
}
</script>
<?php require_once "_incl/post-body.php"; ?>

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,11 +1,4 @@
<?php
ini_set("display_errors", 0);
$PAGE_TITLE = "EntreAulas - Inicio";
require_once "_incl/pre-body.php"; ?>
<div class="card pad">
<h2>Recursos:</h2>
<ul>
<li><a class="btn btn-secondary" href="/entreaulas/recursos/letras-a4.php">Generador de Letras A4 con varios estilos para imprimir</a></li>
</ul>
</div>
<?php require_once "_incl/post-body.php"; ?>
header('Location: /aulatek/');
exit;

View File

@@ -1,15 +1,133 @@
<?php require_once "_incl/pre-body.php"; ?>
<style>
.hero {
text-align: center;
margin: 0 0 28px;
background: linear-gradient(135deg, #e8f0fe 0%, #fce8ff 100%);
padding: 48px 24px;
border-radius: 12px;
color: var(--gw-text-primary, #202124);
border: 1px solid #dadce0;
}
.hero h1 {
font-size: 36px;
font-weight: 400;
margin-bottom: 8px;
color: #202124;
letter-spacing: -0.01em;
}
.hero p {
color: #5f6368;
font-size: 16px;
margin-bottom: 0;
}
.hero hr {
border-color: #dadce0;
margin: 20px auto;
max-width: 200px;
}
.hero h3 {
font-size: 14px;
font-weight: 500;
color: #1a73e8;
letter-spacing: 0.02em;
margin-bottom: 4px;
}
.hero .btn {
border-radius: 20px;
padding: 8px 24px;
}
.section-title {
font-size: 14px;
font-weight: 500;
color: #5f6368;
text-transform: uppercase;
letter-spacing: 0.08em;
margin: 0 0 16px;
}
.app-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 32px;
}
.app-card {
background: #ffffff;
border-radius: 8px;
padding: 20px 16px 16px;
display: flex;
flex-direction: column;
gap: 8px;
border: 1px solid #dadce0;
transition: box-shadow 0.2s ease, border-color 0.2s ease;
}
.app-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: #bdc1c6;
}
.app-card img {
height: 48px;
width: 48px;
object-fit: contain;
}
.app-title {
font-weight: 500;
font-size: 15px;
color: #202124;
margin-top: 4px;
}
.app-desc,
.app-note {
color: #5f6368;
font-size: 13px;
}
.app-actions {
margin-top: auto;
display: flex;
flex-direction: column;
gap: 6px;
padding-top: 8px;
}
.app-actions .btn {
border-radius: 4px;
font-size: 13px;
}
.is-disabled {
opacity: 0.55;
}
.app-card .btn.btn-outline-secondary.disabled {
color: #5f6368;
}
</style>
<section class="hero">
<h1>Bienvenidx a Axia4</h1>
<p>La plataforma unificada de EuskadiTech y Sketaria.</p>
<p>La plataforma unificada creada por EuskadiTech.</p>
<hr>
<h3>Versión 2.0.0</h3>
<p>Con esta versión, cambiamos la interfaz a una mas sencilla.</p>
<h3>Versión 2.1.0</h3>
<p>Con esta versión, hacemos muchos cambios.</p>
<br>
<a class="btn btn-primary" href="/account/">Accede a tu cuenta</a>
</section>
<p class="section-title">Aplicaciones</p>
<div id="grid" class="app-grid">
<div class="app-card">
@@ -19,14 +137,6 @@
<a href="/club/" class="btn btn-primary">Acceso público</a>
</div>
</div>
<div class="app-card">
<img src="/static/logo-telesec.png" alt="Logo TeleSec">
<div class="app-title">TeleSec</div>
<div class="app-desc">Gestión de aularios conectados.</div>
<div class="app-actions">
<a href="https://telesec.tech.eus/" target="_blank" class="btn btn-primary">Tengo cuenta</a>
</div>
</div>
<div class="app-card">
<img src="/static/logo-account.png" alt="Logo Account">
<div class="app-title">Mi Cuenta</div>
@@ -42,72 +152,13 @@
</div>
</div>
<div class="app-card">
<img src="/static/logo-entreaulas.png" alt="Logo EntreAulas">
<div class="app-title">EntreAulas</div>
<div class="app-desc">Recursos educativos digitales.</div>
<img src="/static/logo-aulatek.png" alt="Logo AulaTek">
<div class="app-title">AulaTek</div>
<div class="app-desc">Tu aula, digital.</div>
<div class="app-actions">
<a href="/entreaulas/" target="_blank" class="btn btn-primary">Acceso publico</a>
<a href="/aulatek/" target="_blank" class="btn btn-primary">Acceso público</a>
</div>
</div>
<!-- Arroz con leche: Wiki publica -->
<div class="app-card">
<img src="/static/logo-arroz.png" alt="Logo Arroz con leche">
<div class="app-title">Arroz con leche</div>
<div class="app-desc">Compartiendo nuestros conocimientos.</div>
<div class="app-actions">
<a href="https://arroz.tech.eus/" target="_blank" class="btn btn-primary">Acceso público</a>
</div>
</div>
</div>
<div class="app-grid" style="display: none;">
<div class="app-card is-disabled">
<img src="/static/logo-oscar.png" alt="Logo OSCAR">
<div class="app-title">OSCAR</div>
<div class="app-desc">Red de IA Absoluta.</div>
<div class="app-note">Próximamente</div>
</div>
<div class="app-card is-disabled">
<img src="/static/logo-media.png" alt="Logo ET Media">
<div class="app-title">ET Media</div>
<div class="app-desc">Streaming de pelis y series.</div>
<div class="app-note">Próximamente</div>
</div>
<div class="app-card is-disabled">
<img src="/static/logo-hyper.png" alt="Logo Hyper">
<div class="app-title">Hyper</div>
<div class="app-desc">Plataforma de gestión empresarial.</div>
<div class="app-note">Próximamente</div>
</div>
<div class="app-card is-disabled">
<img src="/static/logo-mail.png" alt="Logo Comunicaciones">
<div class="app-title">Comunicaciones</div>
<div class="app-desc">Correos electrónicos y mensajería.</div>
<div class="app-note">Próximamente</div>
</div>
<div class="app-card is-disabled">
<img src="/static/logo-malla.png" alt="Logo Malla">
<div class="app-title">Malla Meshtastic</div>
<div class="app-desc">Red de comunicación por radio.</div>
<div class="app-note">Próximamente</div>
</div>
<div class="app-card is-disabled">
<img src="/static/logo-aularios.png" alt="Logo Aularios">
<div class="app-title">Aularios<sup>2</sup></div>
<div class="app-desc">Visita virtual a los aularios.</div>
<div class="app-note">Solo lectura · Migrando a Axia4</div>
</div>
<div class="app-card is-disabled">
<img src="/static/logo-nube.png" alt="Logo Axia4 Cloud">
<div class="app-title">Nube Axia4.NET</div>
<div class="app-desc">Almacenamiento central de datos.</div>
<div class="app-note">Cerrado por migración</div>
</div>
<div class="app-card is-disabled">
<img src="/static/logo-nk4.png" alt="Logo Nube Kasa">
<div class="app-title">Nube Kasa</div>
<div class="app-desc">Nube personal con domótica.</div>
<div class="app-note">Cerrado por mantenimiento</div>
</div>
<div class="app-card">
<img src="/static/logo-sysadmin.png" alt="Logo SysAdmin">
<div class="app-title">SysAdmin</div>
@@ -121,95 +172,5 @@
</div>
</div>
</div>
</div>
<style>
body {
background: #f5f5f5;
}
.hero {
text-align: center;
margin: 32px 0 16px;
background: url(/static/portugalete.jpg) #ffffffc2;
padding: 25px 7px;
padding-top: 50px;
min-height: 350px;
border-radius: 50px;
background-size: cover;
background-position: center;
background-blend-mode: lighten;
color: black;
/* -webkit-text-stroke: 0.5px #acacac; */
}
.hero h1 {
font-size: 42px;
margin-bottom: 8px;
color: #000;
}
.hero p {
color: #000;
}
.notice-card {
background: #e8f0fe;
padding: 12px 16px;
border-radius: 12px;
display: flex;
flex-direction: column;
gap: 4px;
color: #1a3c78;
margin-bottom: 20px;
outline: 1px solid #c2d1f0;
}
.app-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 16px;
}
.app-card {
background: #fff;
border-radius: 16px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
}
.app-card img {
height: 64px;
width: 64px;
}
.app-title {
font-weight: 600;
color: #202124;
}
.app-desc,
.app-note {
color: #5f6368;
font-size: 13px;
}
.app-actions {
margin-top: auto;
display: flex;
flex-direction: column;
gap: 6px;
}
.is-disabled {
opacity: 0.6;
}
.app-card .btn.btn-outline-secondary.disabled {
color: black;
}
</style>
<?php require_once "_incl/post-body.php"; ?>

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,7 @@
<div class="sidebar-section-label">Atajos</div>
<nav class="sidebar-nav">
<a class="sidebar-link" href="/sysadmin/users.php?action=add">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><title>account-plus</title><path d="M15,14C12.33,14 7,15.33 7,18V20H23V18C23,15.33 17.67,14 15,14M6,10V7H4V10H1V12H4V15H6V12H9V10M15,12A4,4 0 0,0 19,8A4,4 0 0,0 15,4A4,4 0 0,0 11,8A4,4 0 0,0 15,12Z" /></svg>
<span>Nuevo usuario</span>
</a>
</nav>

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,217 @@ $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
$aulario_dir = "/DATA/entreaulas/Centros/$centro_id/Aularios/$aulario_id";
function rrmdir($dir) {
// Remove from DB
db()->prepare("DELETE FROM aularios WHERE org_id = ? AND aulario_id = ?")
->execute([$centro_id, $aulario_id]);
// Remove comedor, diario, panel data
db()->prepare("DELETE FROM comedor_menu_types WHERE org_id = ? AND aulario_id = ?")
->execute([$centro_id, $aulario_id]);
db()->prepare("DELETE FROM comedor_entries WHERE org_id = ? AND aulario_id = ?")
->execute([$centro_id, $aulario_id]);
db()->prepare("DELETE FROM diario_entries WHERE org_id = ? AND aulario_id = ?")
->execute([$centro_id, $aulario_id]);
db()->prepare("DELETE FROM panel_alumno WHERE org_id = ? AND aulario_id = ?")
->execute([$centro_id, $aulario_id]);
// Remove filesystem directory with student photos
$aulario_dir = aulatek_orgs_base_path() . "/$centro_id/Aularios/$aulario_id";
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"] ?? ($_POST["org"] ?? "")));
$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 organizaciones WHERE org_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 (org_id, aulario_id, name, icon) VALUES (?, ?, ?, ?)"
)->execute([
$centro_id, $aulario_id,
Sf($_POST["name"] ?? ""),
Sf($_POST["icon"] ?? "/static/logo-entreaulas.png"),
]);
// Create Proyectos directory for project file storage
$proyectos_dir = aulatek_orgs_base_path() . "/$centro_id/Aularios/$aulario_id/Proyectos/";
if (!is_dir($proyectos_dir)) {
mkdir($proyectos_dir, 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 org_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"] ?? ($_GET["org"] ?? "")));
$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">
<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>';
}
?>
<label for="centro" class="form-label">Organizacion:</label>
<select id="centro" name="centro" class="form-select" required>
<option value="">-- Selecciona una organizacion --</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="text" 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"] ?? ($_GET["org"] ?? "")));
$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
<?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
<?php
require_once "_incl/post-body.php";
break;
}
require_once "_incl/post-body.php"; ?>

View File

@@ -1,293 +0,0 @@
<?php
require_once "_incl/auth_redir.php";
require_once "../_incl/tools.security.php";
function safe_path_segment($value)
{
$value = trim((string)$value);
$value = str_replace(["\0", "/", "\\"], "", $value);
$value = str_replace("..", "", $value);
$value = basename($value);
if ($value === "." || $value === "..") {
return "";
}
return $value;
}
$form_action = $_GET["form"] ?? "";
switch ($form_action) {
case "create":
$centro_id = safe_path_segment(Sf($_POST["name"] ?? ""));
if (empty($centro_id)) {
die("Nombre del centro no proporcionado.");
}
$centro_path = "/DATA/entreaulas/Centros/$centro_id";
if (is_dir($centro_path)) {
die("El centro ya existe.");
}
mkdir($centro_path, 0777, 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)) {
die("Centro no válido.");
}
$activity_name = safe_path_segment(Sf($_POST["name"] ?? ''));
if (empty($activity_name)) {
die("Nombre de la actividad no proporcionado.");
}
$activity_photo = $_FILES["photo"] ?? null;
if ($activity_photo === null || $activity_photo["error"] !== UPLOAD_ERR_OK) {
die("Error al subir la foto.");
}
$activity_path = "$centro_path/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);
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'] ?? ''));
$activity_name = safe_path_segment(Sf($_GET['activity'] ?? ''));
$activity_path = "/DATA/entreaulas/Centros/$centro_id/Panel/Actividades/$activity_name";
if (!is_dir($activity_path)) {
die("Actividad no válida.");
}
$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);
}
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)) {
die("Ya existe una actividad con ese nombre.");
}
rename($activity_path, $new_activity_path);
}
header("Location: ?action=edit&centro=" . urlencode($centro_id));;
exit();
break;
}
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'] ?? ''));
$activity_name = safe_path_segment(Sf($_GET['activity'] ?? ''));
$activity_path = "/DATA/entreaulas/Centros/$centro_id/Panel/Actividades/$activity_name";
if (!is_dir($activity_path)) {
die("Actividad no válida.");
}
?>
<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">
<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); ?>">
</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);
?>');">
<input title="Drop image or click me" type="file" name="file" accept="image/*">
</label>
</div>
</div>
<button type="submit" class="btn btn-primary">Guardar Cambios</button>
</form>
</div>
</div>
<?php
break;
case "new_activity":
$centro_id = safe_path_segment(Sf($_GET['centro'] ?? ''));
$centro_path = "/DATA/entreaulas/Centros/$centro_id";
if (!is_dir($centro_path)) {
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">
<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">
</div>
<div class="mb-3">
<label for="photo" class="form-label">Foto:</label>
<input required type="file" id="photo" name="photo" class="form-control" accept="image/*">
</div>
<button type="submit" class="btn btn-primary">Crear Actividad</button>
</form>
</div>
</div>
<?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>
<input required type="text" id="name" name="name" class="form-control" placeholder="Ej: Centro-Principal-001">
</div>
<button type="submit" class="btn btn-primary">Crear Centro</button>
</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)) {
die("Centro no válido.");
}
?>
<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>
</div>
</div>
<div class="card pad">
<div>
<h2>Aularios</h2>
<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>
</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>';
}
?>
</tbody>
</table>
</div>
</div>
<div class="card pad">
<div>
<h2>Actividades del panel</h2>
<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>
</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>';
}
?>
</tbody>
</table>
</div>
</div>
<?php
break;
case "index":
default:
?>
<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>
</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>';
}
?>
</tbody>
</table>
</div>
</div>
<?php
break;
}
require_once "_incl/post-body.php"; ?>

View File

@@ -7,9 +7,9 @@ require_once "_incl/pre-body.php"; ?>
</div>
<div id="grid">
<div class="card grid-item">
<img src="/static/logo-entreaulas.png" alt="Logo EntreAulas">
<b>EntreAulas</b>
<a href="/sysadmin/centros.php" class="btn btn-primary">Gestionar Centros</a>
<img src="/static/logo-entreaulas.png" alt="Logo AulaTek">
<b>AulaTek</b>
<a href="/sysadmin/orgs.php" class="btn btn-primary">Gestionar Organizaciones</a>
<a href="/sysadmin/aularios.php" class="btn btn-primary">Gestionar Aularios</a>
</div>
<div class="card grid-item">

View File

@@ -1,100 +1,91 @@
<?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;
}
require_once "_incl/post-body.php";
require_once "_incl/post-body.php";

View File

@@ -0,0 +1,291 @@
<?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 = str_replace(["\0", "/", "\\"], "", $value);
$value = str_replace("..", "", $value);
$value = basename($value);
if ($value === "." || $value === "..") {
return "";
}
return $value;
}
$form_action = $_GET["form"] ?? "";
switch ($form_action) {
case "create":
$org_id = safe_path_segment(Sf($_POST["org_id"] ?? ""));
$org_name = Sf($_POST["org_name"] ?? "");
if (empty($org_id)) {
die("Nombre de la organización no proporcionado.");
}
// Check uniqueness in DB
$existing = db()->prepare("SELECT id FROM organizaciones WHERE org_id = ?");
$existing->execute([$org_id]);
if ($existing->fetch()) {
die("La organización ya existe.");
}
// Create DB record
db()->prepare("INSERT INTO organizaciones (org_id, org_name) VALUES (?, ?)")->execute([$org_id, $org_name !== '' ? $org_name : $org_id]);
// Keep filesystem directory for activity photos (Panel/Actividades)
$org_path = aulatek_orgs_base_path() . "/$org_id";
if (!is_dir($org_path) && !mkdir($org_path, 0755, true) && !is_dir($org_path)) {
error_log("orgs.php: failed to create directory $org_path");
}
header("Location: ?action=index");
exit();
break;
case "edit":
$org_id = safe_path_segment(Sf($_GET['org'] ?? ''));
$org_name = Sf($_POST['org_name'] ?? '');
if ($org_id === '' || $org_name === '') {
die("Datos inválidos para actualizar la organización.");
}
db()->prepare("UPDATE organizaciones SET org_name = ? WHERE org_id = ?")->execute([$org_name, $org_id]);
header("Location: ?action=edit&org=" . urlencode($org_id) . "&_result=" . urlencode("Cambios guardados."));
exit();
break;
case "create_activity":
ini_set('memory_limit', '512M');
ini_set('upload_max_filesize', '256M');
ini_set('post_max_size', '256M');
$org_id = safe_path_segment(Sf($_GET['org'] ?? ''));
// Validate organization exists in DB
$stmt = db()->prepare("SELECT id FROM organizaciones WHERE org_id = ?");
$stmt->execute([$org_id]);
if (!$stmt->fetch()) {
die("Organización no válida.");
}
$activity_name = safe_path_segment(Sf($_POST["name"] ?? ''));
if (empty($activity_name)) {
die("Nombre de la actividad no proporcionado.");
}
$activity_photo = $_FILES["photo"] ?? null;
if ($activity_photo === null || $activity_photo["error"] !== UPLOAD_ERR_OK) {
die("Error al subir la foto.");
}
$activity_path = aulatek_orgs_base_path() . "/$org_id/Panel/Actividades/$activity_name";
if (is_dir($activity_path)) {
die("La actividad ya existe.");
}
mkdir($activity_path, 0755, true);
move_uploaded_file($activity_photo["tmp_name"], "$activity_path/photo.jpg");
header("Location: ?action=edit&org=" . urlencode($org_id));
exit();
break;
case "edit_activity":
ini_set('memory_limit', '512M');
ini_set('upload_max_filesize', '256M');
ini_set('post_max_size', '256M');
$org_id = safe_path_segment(Sf($_GET['org'] ?? ''));
$activity_name = safe_path_segment(Sf($_GET['activity'] ?? ''));
$activity_path = aulatek_orgs_base_path() . "/$org_id/Panel/Actividades/$activity_name";
if (!is_dir($activity_path)) {
die("Actividad no válida.");
}
$activity_photo = $_FILES["file"] ?? null;
if ($activity_photo !== null && $activity_photo["error"] === UPLOAD_ERR_OK) {
move_uploaded_file($activity_photo["tmp_name"], "$activity_path/photo.jpg");
}
$new_name = safe_path_segment(Sf($_POST['nombre'] ?? ''));
if ($new_name !== $activity_name && $new_name !== '') {
$new_path = aulatek_orgs_base_path() . "/$org_id/Panel/Actividades/$new_name";
if (is_dir($new_path)) {
die("Ya existe una actividad con ese nombre.");
}
rename($activity_path, $new_path);
}
header("Location: ?action=edit&org=" . urlencode($org_id));
exit();
break;
}
require_once "_incl/pre-body.php";
$view_action = $_GET["action"] ?? "index";
switch ($view_action) {
case "edit_activity":
$org_id = safe_path_segment(Sf($_GET['org'] ?? ''));
$activity_name = safe_path_segment(Sf($_GET['activity'] ?? ''));
$activity_path = aulatek_orgs_base_path() . "/$org_id/Panel/Actividades/$activity_name";
if (!is_dir($activity_path)) {
die("Actividad no válida.");
}
?>
<div class="card pad">
<div>
<h1 class="card-title">Gestión de la Actividad: <?= htmlspecialchars($activity_name) ?></h1>
<form method="post" action="?form=edit_activity&org=<?= urlencode($org_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="<?= 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
$img = file_exists("$activity_path/photo.jpg")
? "/aulatek/_filefetch.php?type=panel_actividades&org=" . urlencode($org_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>
</div>
</div>
<button type="submit" class="btn btn-primary">Guardar Cambios</button>
</form>
</div>
</div>
<?php
break;
case "new_activity":
$org_id = safe_path_segment(Sf($_GET['org'] ?? ''));
$stmt = db()->prepare("SELECT id FROM organizaciones WHERE org_id = ?");
$stmt->execute([$org_id]);
if (!$stmt->fetch()) {
die("Organización no válida.");
}
?>
<div class="card pad">
<div>
<h1 class="card-title">Nueva Actividad del Panel</h1>
<form method="post" action="?form=create_activity&org=<?= urlencode($org_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">
</div>
<div class="mb-3">
<label for="photo" class="form-label">Foto:</label>
<input required type="file" id="photo" name="photo" class="form-control" accept="image/*">
</div>
<button type="submit" class="btn btn-primary">Crear Actividad</button>
</form>
</div>
</div>
<?php
break;
case "new":
?>
<div class="card pad">
<div>
<h1 class="card-title">Nueva Organización</h1>
<form method="post" action="?form=create">
<div class="mb-3">
<label for="org_id" class="form-label">ID de la organización:</label>
<input required type="text" id="org_id" name="org_id" class="form-control" placeholder="Ej: Organizacion-Principal-001">
</div>
<div class="mb-3">
<label for="org_name" class="form-label">Nombre de la organización:</label>
<input required type="text" id="org_name" name="org_name" class="form-control" placeholder="Ej: Organización Principal">
</div>
<button type="submit" class="btn btn-primary">Crear Organización</button>
</form>
</div>
</div>
<?php
break;
case "edit":
$org_id = safe_path_segment(Sf($_GET['org'] ?? ''));
$stmt = db()->prepare("SELECT org_name FROM organizaciones WHERE org_id = ?");
$stmt->execute([$org_id]);
$org_row = $stmt->fetch();
if (!$org_row) {
die("Organización no válida.");
}
$org_name = $org_row['org_name'] ?? $org_id;
$aularios = db_get_aularios($org_id);
$activities = glob(aulatek_orgs_base_path() . "/$org_id/Panel/Actividades/*", GLOB_ONLYDIR) ?: [];
?>
<div class="card pad">
<div>
<h1 class="card-title">Gestión de la Organización: <?= htmlspecialchars($org_name) ?></h1>
</div>
<form method="post" action="?form=edit&org=<?= urlencode($org_id) ?>">
<div class="mb-3">
<label for="org_name" class="form-label">Nombre de la organización:</label>
<input required type="text" id="org_name" name="org_name" class="form-control" value="<?= htmlspecialchars($org_name) ?>">
</div>
<button type="submit" class="btn btn-primary">Guardar Cambios</button>
</form>
</div>
<div class="card pad">
<div>
<h2>Aularios</h2>
<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&org=<?= urlencode($org_id) ?>" class="btn btn-success">+ Nuevo</a></th>
</tr>
</thead>
<tbody>
<?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) ?>&org=<?= urlencode($org_id) ?>" class="btn btn-primary">Gestionar</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="card pad">
<div>
<h2>Actividades del panel</h2>
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Foto</th><th>Nombre</th>
<th><a href="?action=new_activity&org=<?= urlencode($org_id) ?>" class="btn btn-success">+ Nuevo</a></th>
</tr>
</thead>
<tbody>
<?php foreach ($activities as $ap): ?>
<?php $an = basename($ap); $img_path = "$ap/photo.jpg"; ?>
<tr>
<td><img src="<?= file_exists($img_path) ? htmlspecialchars("/aulatek/_filefetch.php?type=panel_actividades&org=" . urlencode($org_id) . "&activity=" . urlencode($an)) : '/static/logo-entreaulas.png' ?>" style="height: 50px;"></td>
<td><?= htmlspecialchars($an) ?></td>
<td><a href="?action=edit_activity&org=<?= urlencode($org_id) ?>&activity=<?= urlencode($an) ?>" class="btn btn-primary">Gestionar</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php
break;
case "index":
default:
$all_organizaciones = db_get_organizaciones();
?>
<div class="card pad">
<div>
<h1 class="card-title">Gestión de Organizaciones</h1>
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Organización</th>
<th style="text-align: right;"><a href="?action=new" class="btn btn-success">+ Nuevo</a></th>
</tr>
</thead>
<tbody>
<?php foreach ($all_organizaciones as $o): ?>
<tr>
<td><?= htmlspecialchars($o['org_name']) ?><br><small><?= htmlspecialchars($o['org_id']) ?></small></td>
<td><a href="?action=edit&org=<?= urlencode($o['org_id']) ?>" class="btn btn-primary">Gestionar</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php
break;
}
require_once "_incl/post-body.php";

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,91 +1,108 @@
<?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;
function render_users_mobile_styles()
{
?>
<style>
.users-mobile-stack .btn {
width: 100%;
}
.tenant-list {
max-height: 210px;
overflow: auto;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
padding: 0.5rem 0.75rem;
}
.tenant-list .form-check {
margin-bottom: 0.45rem;
}
.aulas-list {
max-height: 220px;
overflow: auto;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
padding: 0.5rem 0.75rem;
}
.aulas-list .form-check {
margin-right: 0.6rem;
margin-bottom: 0.5rem;
}
@media (max-width: 767.98px) {
.card.pad {
padding: 1rem !important;
}
.users-mobile-stack h1 {
font-size: 1.4rem;
}
.users-mobile-stack .accordion-button {
padding-top: 0.7rem;
padding-bottom: 0.7rem;
}
.users-mobile-stack .btn {
width: 100%;
}
}
}
$permissions = $_POST['permissions'] ?? [];
if (!is_array($permissions)) {
$permissions = [];
}
</style>
<?php
}
$aulas = $_POST['aulas'] ?? [];
if (!is_array($aulas)) {
$aulas = [];
}
$aulas = array_values(array_filter(array_map('safe_aulario_id', $aulas)));
switch ($_GET['form'] ?? '') {
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 = [];
}
$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;
$organization_input = $_POST['organization'] ?? [];
if (!is_array($organization_input)) {
$organization_input = [$organization_input];
}
$organizations = array_values(array_unique(array_filter(array_map('safe_organization_id', $organization_input))));
db_upsert_user([
'username' => $username,
'display_name' => $_POST['display_name'] ?? '',
'email' => $_POST['email'] ?? '',
'permissions' => $permissions,
'orgs' => $organizations,
'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";
render_users_mobile_styles();
$all_organizations = db_get_organizations();
?>
<form method="post" action="?form=save_edit">
<form method="post" action="?form=save_edit" class="users-mobile-stack">
<div class="card pad">
<div>
<h1 class="card-title">Agregar Nuevo Usuario</h1>
@@ -104,70 +121,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="#collapseAulatek">AulaTek</button>
</h2>
<div id="collapseEntreaulas" class="accordion-collapse collapse" aria-labelledby="headingEntreaulas" data-bs-parent="#permissionsAccordion">
<div id="collapseAulatek" 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>
<input class="form-check-input" type="checkbox" name="permissions[]" value="aulatek:access" id="aulatek-access">
<label class="form-check-label" for="aulatek-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>
<input class="form-check-input" type="checkbox" name="permissions[]" value="aulatek:docente" id="aulatek-docente">
<label class="form-check-label" for="aulatek-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>
<input class="form-check-input" type="checkbox" name="permissions[]" value="aulatek:proyectos:delete" id="aulatek-proyectos-delete">
<label class="form-check-label" for="aulatek-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>
@@ -178,22 +177,21 @@ switch ($_GET['action'] ?? '') {
</div>
<div class="card pad">
<div>
<h2>EntreAulas: Configuración</h2>
<h2>AulaTek: Configuración</h2>
<div class="mb-3">
<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>';
}
?>
</select>
<label class="form-label">Tenant asociado:</label>
<div class="tenant-list">
<?php foreach ($all_organizations as $orgRow): $cid = $orgRow['org_id']; ?>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="organization[]" value="<?= htmlspecialchars($cid) ?>" id="tenant-<?= htmlspecialchars($cid) ?>">
<label class="form-check-label" for="tenant-<?= htmlspecialchars($cid) ?>"><?= htmlspecialchars($cid) ?></label>
</div>
<?php endforeach; ?>
</div>
<small class="text-muted">Marca uno o varios tenants.</small>
</div>
<div class="mb-3">
<label for="role" class="form-label">Rol en EntreAulas:</label>
<label for="role" class="form-label">Rol en AulaTek:</label>
<select id="role" name="role" class="form-select">
<option value="">-- Selecciona un rol --</option>
<option value="teacher">Profesor</option>
@@ -205,219 +203,225 @@ 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";
render_users_mobile_styles();
$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_organizations = db_get_organizations();
$user_organizations = $userdata['orgs'] ?? [];
if (!is_array($user_organizations)) {
$user_organizations = [];
}
$user_organizations = array_values(array_unique(array_filter(array_map('safe_organization_id', $user_organizations))));
if (empty($user_organizations)) {
$legacy_organization = safe_organization_id($userdata['orgs'] ?? '');
if ($legacy_organization !== '') {
$user_organizations = [$legacy_organization];
}
}
$aularios_by_organization = [];
foreach ($user_organizations as $org_id) {
$aularios_by_organization[$org_id] = db_get_aularios($org_id);
}
$assigned_aulas = $userdata['aulatek']['aulas'] ?? ($userdata['entreaulas']['aulas'] ?? []);
?>
<form method="post" action="?form=save_edit">
<form method="post" action="?form=save_edit" class="users-mobile-stack">
<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>
<div class="mb-3">
<label class="form-label">Organizaciones asociadas:</label>
<div class="organization-list">
<?php foreach ($all_organizations as $orgRow): $org_id = $orgRow['org_id']; $org_name = $orgRow['org_name']; ?>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="organization[]" value="<?= htmlspecialchars($org_id) ?>" id="organization-<?= htmlspecialchars($org_id) ?>" <?= in_array($org_id, $user_organizations, true) ? 'checked' : '' ?>>
<label class="form-check-label" for="organization-<?= htmlspecialchars($org_id) ?>"><?= htmlspecialchars($org_name) ?></label>
</div>
<?php endforeach; ?>
</div>
<small class="text-muted">Marca una o varias organizaciones.</small>
</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="#collapseAulatek">AulaTek</button>
</h2>
<div id="collapseEntreaulas" class="accordion-collapse collapse" aria-labelledby="headingEntreaulas" data-bs-parent="#permissionsAccordion">
<div id="collapseAulatek" 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="aulatek:access" id="aulatek-access" <?= in_array('aulatek:access', $userdata['permissions'] ?? []) ? 'checked' : '' ?>>
<label class="form-check-label" for="aulatek-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="aulatek:docente" id="aulatek-docente" <?= in_array('aulatek:docente', $userdata['permissions'] ?? []) ? 'checked' : '' ?>>
<label class="form-check-label" for="aulatek-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="aulatek:proyectos:delete" id="aulatek-proyectos-delete" <?= in_array('aulatek:proyectos:delete', $userdata['permissions'] ?? []) ? 'checked' : '' ?>>
<label class="form-check-label" for="aulatek-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>
<div class="card pad">
<div>
<h2>EntreAulas: Configuración</h2>
<h2>AulaTek: Configuración</h2>
<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>';
}
?>
</select>
</div>
<div class="mb-3">
<label for="role" class="form-label">Rol en EntreAulas:</label>
<label for="role" class="form-label">Rol en AulaTek:</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['aulatek']['role'] ?? '') ? 'selected' : '' ?>>-- Selecciona un rol --</option>
<option value="teacher" <?= ($userdata['aulatek']['role'] ?? '') === 'teacher' ? 'selected' : '' ?>>Profesor</option>
<option value="student" <?= ($userdata['aulatek']['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>';
}
?>
<div class="aulas-list">
<?php if (empty($aularios_by_organization)): ?>
<small class="text-muted">No hay organizaciones asociadas para mostrar aulas.</small>
<?php endif; ?>
<?php foreach ($aularios_by_organization as $org_id => $org_aularios): ?>
<div style="margin-bottom: 0.75rem; padding-bottom: 0.5rem; border-bottom: 1px solid #e9ecef;">
<div style="font-weight: 600; margin-bottom: 0.4rem;"><?= htmlspecialchars($org_id) ?></div>
<?php if (empty($org_aularios)): ?>
<small class="text-muted">Sin aulas en esta organización.</small>
<?php else: ?>
<?php foreach ($org_aularios as $aula_id => $aula_data): ?>
<?php $checkbox_id = 'aula-' . md5($org_id . '-' . $aula_id); ?>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="aulas[]"
value="<?= htmlspecialchars($aula_id) ?>"
id="<?= htmlspecialchars($checkbox_id) ?>"
<?= in_array($aula_id, $assigned_aulas, true) ? 'checked' : '' ?>>
<label class="form-check-label" for="<?= htmlspecialchars($checkbox_id) ?>">
<?= htmlspecialchars($aula_data['name'] ?? $aula_id) ?>
</label>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</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";
render_users_mobile_styles();
$all_users = db_get_all_users();
?>
<div class="card pad users-mobile-stack">
<div>
<div class="d-flex flex-column flex-md-row align-items-md-center justify-content-md-between gap-2 mb-2">
<h1 class="mb-0">Gestión de Usuarios</h1>
<a href="?action=add" class="btn btn-success">+ Nuevo</a>
</div>
<p>Desde esta sección puedes gestionar los usuarios del sistema.</p>
<div class="d-none d-md-block table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Usuario</th>
<th>Nombre</th>
<th>Correo</th>
<th></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 btn-sm">Editar</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="d-md-none">
<?php foreach ($all_users as $u): ?>
<div class="border rounded p-3 mb-2 bg-white">
<div><strong><?= htmlspecialchars($u['display_name'] ?: 'N/A') ?></strong></div>
<div class="text-muted small"><?= htmlspecialchars($u['username']) ?></div>
<div class="small"><?= htmlspecialchars($u['email'] ?: 'N/A') ?></div>
<a href="?action=edit&user=<?= urlencode($u['username']) ?>" class="btn btn-primary btn-sm mt-2">Editar</a>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<?php
require_once "_incl/post-body.php";
break;
}
?>