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';
|
elseif ($porcentaje > 0) $barColor = 'bg-primary';
|
||||||
?>
|
?>
|
||||||
<tr>
|
<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>
|
<td>
|
||||||
<strong><?php echo htmlspecialchars($ingreso['nombre_producto']); ?></strong>
|
<strong><?php echo htmlspecialchars($ingreso['nombre_producto']); ?></strong>
|
||||||
<?php if ($ingreso['product_id']): ?>
|
<?php if ($ingreso['product_id']): ?>
|
||||||
|
|||||||
@ -251,11 +251,24 @@ $navItems = [
|
|||||||
],
|
],
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
'configuracion_group' => [
|
||||||
|
'icon' => 'fa-cog',
|
||||||
|
'text' => 'Configuración',
|
||||||
|
'roles' => ['Administrador', 'admin'],
|
||||||
|
'submenu' => [
|
||||||
'configuracion' => [
|
'configuracion' => [
|
||||||
'url' => 'configuracion.php',
|
'url' => 'configuracion.php',
|
||||||
'icon' => 'fa-cog',
|
'icon' => 'fa-cog',
|
||||||
'text' => 'Configuración',
|
'text' => 'Configuración',
|
||||||
'roles' => ['Administrador', 'admin']
|
'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['user_id'] = $user['id'];
|
||||||
$_SESSION['username'] = $user['username'];
|
$_SESSION['username'] = $user['username'];
|
||||||
$_SESSION['user_role'] = $user['role'];
|
$_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');
|
header('Location: pedidos.php');
|
||||||
exit();
|
exit();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -237,11 +237,6 @@ include 'layout_header.php';
|
|||||||
|
|
||||||
"
|
"
|
||||||
. "Quedamos atentos a su confirmación.
|
. "Quedamos atentos a su confirmación.
|
||||||
"
|
|
||||||
. "Muchas gracias por su confianza 🤝
|
|
||||||
|
|
||||||
"
|
|
||||||
. "Floower Store 🛍️
|
|
||||||
"
|
"
|
||||||
. "Área de Atención al Cliente";
|
. "Área de Atención al Cliente";
|
||||||
|
|
||||||
@ -254,18 +249,18 @@ include 'layout_header.php';
|
|||||||
'{SALDO_PENDIENTE}' => number_format($monto_debe, 2)
|
'{SALDO_PENDIENTE}' => number_format($monto_debe, 2)
|
||||||
];
|
];
|
||||||
|
|
||||||
$whatsappMessage = str_replace(array_keys($replacements), array_values($replacements), $template);
|
$whatsappMessageRaw = str_replace(array_keys($replacements), array_values($replacements), $template);
|
||||||
$whatsappMessage = urlencode($whatsappMessage);
|
$whatsappMessageEncoded = urlencode($whatsappMessageRaw);
|
||||||
|
|
||||||
$celular = preg_replace('/[^0-9]/', '', $pedido['celular']);
|
$celular = preg_replace('/[^0-9]/', '', $pedido['celular']);
|
||||||
if (strlen($celular) == 9) {
|
if (strlen($celular) == 9) {
|
||||||
$celular = '51' . $celular;
|
$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">
|
<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="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>
|
<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>
|
</div>
|
||||||
@ -347,6 +342,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
var button = event.relatedTarget;
|
var button = event.relatedTarget;
|
||||||
var orderNumber = button.getAttribute('data-order-number');
|
var orderNumber = button.getAttribute('data-order-number');
|
||||||
var orderCode = button.getAttribute('data-order-code');
|
var orderCode = button.getAttribute('data-order-code');
|
||||||
|
var whatsappMessage = button.getAttribute('data-whatsapp-message');
|
||||||
|
|
||||||
var modalOrderNumberSpan = trackingModal.querySelector('#modal-order-number');
|
var modalOrderNumberSpan = trackingModal.querySelector('#modal-order-number');
|
||||||
var modalOrderCodeSpan = trackingModal.querySelector('#modal-order-code');
|
var modalOrderCodeSpan = trackingModal.querySelector('#modal-order-code');
|
||||||
@ -508,13 +504,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-grid gap-2 mt-4">
|
<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
|
<i class="fab fa-whatsapp"></i> Copiar Resumen para Cliente
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
modalStatusDiv.innerHTML = html;
|
modalStatusDiv.innerHTML = html;
|
||||||
|
|
||||||
|
document.getElementById('btn-copy-summary').addEventListener('click', function() {
|
||||||
|
copyStatusToClipboard(whatsappMessage);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
modalStatusDiv.innerHTML = `<p class="text-warning text-center">No se pudo encontrar la guía.</p>`;
|
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) {
|
function copyStatusToClipboard(message) {
|
||||||
const text = `📦 *Estado de tu pedido Shalom*\n\n` +
|
navigator.clipboard.writeText(message).then(() => {
|
||||||
`🔖 *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(() => {
|
|
||||||
alert('Resumen copiado al portapapeles. Ya puedes pegarlo en WhatsApp.');
|
alert('Resumen copiado al portapapeles. Ya puedes pegarlo en WhatsApp.');
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error('Error al copiar:', 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'; ?>
|
||||||