up
This commit is contained in:
+176
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
/**
|
||||
* core/Layout.php
|
||||
* Renderiza el HTML envolvente de cada vista del ERP.
|
||||
*
|
||||
* USO en una vista de módulo (modules/{m}/views/{v}.php):
|
||||
*
|
||||
* Layout::open('Dashboard Turnero', 'fas fa-ticket-alt');
|
||||
* // ... contenido de la vista ...
|
||||
* Layout::close();
|
||||
*
|
||||
* Layout::open() emite DOCTYPE, <head>, Bootstrap, sidebar y abre <main>.
|
||||
* Layout::close() cierra </main>, agrega scripts y </body></html>.
|
||||
*/
|
||||
|
||||
class Layout
|
||||
{
|
||||
// ─── Apertura ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Emite el HTML inicial de la página (head + sidebar + apertura de main).
|
||||
*
|
||||
* @param string $title Título de la pestaña y del encabezado.
|
||||
* @param string $icon Clase FontAwesome del ícono (ej. 'fas fa-ticket-alt').
|
||||
*/
|
||||
public static function open(string $title = 'Panel', string $icon = 'fas fa-th-large'): void
|
||||
{
|
||||
$appName = defined('APP_NAME') ? APP_NAME : 'ERP System';
|
||||
$safeTitle = htmlspecialchars($title, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
$safeIcon = htmlspecialchars($icon, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
|
||||
// Exponer variables para sidebar.php
|
||||
$GLOBALS['_LAYOUT_TITLE'] = $title;
|
||||
$GLOBALS['_LAYOUT_ICON'] = $icon;
|
||||
|
||||
// Detectar base URL (para assets relativos)
|
||||
$base = defined('APP_URL') ? rtrim(APP_URL, '/') : '';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= $safeTitle ?> — <?= htmlspecialchars($appName, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></title>
|
||||
|
||||
<!-- Bootstrap 5 -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome 6 -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<!-- Estilos del sistema -->
|
||||
<link href="<?= $base ?>/assets/css/styles.css?v=12" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* Variables del tema ERP */
|
||||
:root {
|
||||
--sidebar-width: 260px;
|
||||
--header-height: 56px;
|
||||
}
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.main-content {
|
||||
margin-left: var(--sidebar-width);
|
||||
min-height: 100vh;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.main-content { margin-left: 0; }
|
||||
}
|
||||
/* Toast container */
|
||||
#toast-container {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 1090;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Toast container global -->
|
||||
<div id="toast-container" aria-live="polite" aria-atomic="true"></div>
|
||||
|
||||
<?php
|
||||
// Sidebar dinámico
|
||||
$sidebarFile = APP_ROOT . '/shared/components/sidebar.php';
|
||||
if (file_exists($sidebarFile)) {
|
||||
$SIDEBAR_TITLE = $title;
|
||||
$SIDEBAR_ICON = $icon;
|
||||
include $sidebarFile;
|
||||
}
|
||||
?>
|
||||
|
||||
<!-- Contenido principal -->
|
||||
<main class="main-content">
|
||||
<!-- Barra de título de módulo -->
|
||||
<div class="bg-white border-bottom px-4 py-3 d-flex align-items-center justify-content-between">
|
||||
<h5 class="mb-0 fw-semibold">
|
||||
<i class="<?= $safeIcon ?> me-2 text-primary"></i><?= $safeTitle ?>
|
||||
</h5>
|
||||
<small class="text-muted d-none d-sm-block">
|
||||
<?= htmlspecialchars(Auth::fullName(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
|
||||
<span class="badge bg-secondary ms-1"><?= htmlspecialchars(Auth::role(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></span>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<?php
|
||||
}
|
||||
|
||||
// ─── Cierre ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Cierra la sección de contenido y emite los scripts finales.
|
||||
*/
|
||||
public static function close(): void
|
||||
{
|
||||
$base = defined('APP_URL') ? rtrim(APP_URL, '/') : '';
|
||||
?>
|
||||
</div><!-- /.p-4 -->
|
||||
</main><!-- /.main-content -->
|
||||
|
||||
<!-- Bootstrap 5 JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- Helper JS global: toasts y fetch wrapper -->
|
||||
<script>
|
||||
/**
|
||||
* erp.toast(message, type)
|
||||
* Muestra un Bootstrap Toast en la esquina superior derecha.
|
||||
* type: 'success' | 'danger' | 'warning' | 'info'
|
||||
*/
|
||||
window.erp = window.erp || {};
|
||||
|
||||
erp.toast = function(message, type = 'success') {
|
||||
const id = 'toast-' + Date.now();
|
||||
const icons = { success: 'fa-check-circle', danger: 'fa-times-circle', warning: 'fa-exclamation-triangle', info: 'fa-info-circle' };
|
||||
const icon = icons[type] || icons.info;
|
||||
const html = `
|
||||
<div id="${id}" class="toast align-items-center text-bg-${type} border-0 mb-2" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="4000">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body"><i class="fas ${icon} me-2"></i>${message}</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Cerrar"></button>
|
||||
</div>
|
||||
</div>`;
|
||||
document.getElementById('toast-container').insertAdjacentHTML('beforeend', html);
|
||||
const el = document.getElementById(id);
|
||||
const toast = new bootstrap.Toast(el);
|
||||
toast.show();
|
||||
el.addEventListener('hidden.bs.toast', () => el.remove());
|
||||
};
|
||||
|
||||
/**
|
||||
* erp.fetch(url, options)
|
||||
* Wrapper de fetch que muestra toast en caso de error y devuelve el JSON parseado.
|
||||
* Lanza un Error si ok === false.
|
||||
*/
|
||||
erp.fetch = async function(url, options = {}) {
|
||||
try {
|
||||
const res = await fetch(url, options);
|
||||
const data = await res.json();
|
||||
if (!data.ok) {
|
||||
throw new Error(data.error || 'Error desconocido');
|
||||
}
|
||||
return data;
|
||||
} catch (err) {
|
||||
erp.toast(err.message, 'danger');
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user