panel admin: asistente, backups y ayuda

This commit is contained in:
Brisa
2026-06-30 15:01:01 -03:00
parent 504746fede
commit f9c5623d43
4 changed files with 1151 additions and 0 deletions
+219
View File
@@ -0,0 +1,219 @@
<?php
// 1. CONEXIÓN A LA BASE DE DATOS
$host = "localhost";
$user = "root";
$pass = "";
$db = "lapecosa";
$conn = new mysqli($host, $user, $pass, $db);
if ($conn->connect_error) {
die("Error de conexión: " . $conn->connect_error);
}
// 2. LÓGICA PARA GUARDAR CAMBIOS (PROCESAMIENTO)
$mensaje_alerta = "";
// Agregar nueva pregunta CORREGIDO (Incluye keywords y categoría dinámica)
if (isset($_POST['agregar_pregunta'])) {
$preg = $conn->real_escape_string($_POST['nueva_pregunta']);
$resp = $conn->real_escape_string($_POST['nueva_respuesta']);
$keys = $conn->real_escape_string($_POST['nuevas_keywords']);
// Procesamiento inteligente de la categoría
$cat_seleccionada = $_POST['categoria_existente'];
if ($cat_seleccionada === 'nueva') {
$cat = $conn->real_escape_string(trim(strtolower($_POST['nueva_categoria'])));
} else {
$cat = $conn->real_escape_string($cat_seleccionada);
}
if (!empty($preg) && !empty($resp) && !empty($keys) && !empty($cat)) {
// Ejecución del INSERT incluyendo la nueva columna
$conn->query("INSERT INTO bot_preguntas (pregunta, respuesta, keywords, categoria) VALUES ('$preg', '$resp', '$keys', '$cat')");
$mensaje_alerta = "Pregunta, palabras clave y categoría agregadas con éxito.";
} else {
$mensaje_alerta = "Error: Todos los campos del asistente virtual, incluida la categoría, son obligatorios.";
}
}
// Eliminar pregunta
if (isset($_GET['eliminar'])) {
$id_del = intval($_GET['eliminar']);
$conn->query("DELETE FROM bot_preguntas WHERE id = $id_del");
header("Location: asistente.php"); // Recargar para limpiar la URL
exit();
}
// Consultas dinámicas para la vista y el selector
$preguntas = $conn->query("SELECT * FROM bot_preguntas ORDER BY id DESC");
$categorias_existentes = $conn->query("SELECT DISTINCT categoria FROM bot_preguntas WHERE categoria IS NOT NULL AND categoria != '' ORDER BY categoria ASC");
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Panel Asistente Virtual | La Pecosa</title>
<link rel="stylesheet" href="../css/all.min.css">
<style>
:root { --rojo: #D32F2F; --oscuro: #121212; --gris: #1e1e1e; --texto: #f5f5f5; }
body { font-family: 'Segoe UI', sans-serif; background-color: var(--oscuro); color: var(--texto); margin: 0; padding: 20px; }
.container { max-width: 1200px; margin: auto; }
header { border-bottom: 2px solid var(--rojo); padding-bottom: 10px; margin-bottom: 30px; display: flex; align-items: center; }
header h1 { color: var(--rojo); margin: 0; text-transform: uppercase; font-size: 24px; }
header i { margin-right: 15px; font-size: 30px; color: var(--rojo); }
.grid { display: grid; grid-template-columns: 1fr 1.8fr; gap: 20px; }
.card { background: var(--gris); padding: 20px; border-radius: 12px; border: 1px solid #333; }
h2 { font-size: 18px; border-left: 4px solid var(--rojo); padding-left: 10px; margin-bottom: 20px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-size: 14px; color: #bbb; }
input, textarea, select { width: 100%; padding: 10px; background: #2a2a2a; border: 1px solid #444; color: white; border-radius: 6px; box-sizing: border-box; }
select option { background: #1e1e1e; color: white; }
.btn { background: var(--rojo); color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: bold; width: 100%; }
.btn:hover { background: #b71c1c; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th { text-align: left; background: #252525; padding: 12px; border-bottom: 2px solid var(--rojo); }
td { padding: 12px; border-bottom: 1px solid #333; font-size: 14px; }
.btn-del { color: #ff5252; text-decoration: none; font-size: 18px; }
.alerta { background: #2e7d32; color: white; padding: 15px; border-radius: 6px; margin-bottom: 20px; text-align: center; }
.btn-volver {
background: #252525;
color: var(--rojo);
padding: 10px 15px;
border-radius: 8px;
text-decoration: none;
margin-right: 20px;
border: 1px solid #444;
transition: 0.3s;
display: flex;
align-items: center;
justify-content: center;
}
.btn-volver:hover { background: var(--rojo); color: white; border-color: var(--rojo); }
.badge-key { background: #444; color: #ffeb3b; padding: 2px 6px; border-radius: 4px; font-size: 12px; margin-right: 4px; display: inline-block; }
.badge-cat { background: rgba(211, 47, 47, 0.2); color: #ff8a80; padding: 2px 6px; border-radius: 4px; font-size: 11px; font-weight: bold; text-transform: uppercase; }
</style>
</head>
<body>
<div class="container">
<header>
<a href="dashboard.php" class="btn-volver" title="Volver al Panel Principal">
<i class="fas fa-arrow-left"></i>
</a>
<i class="fas fa-robot"></i>
<h1>Asistente Virtual La Pecosa - Administración</h1>
</header>
<?php if($mensaje_alerta): ?>
<div class="alerta"><?php echo $mensaje_alerta; ?></div>
<?php endif; ?>
<div class="grid">
<div class="card">
<h2>Nueva pregunta frecuente</h2>
<form action="asistente.php" method="POST">
<div class="form-group">
<label>Título de la pregunta (Referencia interna):</label>
<input type="text" name="nueva_pregunta" placeholder="Ej: Precio de las Milanesas" required>
</div>
<div class="form-group">
<label>Categoría del bot:</label>
<select name="categoria_existente" id="categoria_select" onchange="alternarNuevaCategoria(this.value)" required>
<option value="">-- Seleccioná una categoría --</option>
<?php while($c = $categorias_existentes->fetch_assoc()): ?>
<option value="<?php echo htmlspecialchars($c['categoria']); ?>">
<?php echo htmlspecialchars(ucfirst($c['categoria'])); ?>
</option>
<?php endwhile; ?>
<option value="nueva" style="color: #ff8a80;">+ Crear una nueva categoría...</option>
</select>
</div>
<div id="campo_nueva_categoria" class="form-group" style="display: none;">
<label>Nombre de la nueva categoría:</label>
<input type="text" name="nueva_categoria" id="nueva_categoria_input" placeholder="Ej: promos, horarios, envios">
</div>
<div class="form-group">
<label>Palabras clave activadoras (Separadas por comas):</label>
<input type="text" name="nuevas_keywords" placeholder="Ej: milanesa, milanesas, precio, cuanto cuesta" required>
<small style="color: #aaa; font-size: 11px;">Si el usuario escribe alguna de estas palabras, el bot responderá.</small>
</div>
<div class="form-group">
<label>El asistente virtual responde:</label>
<textarea name="nueva_respuesta" placeholder="Ej: La porción de milanesa con fritas cuesta..." required></textarea>
</div>
<button type="submit" name="agregar_pregunta" class="btn" style="background: #e02828;">Añadir al asistente virtual</button>
</form>
</div>
<div class="card">
<h2>Base de conocimiento</h2>
<table>
<thead>
<tr>
<th>Pregunta / Keywords</th>
<th>Categoría</th>
<th>Respuesta configurada</th>
<th>Acción</th>
</tr>
</thead>
<tbody>
<?php while($p = $preguntas->fetch_assoc()): ?>
<tr>
<td>
<strong><?php echo htmlspecialchars($p['pregunta']); ?></strong><br>
<span style="font-size: 12px; color: #bbb;">Palabras clave:</span><br>
<?php
$kw = explode(',', $p['keywords']);
foreach($kw as $k) {
if(trim($k) != "") echo '<span class="badge-key">'.htmlspecialchars(trim($k)).'</span>';
}
?>
</td>
<td>
<span class="badge-cat"><?php echo htmlspecialchars($p['categoria']); ?></span>
</td>
<td><?php echo htmlspecialchars($p['respuesta']); ?></td>
<td align="center">
<a href="asistente.php?eliminar=<?php echo $p['id']; ?>" class="btn-del" onclick="return confirm('¿Eliminar esta respuesta?')">
<i class="fas fa-trash-can"></i>
</a>
</td>
</tr>
<?php endwhile; ?>
</tbody>
</table>
</div>
</div>
</div>
<script>
function alternarNuevaCategoria(valor) {
const contenedorNueva = document.getElementById('campo_nueva_categoria');
const inputNueva = document.getElementById('nueva_categoria_input');
if (valor === 'nueva') {
contenedorNueva.style.display = 'block';
inputNueva.required = true;
inputNueva.focus();
} else {
contenedorNueva.style.display = 'none';
inputNueva.required = false;
inputNueva.value = '';
}
}
</script>
</body>
</html>
+622
View File
@@ -0,0 +1,622 @@
<?php
include("../db.php");
// Usamos tu conexión real de mysqli para no romper nada
$preguntas_bot = [];
if (isset($conexion)) {
$result = mysqli_query($conexion, "SELECT keywords FROM bot_preguntas");
if ($result) {
while ($row = mysqli_fetch_assoc($result)) {
$preguntas_bot[] = $row;
}
}
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manual de Administrador - La Pecosa</title>
<link rel="stylesheet" href="../css/all.min.css">
<link rel="stylesheet" href="admin_style.css">
<style>
/* ── TABS ── */
.help-tabs-bar {
display: flex;
gap: 2px;
margin-bottom: 24px;
border-bottom: 2px solid #e5e7eb;
overflow-x: auto;
}
.htab {
background: none;
border: none;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
padding: 12px 18px;
font-size: 0.88rem;
font-weight: 600;
color: #6b7280;
cursor: pointer;
white-space: nowrap;
display: flex;
align-items: center;
gap: 7px;
transition: color 0.15s;
}
.htab:hover { color: #dc2626; }
.htab.active { color: #dc2626; border-bottom-color: #dc2626; }
.htab-content { display: none; }
.htab-content.active { display: block; animation: fadeUp 0.25s ease; }
@keyframes fadeUp {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
/* ── SECTION LABEL ── */
.sec-label {
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.8px;
color: #9ca3af;
margin-bottom: 12px;
}
/* ── CARDS ── */
.help-card {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 10px;
padding: 20px 22px;
margin-bottom: 14px;
}
.help-card-title {
font-size: 0.95rem;
font-weight: 700;
color: #111827;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
}
.help-card-title .card-icon {
width: 28px;
height: 28px;
border-radius: 6px;
background: #fee2e2;
color: #dc2626;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
flex-shrink: 0;
}
.help-card p {
font-size: 0.875rem;
color: #4b5563;
line-height: 1.6;
}
.help-card p + p { margin-top: 6px; }
/* ── INSET BOX (detalle dentro de card) ── */
.inset {
background: #f9fafb;
border: 1px solid #f3f4f6;
border-radius: 7px;
padding: 14px 16px;
margin-top: 12px;
font-size: 0.855rem;
color: #374151;
line-height: 1.65;
}
.inset strong { color: #111827; }
/* ── ALERT BANNERS ── */
.alert-info {
background: #eff6ff;
border: 1px solid #bfdbfe;
color: #1e40af;
border-radius: 8px;
padding: 13px 16px;
font-size: 0.855rem;
display: flex;
gap: 10px;
align-items: flex-start;
line-height: 1.55;
margin-top: 14px;
}
.alert-warn {
background: #fef2f2;
border: 1px solid #fee2e2;
color: #991b1b;
border-radius: 8px;
padding: 13px 16px;
font-size: 0.855rem;
display: flex;
gap: 10px;
align-items: flex-start;
line-height: 1.55;
margin-top: 14px;
}
.alert-info i, .alert-warn i { margin-top: 2px; flex-shrink: 0; }
/* ── BADGES ── */
.badge {
display: inline-block;
padding: 3px 9px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 700;
}
.b-pend { background: #fef9c3; color: #854d0e; }
.b-cocina { background: #fce7f3; color: #9d174d; }
.b-listo { background: #cffafe; color: #155e75; }
.b-camino { background: #dbeafe; color: #1e40af; }
.b-entrega { background: #dcfce7; color: #166534; }
.b-cancel { background: #fee2e2; color: #991b1b; }
/* ── STATUS FLOW (pedidos) ── */
.status-flow {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 14px;
}
.status-row {
display: flex;
align-items: flex-start;
gap: 10px;
font-size: 0.855rem;
color: #374151;
line-height: 1.45;
}
/* ── CSV CHIP ── */
.csv-chip {
display: inline-block;
background: #f1f5f9;
border: 1px solid #e2e8f0;
border-radius: 5px;
padding: 3px 8px;
font-family: monospace;
font-size: 0.8rem;
color: #0f172a;
font-weight: 600;
}
/* ── TWO-COL GRID ── */
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
margin-bottom: 14px;
}
@media (max-width: 640px) { .two-col { grid-template-columns: 1fr; } }
/* ── MINI BTN SIMS ── */
.sim-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 9px;
border-radius: 5px;
font-size: 0.75rem;
font-weight: 700;
vertical-align: middle;
}
.sim-red { background: #dc2626; color: #fff; }
.sim-green { background: #16a34a; color: #fff; }
.sim-dark { background: #1f2937; color: #fff; }
.sim-blue { background: #2563eb; color: #fff; }
/* ── INTERFAZ VISTA PREVIA CHATBOT (Fiel a la Realidad) ── */
.chat-demo {
background: white;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(0,0,0,0.08);
border: 1px solid #eaeaea;
overflow: hidden;
max-width: 380px;
margin: 15px auto 0 auto;
}
.chat-demo-header {
background: #e02828;
color: white;
padding: 14px 18px;
font-weight: 700;
font-size: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-demo-header .header-title {
display: flex;
align-items: center;
gap: 8px;
}
.chat-demo-header .close-bot {
font-size: 1.1rem;
opacity: 0.8;
cursor: pointer;
}
.chat-demo-body {
padding: 18px;
display: flex;
flex-direction: column;
gap: 14px;
background: #fff;
min-height: 220px;
}
.chat-bubble {
max-width: 85%;
padding: 10px 14px;
border-radius: 14px;
font-size: 0.88rem;
line-height: 1.4;
position: relative;
}
.bubble-bot {
background: #fff;
color: #1a1a1a;
align-self: flex-start;
border-top-left-radius: 4px;
border: 1px solid #eaeaea;
}
.bubble-bot::before {
content: '';
position: absolute;
left: 4px;
top: 10px;
bottom: 10px;
width: 3px;
background: #e02828;
border-radius: 2px;
}
.bubble-bot-text {
padding-left: 6px;
}
.bubble-user {
background: #f1f5f9;
color: #334155;
align-self: flex-end;
border-bottom-right-radius: 4px;
}
.chat-demo-footer {
padding: 12px 18px;
background: #fff;
border-top: 1px solid #f3f3f3;
}
.chat-input-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.fake-input {
flex: 1;
border: 1.5px solid #e02828;
border-radius: 25px;
padding: 8px 16px;
font-size: 0.85rem;
color: #777;
background: #fff;
}
.fake-send-btn {
background: #e02828;
color: white;
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.85rem;
}
.chat-keywords-row {
font-size: 0.78rem;
color: #a0a0a0;
text-align: center;
padding-top: 2px;
}
</style>
</head>
<body>
<?php include("sidebar.php"); ?>
<div class="main-content">
<div class="page-header">
<h1><i class="fa-solid fa-screwdriver-wrench"></i> Manual del Administrador</h1>
</div>
<div class="help-tabs-bar">
<button class="htab active" onclick="switchTab(event,'t-dashboard')">
<i class="fa-solid fa-chart-line"></i> Estadísticas
</button>
<button class="htab" onclick="switchTab(event,'t-pedidos')">
<i class="fa-solid fa-utensils"></i> Pedidos
</button>
<button class="htab" onclick="switchTab(event,'t-inventario')">
<i class="fa-solid fa-boxes-stacked"></i> Inventario
</button>
<button class="htab" onclick="switchTab(event,'t-promos')">
<i class="fa-solid fa-bullhorn"></i> Promociones
</button>
<button class="htab" onclick="switchTab(event,'t-bot')">
<i class="fa-solid fa-robot"></i> Asistente virtual
</button>
<button class="htab" onclick="switchTab(event,'t-backup')">
<i class="fa-solid fa-database"></i> Backups
</button>
</div>
<div id="t-dashboard" class="htab-content active">
<p class="sec-label">Panel de control</p>
<div class="help-card">
<div class="help-card-title">
<span class="card-icon"><i class="fa-solid fa-gauge"></i></span>
Tarjetas de resumen en tiempo real
</div>
<p>El dashboard muestra tres métricas clave al tope de la página:</p>
<div class="inset">
<strong>Ventas de hoy</strong> — sumatoria monetaria de todos los pedidos del día.<br><br>
<strong>Pedidos activos</strong> — contador de órdenes en estado <span class="badge b-pend">Pendiente</span> o <span class="badge b-cocina">En cocina</span> que todavía no se cerraron.<br><br>
</div>
</div>
<div class="help-card">
<div class="help-card-title">
<span class="card-icon"><i class="fa-solid fa-chart-bar"></i></span>
Top 5 productos más vendidos
</div>
<p>Gráfico de barras que acumula unidades despachadas por artículo. Útil para medir el impacto de las promociones del día y planificar insumos de alta rotación.</p>
</div>
<div class="help-card">
<div class="help-card-title">
<span class="card-icon"><i class="fa-solid fa-list-check"></i></span>
Últimos 10 movimientos
</div>
<p>Historial rápido con cliente, monto total y estado de cada orden. Ideal para un vistazo rápido sin entrar al módulo de Pedidos.</p>
</div>
<div class="help-card">
<div class="help-card-title">
<span class="card-icon"><i class="fa-solid fa-toggle-on"></i></span>
Abrir / Cerrar la página al público
</div>
<p>El botón en la esquina superior derecha del apartado Estadísticas restringe temporalmente el acceso de compra en el frontend. Usalo cuando la cocina esté saturada o surja algún imprevisto.</p>
<div class="alert-info" style="margin-top: 10px;">
<i class="fa-solid fa-circle-info"></i>
<span>Cuando la página está cerrada, los clientes ven un aviso pero no pueden agregar productos al carrito ni confirmar pedidos.</span>
</div>
</div>
</div>
<div id="t-pedidos" class="htab-content">
<p class="sec-label">Gestión de comandas</p>
<div class="help-card">
<div class="help-card-title">
<span class="card-icon"><i class="fa-solid fa-magnifying-glass"></i></span>
Búsqueda y filtrado
</div>
<p>Escribí en la barra superior el <strong>#ID del pedido</strong>, el <strong>nombre del cliente</strong> o su <strong>ID de usuario</strong> y presioná <span class="sim-btn sim-red">Buscar</span>. El sistema filtra las filas al instante, útil cuando un cliente llama a consultar por su orden.</p>
</div>
<div class="help-card">
<div class="help-card-title">
<span class="card-icon"><i class="fa-solid fa-receipt"></i></span>
Ver detalle y comprobantes
</div>
<p>El botón <strong>"Ver detalle"</strong> abre el desglose exacto del pedido: productos, cantidades y notas del cliente.</p>
<div class="inset">
<strong>Comprobante de transferencia:</strong> aparece automáticamente solo cuando el cliente eligió envío a domicilio con pago por transferencia. Expandilo para verificar que el pago haya impactado antes de despachar.
</div>
</div>
<div class="help-card">
<div class="help-card-title">
<span class="card-icon"><i class="fa-solid fa-arrow-right-arrow-left"></i></span>
Flujo de estados
</div>
<p>Para cambiar la fase de un pedido: desplegá el selector en la columna <strong>"Cambiar estado"</strong>, elegí la instancia y confirmá con <span class="sim-btn sim-dark">✓</span>.</p>
<div class="status-flow">
<div class="status-row"><span class="badge b-pend">Pendiente</span> El pedido ingresó al sistema y espera ser tomado por cocina.</div>
<div class="status-row"><span class="badge b-cocina">En cocina</span> Los cocineros ya están preparando la orden.</div>
<div class="status-row"><span class="badge b-listo">Listo para retirar</span> Para modalidad para llevar. El cliente puede pasar a buscarlo.</div>
<div class="status-row"><span class="badge b-camino">En camino</span> El delivery está llevando el pedido al domicilio.</div>
<div class="status-row"><span class="badge b-entrega">Entregado</span> Cierre definitivo. Las ventas se contabilizan en el panel de Estadísticas.</div>
<div class="status-row"><span class="badge b-cancel">Cancelado</span> Falta de insumos, pago rechazado o cancelación del cliente.</div>
</div>
</div>
</div>
<div id="t-inventario" class="htab-content">
<p class="sec-label">Menú y stock</p>
<div class="two-col">
<div class="help-card" style="margin-bottom:0">
<div class="help-card-title">
<span class="card-icon" style="background:#dcfce7; color:#16a34a;"><i class="fa-solid fa-plus"></i></span>
Agregar un producto
</div>
<p>Completá nombre, categoría, precio, stock inicial y foto. Ideal para platos del día o artículos nuevos de forma rápida.</p>
</div>
<div class="help-card" style="margin-bottom:0">
<div class="help-card-title">
<span class="card-icon" style="background:#dbeafe; color:#2563eb;"><i class="fa-solid fa-file-csv"></i></span>
Carga masiva (.csv)
</div>
<p>Para actualizaciones grandes de precios o stocks. Armá el Excel, guardalo como CSV y presioná <span class="sim-btn sim-green">Procesar catálogo</span>.</p>
</div>
</div>
<div class="help-card">
<div class="help-card-title">
<span class="card-icon"><i class="fa-solid fa-table"></i></span>
Estructura requerida del CSV
</div>
<p>El archivo debe tener exactamente estas columnas, separadas por punto y coma:</p>
<div class="inset" style="font-family: monospace; font-size: 0.85rem; letter-spacing: 0.3px;">
<span class="csv-chip">id</span> ; <span class="csv-chip">nombre</span> ; <span class="csv-chip">precio</span> ; <span class="csv-chip">categoria</span> ; <span class="csv-chip">imagen</span> ; <span class="csv-chip">activo</span> ; <span class="csv-chip">stock</span>
</div>
<p style="margin-top: 10px;">Categorías válidas: <em>Almacén, Carne, Bebidas, Limpieza, Lácteos, Verdura, Vinoteca, Panadería.</em></p>
</div>
<div class="help-card">
<div class="help-card-title">
<span class="card-icon" style="background:#fee2e2; color:#dc2626;"><i class="fa-solid fa-triangle-exclamation"></i></span>
Regla de quiebre de stock
</div>
<p>Cuando un artículo llega a <strong>0 unidades</strong>, el botón "Agregar al carrito" se deshabilita automáticamente en el frontend. El cliente ve el producto pero no puede comprarlo. Actualizá el stock apenas haya reposición para reactivarlo.</p>
</div>
</div>
<div id="t-promos" class="htab-content">
<p class="sec-label">Ofertas y menú del día</p>
<div class="help-card">
<div class="help-card-title">
<span class="card-icon"><i class="fa-solid fa-bolt"></i></span>
Publicar una promoción
</div>
<p>Completá los campos del formulario y presioná <span class="sim-btn sim-red">Publicar promoción</span>. La oferta aparece inmediatamente en la portada del cliente.</p>
<div class="inset">
<strong>Mensaje:</strong> El texto que verá el cliente. Ej: <em>¡2 Lomitos + papas fritas por $X!</em><br><br>
<strong>Precio y stock:</strong> El valor de oferta y los combos límite disponibles para cocina.<br><br>
<strong>Imagen:</strong> Acepta <span class="csv-chip">PNG</span>, <span class="csv-chip">JPG</span> o <span class="csv-chip">WEBP</span>. Se muestra como banner en el catálogo.
</div>
</div>
<div class="help-card">
<div class="help-card-title">
<span class="card-icon"><i class="fa-solid fa-clock-rotate-left"></i></span>
Historial y baja de promociones
</div>
<p>La tabla inferior registra todas las ofertas lanzadas con fecha, hora y mensaje. Para dar de baja una promo, presioná el <span style="color:#dc2626; font-weight:700;"><i class="fa-solid fa-trash-can"></i> ícono de borrar</span> en la columna Acción, se elimina del frontend de forma inmediata.</p>
</div>
</div>
<div id="t-bot" class="htab-content">
<p class="sec-label">Respuestas automáticas</p>
<div class="two-col">
<div>
<div class="help-card">
<div class="help-card-title">
<span class="card-icon"><i class="fa-solid fa-comment-medical"></i></span>
Añadir Preguntas y Palabras Clave
</div>
<p>El bot responde de forma dinámica basándose en coincidencias de términos. Rellená los siguientes campos para alimentarlo:</p>
<div class="inset">
<strong>Pregunta de referencia:</strong> Título orientativo de la consulta para organizarte en el panel administrativo.<br><br>
<strong>Categoría del bot:</strong> Desplegable obligatorio para agrupar las respuestas. Permite reutilizar una categoría previa de la lista o seleccionar <span style="color:#dc2626; font-weight:bold;">"+ Crear una nueva categoría..."</span>, lo que desplegará dinámicamente un campo de texto adicional para que agregues una nueva.<br><br>
<strong>Respuesta del asistente virtual:</strong> El mensaje exacto que el cliente va a recibir cuando invoque la palabra clave.<br><br>
<strong>Palabras clave (Keywords):</strong> Términos separados por comas que dispararán la respuesta. Ej: <code class="csv-chip">horario,abierto,cierran</code>. El sistema procesa los ingresos omitiendo mayúsculas y acentos automáticamente.
</div>
</div>
<div class="help-card">
<div class="help-card-title">
<span class="card-icon" style="background:#fee2e2; color:#dc2626;"><i class="fa-solid fa-lightbulb"></i></span>
Recomendaciones de configuración
</div>
<p>Mantené las palabras clave cortas e intuitivas (una sola palabra por ítem). Si una consulta del usuario no coincide con ninguna palabra clave de la base de datos, el bot responderá con un mensaje genérico invitándolo a comunicarse por teléfono.</p>
</div>
</div>
<div>
<p class="sec-label" style="text-align: center;">Vista previa del asistente virtual</p>
<div class="chat-demo">
<div class="chat-demo-header">
<span class="header-title">🍎 La Pecosa Asistente Virtual</span>
<span class="close-bot"><i class="fa-solid fa-xmark"></i></span>
</div>
<div class="chat-demo-body">
<div class="chat-bubble bubble-bot">
<div class="bubble-bot-text">¡Hola! Soy el asistente de La Pecosa 🍎 ¿En qué puedo ayudarte?</div>
</div>
<div class="chat-bubble bubble-user">
¿Aceptan tarjeta?
</div>
<div class="chat-bubble bubble-bot">
<div class="bubble-bot-text">Aceptamos efectivo y transferencia bancaria. Al retirar tu pedido podés coordinar el método de pago con el local.</div>
</div>
</div>
<div class="chat-demo-footer">
<div class="chat-input-row">
<div class="fake-input">Escribí tu consulta...</div>
<div class="fake-send-btn"><i class="fa-solid fa-paper-plane"></i></div>
</div>
<div class="chat-keywords-row">
<?php
if(!empty($preguntas_bot)) {
$listado_keys = [];
foreach($preguntas_bot as $pb) {
$keys_array = explode(',', $pb['keywords']);
if(!empty($keys_array[0])) {
$listado_keys[] = strtolower(trim($keys_array[0]));
}
}
echo htmlspecialchars(implode(' · ', array_unique($listado_keys)));
} else {
echo "horarios · ubicación · pagos · teléfono · novedades · redes";
}
?>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="t-backup" class="htab-content">
<p class="sec-label">Copias de seguridad</p>
<div class="help-card">
<div class="help-card-title">
<span class="card-icon"><i class="fa-solid fa-cloud-arrow-down"></i></span>
Generar un respaldo completo
</div>
<p>Presioná <span class="sim-btn sim-red"><i class="fa-solid fa-cloud-arrow-down"></i> Generar respaldo (.zip)</span>. El servidor empaqueta automáticamente:</p>
<div class="inset">
<strong>Volcado SQL</strong> — estructura y datos de todas las tablas: pedidos, clientes, stock, promociones.<br><br>
<strong>Archivos de configuración</strong> — <span class="csv-chip">db.php</span> y configuraciones del sistema necesarias para reinstalar.<br><br>
<strong>Imágenes</strong> — todos los recursos multimedia subidos por la administración (<span class="csv-chip">/img</span> e <span class="csv-chip">/imagenes</span>).
</div>
<div class="alert-warn">
<i class="fa-solid fa-triangle-exclamation"></i>
<span>No cerrés ni recargués la pestaña mientras se genera el backup. El proceso puede tardar unos segundos porque comprime imágenes además del SQL. El archivo aparecerá en la tabla inferior cuando esté listo.</span>
</div>
</div>
<div class="help-card">
<div class="help-card-title">
<span class="card-icon" style="background:#dcfce7; color:#16a34a;"><i class="fa-solid fa-download"></i></span>
Descargar y guardar el respaldo
</div>
<p>Los archivos generados se listan en la tabla con su nombre de marcación temporal (<span class="csv-chip">backup_sistema_completo_YYYY-MM-DD.zip</span>). Presioná <span class="sim-btn sim-green"><i class="fa-solid fa-download"></i> Descargar .zip</span> para guardar la copia en tu computadora.</p>
<div class="alert-info">
<i class="fa-solid fa-lightbulb"></i>
<span>Guardá el ZIP en un pendrive o servicio de nube externo (Google Drive, Dropbox). Si el servidor falla, con el ZIP y el código fuente en GitHub podés restaurar el sistema completo.</span>
</div>
</div>
</div>
</div>
<script>
function switchTab(evt, tabId) {
document.querySelectorAll('.htab-content').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.htab').forEach(b => b.classList.remove('active'));
document.getElementById(tabId).classList.add('active');
evt.currentTarget.classList.add('active');
}
</script>
</body>
</html>
+276
View File
@@ -0,0 +1,276 @@
<?php
include("../db.php");
if (!isset($_SESSION['rol']) || $_SESSION['rol'] !== 'admin') {
header("Location: ../login.php?error=acceso_denegado");
exit();
}
// Configuración de la carpeta donde se guardan los archivos de respaldo comprimidos
$carpeta_backups = "backups_files/";
// Si no existe la carpeta, la creamos automáticamente con permisos
if (!file_exists($carpeta_backups)) {
mkdir($carpeta_backups, 0777, true);
}
$mensaje = "";
$tipo_mensaje = "";
// --- PROCESO DE GENERACIÓN DE BACKUP INTEGRAL ---
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['crear_backup'])) {
// Nombres de archivos con marcas de tiempo únicas
$timestamp = date("Y-m-d_H-i-s");
$nombre_sql = "backup_la_pecosa_" . $timestamp . ".sql";
$nombre_zip = "backup_sistema_completo_" . $timestamp . ".zip";
$ruta_sql_temporal = $carpeta_backups . $nombre_sql;
$ruta_zip_final = $carpeta_backups . $nombre_zip;
// Parámetros locales para XAMPP
$host = "localhost";
$user = "root";
$bd = "lapecosa";
// 1. Ejecutar comando para extraer el volcado SQL de las tablas
// Buscar mysqldump automáticamente en Laragon
$laragon_mysql = glob("C:/laragon/bin/mysql/*/bin/mysqldump.exe");
$mysqldump_path = !empty($laragon_mysql) ? $laragon_mysql[0] : "mysqldump";
$comando = "\"$mysqldump_path\" --no-defaults --user=$user --host=$host $bd > $ruta_sql_temporal";
system($comando, $resultado_sql);
// 2. Si el SQL se generó correctamente, procedemos a empaquetar archivos de configuración y recursos
if ($resultado_sql === 0 && file_exists($ruta_sql_temporal)) {
$zip = new ZipArchive();
if ($zip->open($ruta_zip_final, ZipArchive::CREATE) === TRUE) {
// A. Añadir el script SQL de la Base de Datos al ZIP
$zip->addFile($ruta_sql_temporal, $nombre_sql);
// B. Añadir el archivo crítico de configuración técnica (db.php)
if (file_exists("../db.php")) {
$zip->addFile("../db.php", "config/db.php");
}
// C. Añadir de forma recursiva la carpeta de imágenes generales de diseño (../imagenes)
$ruta_imagenes = "../imagenes";
if (file_exists($ruta_imagenes)) {
$archivos_imagenes = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($ruta_imagenes, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($archivos_imagenes as $item) {
$ruta_real = $item->getRealPath();
$ruta_relativa_zip = "imagenes_sitio/" . substr($ruta_real, strlen(realpath($ruta_imagenes)) + 1);
if ($item->isDir()) {
$zip->addEmptyDir($ruta_relativa_zip);
} else {
$zip->addFile($ruta_real, $ruta_relativa_zip);
}
}
}
// Añadir de forma recursiva la carpeta de fotos de productos cargados (../img)
$ruta_img_productos = "../img";
if (file_exists($ruta_img_productos)) {
$archivos_productos = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($ruta_img_productos, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($archivos_productos as $item) {
$ruta_real = $item->getRealPath();
// Lo guardamos estructurado bajo una carpeta propia dentro del ZIP
$ruta_relativa_zip = "imagenes_productos/" . substr($ruta_real, strlen(realpath($ruta_img_productos)) + 1);
if ($item->isDir()) {
$zip->addEmptyDir($ruta_relativa_zip);
} else {
$zip->addFile($ruta_real, $ruta_relativa_zip);
}
}
}
// Cerramos y guardamos definitivamente el archivo ZIP
$zip->close();
// Borramos el archivo .sql suelto temporal para no duplicar espacio en el disco duro
unlink($ruta_sql_temporal);
header("Location: backup.php?status=success&file=$nombre_zip");
} else {
header("Location: backup.php?status=error_zip");
}
} else {
header("Location: backup.php?status=error_sql");
}
exit();
}
if (isset($_GET['status'])) {
if ($_GET['status'] === 'success') {
$file = htmlspecialchars($_GET['file']);
$mensaje = "¡Copia de seguridad del sistema completo generada con éxito! Archivo: $file";
$tipo_mensaje = "success";
} elseif ($_GET['status'] === 'error_sql') {
$mensaje = "Error al generar el volcado SQL de la Base de Datos. Verifique la ruta de mysqldump en XAMPP.";
$tipo_mensaje = "error";
} elseif ($_GET['status'] === 'error_zip') {
$mensaje = "Error al comprimir el archivo ZIP. Asegúrese de que PHP tenga extensiones activas y permisos de escritura.";
$tipo_mensaje = "error";
}
}
// Escaneamos la carpeta buscando únicamente los archivos ZIP generados
$archivos_guardados = array_diff(scandir($carpeta_backups), array('.', '..'));
$archivos_guardados = array_filter($archivos_guardados, function($archivo) {
return pathinfo($archivo, PATHINFO_EXTENSION) === 'zip';
});
rsort($archivos_guardados);
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Copia de Seguridad - La Pecosa</title>
<link rel="stylesheet" href="../css/all.min.css">
<link rel="stylesheet" href="admin_style.css">
<style>
.box-backup {
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.02);
border: 1px solid rgba(0,0,0,0.04);
margin-bottom: 35px;
display: flex;
align-items: center;
justify-content: space-between;
}
.box-backup-info h2 { font-size: 1.2rem; margin-bottom: 8px; color: #222; }
.box-backup-info p { font-size: 0.85rem; color: #666; max-width: 500px; line-height: 1.4; }
.btn-backup {
background-color: #dc2626;
color: white;
border: none;
padding: 14px 28px;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
font-size: 1rem;
box-shadow: 0 4px 10px rgba(220, 38, 38, 0.2);
transition: 0.3s ease;
display: flex;
align-items: center;
gap: 10px;
}
.btn-backup:hover {
background-color: #b91c1c;
transform: translateY(-1px);
box-shadow: 0 6px 15px rgba(220, 38, 38, 0.3);
}
.alert-ui { padding: 15px; border-radius: 8px; margin-bottom: 25px; font-weight: 600; font-size: 0.95rem; }
.alert-ui.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.alert-ui.error { background: #fdf2f2; color: #e02828; border: 1px solid #fde2e2; }
.btn-download {
background: #e99535;
color: white;
padding: 8px 14px;
border-radius: 6px;
text-decoration: none;
font-size: 0.85rem;
font-weight: bold;
transition: 0.2s ease;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn-download:hover {
background: #eb830b;
transform: translateY(-1px);
}
</style>
</head>
<body>
<?php include("sidebar.php"); ?>
<div class="main-content">
<div class="page-header">
<h1><i class="fa-solid fa-database"></i> Copias de seguridad (Backup integral)</h1>
</div>
<?php if(!empty($mensaje)): ?>
<div class="alert-ui <?php echo $tipo_mensaje; ?>">
<i class="fa-solid <?php echo $tipo_mensaje === 'success' ? 'fa-circle-check' : 'fa-circle-exclamation'; ?>"></i>
<?php echo $mensaje; ?>
</div>
<?php endif; ?>
<div class="box-backup">
<div class="box-backup-info">
<h2>Respaldar sistema completo e infraestructura</h2>
<p>
Al hacer clic en el botón se empaquetará un archivo comprimido <strong>.ZIP</strong> conteniendo el volcado SQL de la base de datos (tablas, pedidos, menú), los archivos de conexión y configuración del sistema, junto con la totalidad de imágenes y recursos multimedia subidos por la administración de La Pecosa.
</p>
</div>
<form action="backup.php" method="POST">
<button type="submit" name="crear_backup" class="btn-backup">
<i class="fa-solid fa-cloud-arrow-down"></i> Generar respaldo (.zip)
</button>
</form>
</div>
<div class="table-container">
<h2 style="font-size: 1.1rem; margin-bottom: 15px; color: #222;"><i class="fa-solid fa-folder-open"></i> Respaldos integrales guardados en el servidor</h2>
<table class="admin-table">
<thead>
<tr>
<th>Nombre del Archivo Completo</th>
<th>Tipo de Paquete</th>
<th>Ubicación en Servidor</th>
<th>Acción</th>
</tr>
</thead>
<tbody>
<?php if (empty($archivos_guardados)): ?>
<tr>
<td colspan="4" style="text-align: center; color: #888; padding: 25px;">
<i class="fa-solid fa-box-open" style="font-size: 1.5rem; margin-bottom: 8px;"></i><br>
No se encontraron paquetes comprimidos de seguridad.
</td>
</tr>
<?php else: ?>
<?php foreach ($archivos_guardados as $archivo_zip): ?>
<tr>
<td><strong><i class="fa-solid fa-file-archive" style="color: #f59e0b; margin-right: 5px;"></i> <?php echo $archivo_zip; ?></strong></td>
<td><span style="background: #fffbeb; padding: 3px 8px; border-radius: 4px; font-size: 0.8rem; font-weight: bold; color: #b45309; border: 1px solid #fef3c7;">ZIP (BD + Archivos)</span></td>
<td><code>/admin/backups_files/</code></td>
<td>
<a href="descargar_file.php?file=<?php echo urlencode($archivo_zip); ?>" class="btn-download" style="background-color: #10b981;">
<i class="fa-solid fa-download"></i> Descargar .zip
</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</body>
</html>
+34
View File
@@ -0,0 +1,34 @@
<?php
include("../db.php");
if (!isset($_SESSION['rol']) || $_SESSION['rol'] !== 'admin') {
header("Location: ../login.php?error=acceso_denegado");
exit();
}
if (isset($_GET['file'])) {
$archivo = basename($_GET['file']);
$ruta_completa = "backups_files/" . $archivo;
if (file_exists($ruta_completa)) {
// MEJORA: Limpia y deshabilita los buffers de salida para evitar corromper el ZIP
if (ob_get_level()) {
ob_end_clean();
}
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $archivo . '"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($ruta_completa));
flush();
readfile($ruta_completa);
exit();
} else {
echo "El archivo no existe.";
}
}
?>