Autosave: 20260512-105548
BIN
assets/uploads/vouchers/6a01e367e6894-Screenshot_317.png
Normal file
|
After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 190 KiB |
BIN
assets/uploads/vouchers/6a0203449478a-Screenshot_318.png
Normal file
|
After Width: | Height: | Size: 234 KiB |
BIN
assets/uploads/vouchers/6a020419908fd-Screenshot_319.png
Normal file
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 141 KiB |
BIN
assets/uploads/vouchers/6a0225938fe98-Screenshot_320.png
Normal file
|
After Width: | Height: | Size: 235 KiB |
BIN
assets/uploads/vouchers/6a024db09d697-Screenshot_321.png
Normal file
|
After Width: | Height: | Size: 351 KiB |
BIN
assets/uploads/vouchers/6a025c7460a90-Screenshot_322.png
Normal file
|
After Width: | Height: | Size: 473 KiB |
8
db/migrations/032_create_user_sessions_table.sql
Normal file
@ -0,0 +1,8 @@
|
||||
CREATE TABLE IF NOT EXISTS user_sessions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
ip_address VARCHAR(45),
|
||||
user_agent TEXT,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
1
db/migrations/033_add_city_to_user_sessions.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE user_sessions ADD COLUMN ciudad VARCHAR(100) AFTER ip_address;
|
||||
@ -65,7 +65,9 @@ $ingresos = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
elseif ($porcentaje > 0) $barColor = 'bg-primary';
|
||||
?>
|
||||
<tr>
|
||||
<td><?php echo date('d/m/Y H:i', strtotime($ingreso['fecha_registro'])); ?></td>
|
||||
<td data-order="<?php echo strtotime($ingreso['fecha_registro']); ?>">
|
||||
<?php echo date('d/m/Y H:i', strtotime($ingreso['fecha_registro'])); ?>
|
||||
</td>
|
||||
<td>
|
||||
<strong><?php echo htmlspecialchars($ingreso['nombre_producto']); ?></strong>
|
||||
<?php if ($ingreso['product_id']): ?>
|
||||
|
||||
@ -251,11 +251,24 @@ $navItems = [
|
||||
],
|
||||
]
|
||||
],
|
||||
'configuracion' => [
|
||||
'url' => 'configuracion.php',
|
||||
'configuracion_group' => [
|
||||
'icon' => 'fa-cog',
|
||||
'text' => 'Configuración',
|
||||
'roles' => ['Administrador', 'admin']
|
||||
'roles' => ['Administrador', 'admin'],
|
||||
'submenu' => [
|
||||
'configuracion' => [
|
||||
'url' => 'configuracion.php',
|
||||
'icon' => 'fa-cog',
|
||||
'text' => 'Configuración',
|
||||
'roles' => ['Administrador', 'admin']
|
||||
],
|
||||
'sesiones_iniciadas' => [
|
||||
'url' => 'sesiones_iniciadas.php',
|
||||
'icon' => 'fa-user-shield',
|
||||
'text' => 'Sesiones Iniciadas',
|
||||
'roles' => ['Administrador', 'admin']
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
|
||||
27
login.php
@ -28,6 +28,33 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
$_SESSION['user_role'] = $user['role'];
|
||||
|
||||
// Record session
|
||||
try {
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
$ua = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
|
||||
$ciudad = 'Desconocida';
|
||||
|
||||
if ($ip !== 'unknown' && $ip !== '127.0.0.1' && $ip !== '::1') {
|
||||
$geo = @file_get_contents("http://ip-api.com/json/{$ip}?fields=city");
|
||||
if ($geo) {
|
||||
$geoData = json_decode($geo, true);
|
||||
if (isset($geoData['city'])) {
|
||||
$ciudad = $geoData['city'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$stmtSession = $db->prepare('INSERT INTO user_sessions (user_id, ip_address, ciudad, user_agent) VALUES (:user_id, :ip, :ciudad, :ua)');
|
||||
$stmtSession->bindParam(':user_id', $user['id']);
|
||||
$stmtSession->bindParam(':ip', $ip);
|
||||
$stmtSession->bindParam(':ciudad', $ciudad);
|
||||
$stmtSession->bindParam(':ua', $ua);
|
||||
$stmtSession->execute();
|
||||
} catch (PDOException $e) {
|
||||
// Silently fail if session recording fails, don't block login
|
||||
}
|
||||
|
||||
header('Location: pedidos.php');
|
||||
exit();
|
||||
} else {
|
||||
|
||||
@ -237,11 +237,6 @@ include 'layout_header.php';
|
||||
|
||||
"
|
||||
. "Quedamos atentos a su confirmación.
|
||||
"
|
||||
. "Muchas gracias por su confianza 🤝
|
||||
|
||||
"
|
||||
. "Floower Store 🛍️
|
||||
"
|
||||
. "Área de Atención al Cliente";
|
||||
|
||||
@ -254,18 +249,18 @@ include 'layout_header.php';
|
||||
'{SALDO_PENDIENTE}' => number_format($monto_debe, 2)
|
||||
];
|
||||
|
||||
$whatsappMessage = str_replace(array_keys($replacements), array_values($replacements), $template);
|
||||
$whatsappMessage = urlencode($whatsappMessage);
|
||||
$whatsappMessageRaw = str_replace(array_keys($replacements), array_values($replacements), $template);
|
||||
$whatsappMessageEncoded = urlencode($whatsappMessageRaw);
|
||||
|
||||
$celular = preg_replace('/[^0-9]/', '', $pedido['celular']);
|
||||
if (strlen($celular) == 9) {
|
||||
$celular = '51' . $celular;
|
||||
}
|
||||
|
||||
$whatsappUrl = "https://api.whatsapp.com/send?phone={$celular}&text={$whatsappMessage}";
|
||||
$whatsappUrl = "https://api.whatsapp.com/send?phone={$celular}&text={$whatsappMessageEncoded}";
|
||||
?>
|
||||
<div class="mt-1">
|
||||
<button type="button" class="btn btn-sm btn-info" title="Consultar Estado" data-bs-toggle="modal" data-bs-target="#trackingModal" data-order-number="<?php echo htmlspecialchars($pedido['codigo_rastreo'] ?? 'N/A'); ?>" data-order-code="<?php echo htmlspecialchars($pedido['codigo_tracking'] ?? 'N/A'); ?>">🔍</button>
|
||||
<button type="button" class="btn btn-sm btn-info" title="Consultar Estado" data-bs-toggle="modal" data-bs-target="#trackingModal" data-order-number="<?php echo htmlspecialchars($pedido['codigo_rastreo'] ?? 'N/A'); ?>" data-order-code="<?php echo htmlspecialchars($pedido['codigo_tracking'] ?? 'N/A'); ?>" data-whatsapp-message="<?php echo htmlspecialchars($whatsappMessageRaw); ?>">🔍</button>
|
||||
<a href="https://rastrea.shalom.pe/rastrea" target="_blank" class="btn btn-sm btn-primary" title="Rastreo Shalom">🚚</a>
|
||||
<a href="<?php echo $whatsappUrl; ?>" target="_blank" class="btn btn-sm btn-secondary whatsapp-icon" id="whatsapp-icon-<?php echo $pedido['id']; ?>" title="Enviar WhatsApp">💬</a>
|
||||
</div>
|
||||
@ -347,6 +342,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
var button = event.relatedTarget;
|
||||
var orderNumber = button.getAttribute('data-order-number');
|
||||
var orderCode = button.getAttribute('data-order-code');
|
||||
var whatsappMessage = button.getAttribute('data-whatsapp-message');
|
||||
|
||||
var modalOrderNumberSpan = trackingModal.querySelector('#modal-order-number');
|
||||
var modalOrderCodeSpan = trackingModal.querySelector('#modal-order-code');
|
||||
@ -508,13 +504,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-grid gap-2 mt-4">
|
||||
<button class="btn btn-success btn-lg" onclick="copyStatusToClipboard('${statusMessage}', '${orderNumber}', '${searchData.destino.nombre}')">
|
||||
<button class="btn btn-success btn-lg" id="btn-copy-summary">
|
||||
<i class="fab fa-whatsapp"></i> Copiar Resumen para Cliente
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modalStatusDiv.innerHTML = html;
|
||||
|
||||
document.getElementById('btn-copy-summary').addEventListener('click', function() {
|
||||
copyStatusToClipboard(whatsappMessage);
|
||||
});
|
||||
} else {
|
||||
modalStatusDiv.innerHTML = `<p class="text-warning text-center">No se pudo encontrar la guía.</p>`;
|
||||
}
|
||||
@ -614,14 +614,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
|
||||
function copyStatusToClipboard(status, order, destination) {
|
||||
const text = `📦 *Estado de tu pedido Shalom*\n\n` +
|
||||
`🔖 *Nro de Orden:* ${order}\n` +
|
||||
`📍 *Destino:* ${destination}\n` +
|
||||
`🚚 *Estado:* ${status}\n\n` +
|
||||
`¡Tu pedido está en camino! Gracias por tu confianza. ✨`;
|
||||
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
function copyStatusToClipboard(message) {
|
||||
navigator.clipboard.writeText(message).then(() => {
|
||||
alert('Resumen copiado al portapapeles. Ya puedes pegarlo en WhatsApp.');
|
||||
}).catch(err => {
|
||||
console.error('Error al copiar:', err);
|
||||
|
||||
86
sesiones_iniciadas.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once 'db/config.php';
|
||||
|
||||
$user_role = $_SESSION['user_role'];
|
||||
if ($user_role !== 'Administrador' && $user_role !== 'admin') {
|
||||
header('Location: dashboard.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
$pageTitle = "Sesiones Iniciadas";
|
||||
|
||||
// Obtener el historial de sesiones
|
||||
$stmt = $pdo->query("
|
||||
SELECT s.*, u.username, u.nombre_asesor
|
||||
FROM user_sessions s
|
||||
JOIN users u ON s.user_id = u.id
|
||||
ORDER BY s.login_time DESC
|
||||
LIMIT 500
|
||||
");
|
||||
$sessions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
include 'layout_header.php';
|
||||
?>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Historial de Inicios de Sesión (Últimos 500)</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered" id="sessionsTable" width="100%" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Usuario</th>
|
||||
<th>Nombre/Asesor</th>
|
||||
<th>Fecha y Hora</th>
|
||||
<th>Dirección IP</th>
|
||||
<th>Ciudad</th>
|
||||
<th>Navegador / Dispositivo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($sessions as $session): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($session['username']); ?></td>
|
||||
<td><?php echo htmlspecialchars($session['nombre_asesor'] ?? 'N/A'); ?></td>
|
||||
<td><?php echo date('d/m/Y H:i:s', strtotime($session['login_time'])); ?></td>
|
||||
<td><?php echo htmlspecialchars($session['ip_address']); ?></td>
|
||||
<td><span class="badge bg-info text-dark"><?php echo htmlspecialchars($session['ciudad'] ?? 'N/A'); ?></span></td>
|
||||
<td style="font-size: 0.85rem;"><?php echo htmlspecialchars($session['user_agent']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($sessions)): ?>
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">No hay registros de sesiones aún.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#sessionsTable').DataTable({
|
||||
"order": [[ 2, "desc" ]],
|
||||
"language": {
|
||||
"url": "//cdn.datatables.net/plug-ins/1.13.6/i18n/es-ES.json"
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include 'layout_footer.php'; ?>
|
||||