Compare commits

..

1 Commits

Author SHA1 Message Date
Flatlogic Bot
d9d2abcbfc Auto commit: 2025-12-12T16:33:10.009Z 2025-12-12 16:33:10 +00:00
89 changed files with 5650 additions and 146 deletions

36
agregar_ciudad.php Normal file
View File

@ -0,0 +1,36 @@
<?php
session_start();
if (!isset($_SESSION["user_id"])) {
header("Location: auth/login.php");
exit();
}
require_once 'includes/header.php';
?>
<div class="container-fluid">
<div class="d-sm-flex justify-content-between align-items-center mb-4">
<h3 class="text-dark mb-0">Agregar Nueva Ciudad</h3>
</div>
<div class="card shadow">
<div class="card-header py-3">
<p class="text-primary m-0 fw-bold">Datos de la Ciudad</p>
</div>
<div class="card-body">
<form action="handle_agregar_ciudad.php" method="post">
<div class="row">
<div class="col-md-12">
<div class="mb-3">
<label for="nombre" class="form-label">Nombre de la Ciudad</label>
<input type="text" class="form-control" id="nombre" name="nombre" required>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Agregar Ciudad</button>
<a href="ciudades.php" class="btn btn-secondary">Cancelar</a>
</form>
</div>
</div>
</div>
<?php require_once 'includes/footer.php'; ?>

54
agregar_colaborador.php Normal file
View File

@ -0,0 +1,54 @@
<?php
session_start();
if (!isset($_SESSION["user_id"])) {
header("Location: auth/login.php");
exit();
}
if (!isset($_SESSION['user_rol']) || $_SESSION['user_rol'] !== 'Administrador General') {
$_SESSION['error_message'] = 'No tienes permiso para acceder a esta página.';
header('Location: colaboradores.php');
exit();
}
require_once 'includes/header.php';
?>
<div class="container-fluid">
<div class="d-sm-flex justify-content-between align-items-center mb-4">
<h3 class="text-dark mb-0">Agregar Nuevo Colaborador</h3>
</div>
<div class="card shadow">
<div class="card-header py-3">
<p class="text-primary m-0 fw-bold">Datos del Colaborador</p>
</div>
<div class="card-body">
<form action="handle_agregar_colaborador.php" method="post">
<div class="mb-3">
<label for="nombre" class="form-label">Nombre Completo</label>
<input type="text" class="form-control" id="nombre" name="nombre" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Correo Electrónico</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Contraseña</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3">
<label for="rol" class="form-label">Rol</label>
<select class="form-control" id="rol" name="rol" required>
<option value="" disabled selected>Selecciona un rol</option>
<option value="Administrador General">Administrador General</option>
<option value="Encargado de Stock">Encargado de Stock</option>
</select>
</div>
<button type="submit" class="btn btn-primary">Agregar Colaborador</button>
<a href="colaboradores.php" class="btn btn-secondary">Cancelar</a>
</form>
</div>
</div>
</div>
<?php require_once 'includes/footer.php'; ?>

77
agregar_producto.php Normal file
View File

@ -0,0 +1,77 @@
<?php
require_once 'includes/header.php';
// Fetch cities for the stock inputs
$ciudades = [];
try {
$pdo = db();
$stmt = $pdo->query("SELECT id, nombre FROM ciudades ORDER BY nombre");
$ciudades = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
die("Error: No se pudieron cargar las ciudades. Por favor, contacte al administrador.");
}
?>
<div class="container-fluid">
<h3 class="text-dark mb-4">Agregar Nuevo Producto</h3>
<div class="card shadow mb-4">
<div class="card-header py-3">
<p class="text-primary m-0 fw-bold">Detalles del Producto</p>
</div>
<div class="card-body">
<form action="handle_agregar_producto.php" method="post">
<div class="mb-3">
<label class="form-label" for="nombre"><strong>Nombre del Producto</strong></label>
<input class="form-control" type="text" id="nombre" name="nombre" required>
</div>
<div class="mb-3">
<label class="form-label" for="descripcion"><strong>Descripción</strong></label>
<textarea class="form-control" id="descripcion" name="descripcion" rows="4"></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label" for="costo"><strong>Costo</strong></label>
<input class="form-control" type="number" id="costo" name="costo" step="0.01" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label" for="precio_venta"><strong>Precio de Venta</strong></label>
<input class="form-control" type="number" id="precio_venta" name="precio_venta" step="0.01" required>
</div>
</div>
</div>
<hr>
<div class="mb-3">
<h5 class="text-dark mb-3">Inventario por Ciudad</h5>
<div class="row">
<?php if (empty($ciudades)): ?>
<div class="col">
<p class="text-center">No hay ciudades registradas. <a href="#">Agrega una ciudad</a> para continuar.</p>
</div>
<?php else: ?>
<?php foreach ($ciudades as $ciudad): ?>
<div class="col-md-4 mb-3">
<label class="form-label" for="stock_ciudad_<?php echo $ciudad['id']; ?>"><strong><?php echo htmlspecialchars($ciudad['nombre']); ?></strong></label>
<input class="form-control" type="number" id="stock_ciudad_<?php echo $ciudad['id']; ?>" name="stock_ciudad[<?php echo $ciudad['id']; ?>]" value="0" min="0">
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<div class="mb-3 mt-4">
<button class="btn btn-primary btn-sm" type="submit" <?php if (empty($ciudades)) echo 'disabled'; ?>>Guardar Producto</button>
<a class="btn btn-secondary btn-sm" href="productos.php">Cancelar</a>
</div>
</form>
</div>
</div>
</div>
<?php require_once 'includes/footer.php'; ?>

321
assets/css/style.css Normal file
View File

@ -0,0 +1,321 @@
:root {
--color-principal: #1E88E5;
--color-secundario: #43A047;
--color-fondo: #F9FAFB;
--color-texto: #333;
--color-texto-claro: #FFF;
--color-borde: #E0E0E0;
--font-principal: 'Inter', 'Roboto', sans-serif;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family: var(--font-principal);
background-color: #f0f2f5; /* Un gris un poco más visible */
color: #1a1a1a; /* Un negro más intenso */
font-size: 16px;
line-height: 1.6;
}
a {
color: var(--color-principal);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.content-wrapper {
padding: 2rem;
background-color: #ffffff; /* Fondo blanco explícito */
border-radius: 8px;
}
/* --- Formularios --- */
.form-container {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
background-color: #fff;
border: 1px solid var(--color-borde);
border-radius: 8px;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
font-weight: 500;
margin-bottom: 0.5rem;
}
.form-control {
display: block;
width: 100%;
padding: 0.75rem;
font-size: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
transition: border-color 0.2s;
}
.form-control:focus {
outline: none;
border-color: var(--color-principal);
}
/* --- Botones --- */
.btn {
display: inline-block;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
text-align: center;
transition: background-color 0.2s;
}
.btn-principal {
background-color: var(--color-principal);
color: var(--color-texto-claro);
}
.btn-principal:hover {
background-color: #1a73c8;
}
.btn-secundario {
background-color: var(--color-secundario);
color: var(--color-texto-claro);
}
.btn-secundario:hover {
background-color: #388e3c;
}
/* --- Tablas --- */
.table {
width: 100%;
border-collapse: collapse;
margin-top: 2rem;
}
.table th,
.table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--color-borde);
}
.table thead th {
background-color: #f5f5f5;
font-weight: 500;
}
/* --- Alertas --- */
.alert {
padding: 1rem;
margin-bottom: 1rem;
border-radius: 4px;
}
.alert-error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
/* --- Login Page --- */
.login-container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
min-height: 100vh;
padding: 2rem;
}
/* Fix para botones de acciones en tablas */
.btn-warning, .btn-warning:hover {
color: #fff !important;
background-color: #ffc107 !important;
border-color: #ffc107 !important;
}
.btn-danger, .btn-danger:hover {
color: #fff !important;
background-color: #dc3545 !important;
border-color: #dc3545 !important;
}
.btn-warning i, .btn-danger i {
color: #fff !important;
}
/* --- Estilos del Panel de Control (Dashboard) --- */
.welcome-title {
font-size: 2.5rem;
font-weight: 700;
color: #333;
margin-bottom: 0.5rem;
}
.welcome-subtitle {
font-size: 1.2rem;
color: #666;
margin-bottom: 2rem;
}
.info-card {
background-color: #fff;
border: 1px solid var(--color-borde);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.info-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
}
.info-card .card-body {
padding: 1.5rem;
}
.info-card .card-title {
font-size: 1rem;
font-weight: 600;
color: #555;
text-transform: uppercase;
margin-bottom: 0.5rem;
}
.info-card .card-text {
font-size: 2rem;
font-weight: 700;
color: var(--color-principal);
margin: 0;
}
.icon-circle {
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
border-radius: 50%;
background-color: #e3f2fd; /* Un azul muy claro */
}
.icon-circle svg {
color: var(--color-principal);
width: 24px;
height: 24px;
}
/* Estilo para la tarjeta que es un enlace */
.card-link {
text-decoration: none;
}
.card-link .card-title {
color: #333;
font-size: 1.1rem;
font-weight: 600;
text-transform: none;
}
.card-link .card-text {
font-size: 1rem;
font-weight: 400;
color: #666;
}
.card-link:hover .card-title {
color: var(--color-principal);
}
/* --- Estilos del Menú Lateral (Sidebar) --- */
.sidebar {
min-width: 250px;
max-width: 250px;
background-color: #00754A;
color: #fff;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.sidebar-heading {
padding: 1.25rem;
font-size: 1.5rem;
font-weight: 700;
text-align: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar .nav-link {
color: #fff;
padding: 0.8rem 1.25rem; /* Un poco más de padding vertical */
display: flex;
align-items: center;
margin: 0.25rem 0.75rem; /* Margen para separar los elementos */
border-radius: 8px; /* Bordes redondeados */
transition: background-color 0.3s ease, color 0.3s ease;
}
.sidebar .nav-link i {
margin-right: 0.85rem;
width: 20px;
text-align: center;
}
.sidebar .nav-item.active .nav-link,
.sidebar .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1); /* Un fondo más sutil al pasar el ratón o si está activo */
color: #fff;
text-decoration: none;
}
/* Separar el último elemento (Cerrar Sesión) */
.sidebar .nav-item.mt-auto {
margin-top: auto;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
/* Contenido Principal */
.main-content {
flex-grow: 1;
padding: 2rem;
background-color: var(--color-fondo);
}
/* Forzar color de texto en el cuerpo de las tablas */
.table tbody td {
color: #000 !important;
}
/* Forzar color de texto en el encabezado de las tablas */
.table thead th {
color: #000 !important;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

49
auth/handle_login.php Normal file
View File

@ -0,0 +1,49 @@
<?php
session_start();
require_once __DIR__ . '/../db/config.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /auth/login.php');
exit();
}
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';
if (empty($email) || empty($password)) {
$_SESSION['login_error'] = 'Por favor, ingresa tu correo y contraseña.';
header('Location: /auth/login.php');
exit();
}
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT * FROM usuarios WHERE email = :email");
$stmt->execute(['email' => $email]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password'])) {
// Login exitoso
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
$_SESSION['user_nombre'] = $user['nombre'];
$_SESSION['user_rol'] = $user['rol'];
// Redirigir al dashboard
header('Location: /index.php');
exit();
} else {
// Error de credenciales
$_SESSION['login_error'] = 'El correo o la contraseña son incorrectos.';
header('Location: /auth/login.php');
exit();
}
} catch (PDOException $e) {
// Error de base de datos
// En un entorno de producción, esto debería ser registrado en un log.
$_SESSION['login_error'] = 'Ocurrió un error en el servidor. Inténtalo de nuevo más tarde.';
// die($e->getMessage()); // Para depuración
header('Location: /auth/login.php');
exit();
}

46
auth/login.php Normal file
View File

@ -0,0 +1,46 @@
<?php
$page_title = 'Iniciar Sesión';
// We don't want the full header here, just the HTML head part.
// So we create a simpler header for login/public pages.
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($page_title) ?></title>
<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=Inter:wght@400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/assets/css/style.css?v=<?php echo time(); ?>">
</head>
<body>
<div class="login-container">
<div class="form-container">
<h1>Iniciar Sesión</h1>
<?php
session_start();
if (isset($_SESSION['login_error'])) {
echo '<div class="alert alert-error">' . $_SESSION['login_error'] . '</div>';
unset($_SESSION['login_error']);
}
?>
<form action="/auth/handle_login.php" method="POST">
<div class="form-group">
<label for="email">Correo Electrónico</label>
<input type="email" id="email" name="email" class="form-control" required>
</div>
<div class="form-group">
<label for="password">Contraseña</label>
<input type="password" id="password" name="password" class="form-control" required>
</div>
<button type="submit" class="btn btn-principal" style="width:100%;">Ingresar</button>
</form>
</div>
</div>
</body>
</html>

22
auth/logout.php Normal file
View File

@ -0,0 +1,22 @@
<?php
session_start();
// Destruir todas las variables de sesión.
$_SESSION = [];
// Si se desea destruir la sesión completamente, borre también la cookie de sesión.
// Nota: ¡Esto destruirá la sesión, y no solo los datos de la sesión!
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
// Finalmente, destruir la sesión.
session_destroy();
// Redirigir a la página de login
header("Location: /auth/login.php");
exit();

89
ciudades.php Normal file
View File

@ -0,0 +1,89 @@
<?php
session_start();
if (!isset($_SESSION["user_id"])) {
header("Location: auth/login.php");
exit();
}
require_once 'includes/header.php';
$pdo = db();
// Fetch all cities ordered by the 'orden' column
$stmt = $pdo->prepare("SELECT * FROM ciudades ORDER BY orden ASC");
$stmt->execute();
$ciudades = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<div class="container-fluid">
<!-- Page Heading -->
<div class="d-sm-flex justify-content-between align-items-center mb-4">
<h3 class="text-dark mb-0">Gestión de Ciudades</h3>
<a class="btn btn-primary btn-sm" role="button" href="agregar_ciudad.php">
<i class="fas fa-plus fa-sm text-white-50"></i>&nbsp;Agregar Ciudad
</a>
</div>
<!-- Success/Error Messages -->
<?php if (isset($_SESSION['success_message'])): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?php echo $_SESSION['success_message']; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php unset($_SESSION['success_message']); ?>
<?php endif; ?>
<?php if (isset($_SESSION['error_message'])): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?php echo $_SESSION['error_message']; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php unset($_SESSION['error_message']); ?>
<?php endif; ?>
<div class="card shadow">
<div class="card-header py-3 d-flex justify-content-between align-items-center">
<p class="text-primary m-0 fw-bold">Listado y Orden de Ciudades</p>
</div>
<div class="card-body">
<form action="handle_orden_ciudades.php" method="post">
<div class="table-responsive">
<table class="table table-bordered" id="dataTable">
<thead>
<tr>
<th style="width: 100px;">Orden</th>
<th>Nombre</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php if (count($ciudades) > 0): ?>
<?php foreach ($ciudades as $ciudad): ?>
<tr>
<td>
<input type="number" name="orden[<?php echo $ciudad['id']; ?>]" value="<?php echo htmlspecialchars($ciudad['orden']); ?>" class="form-control form-control-sm text-center" style="width: 80px;">
</td>
<td><?php echo htmlspecialchars($ciudad['nombre']); ?></td>
<td>
<a href="editar_ciudad.php?id=<?php echo $ciudad['id']; ?>" class="btn btn-warning btn-sm"><i class="fas fa-edit"></i> Editar</a>
<a href="eliminar_ciudad.php?id=<?php echo $ciudad['id']; ?>" class="btn btn-danger btn-sm" onclick="return confirm('¿Estás seguro de que quieres eliminar esta ciudad?');"><i class="fas fa-trash"></i> Eliminar</a>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="3" class="text-center">No hay ciudades registradas.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="text-end mt-3">
<button type="submit" class="btn btn-success"><i class="fas fa-save"></i> Guardar Orden</button>
</div>
</form>
</div>
</div>
</div>
<?php require_once 'includes/footer.php'; ?>

160
colaboradores.php Normal file
View File

@ -0,0 +1,160 @@
<?php
session_start();
if (!isset($_SESSION["user_id"])) {
header("Location: auth/login.php");
exit();
}
require_once 'includes/header.php';
// Search term
$search_term = isset($_GET['search']) ? trim($_GET['search']) : '';
// Pagination settings
$items_per_page = 10;
$page = isset($_GET['page']) && is_numeric($_GET['page']) ? (int)$_GET['page'] : 1;
$offset = ($page - 1) * $items_per_page;
$pdo = db();
// Base query
$sql_count = "SELECT COUNT(*) FROM usuarios";
$sql = "SELECT id, nombre, email, rol FROM usuarios";
$params = [];
// Apply search filter
if (!empty($search_term)) {
$sql_count .= " WHERE nombre LIKE :search OR email LIKE :search";
$sql .= " WHERE nombre LIKE :search OR email LIKE :search";
$params[':search'] = '%' . $search_term . '%';
}
// Get total number of items (filtered or not)
$total_stmt = $pdo->prepare($sql_count);
$total_stmt->execute($params);
$total_items = $total_stmt->fetchColumn();
$total_pages = ceil($total_items / $items_per_page);
// Add ordering and pagination to the main query
$sql .= " ORDER BY id DESC LIMIT :limit OFFSET :offset";
$stmt = $pdo->prepare($sql);
// Bind search param if it exists
if (!empty($search_term)) {
$stmt->bindValue(':search', $params[':search'], PDO::PARAM_STR);
}
$stmt->bindValue(':limit', $items_per_page, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$usuarios = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<div class="container-fluid">
<!-- Page Heading -->
<div class="d-sm-flex justify-content-between align-items-center mb-4">
<h3 class="text-dark mb-0">Gestión de Colaboradores</h3>
<?php if (isset($_SESSION['user_rol']) && $_SESSION['user_rol'] === 'Administrador General'): ?>
<a class="btn btn-primary btn-sm" role="button" href="agregar_colaborador.php">
<i class="fas fa-plus fa-sm text-white-50"></i>&nbsp;Agregar Colaborador
</a>
<?php endif; ?>
</div>
<!-- Success/Error Messages -->
<?php if (isset($_SESSION['success_message'])): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?php echo $_SESSION['success_message']; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php unset($_SESSION['success_message']); ?>
<?php endif; ?>
<?php if (isset($_SESSION['error_message'])): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?php echo $_SESSION['error_message']; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php unset($_SESSION['error_message']); ?>
<?php endif; ?>
<!-- Search Form -->
<div class="card shadow mb-4">
<div class="card-body">
<form action="colaboradores.php" method="get" class="form-inline">
<div class="input-group">
<input type="text" name="search" class="form-control" placeholder="Buscar por nombre o email..." value="<?php echo isset($_GET['search']) ? htmlspecialchars($_GET['search']) : ''; ?>">
<button class="btn btn-primary" type="submit">Buscar</button>
</div>
</form>
</div>
</div>
<div class="card shadow">
<div class="card-header py-3">
<p class="text-primary m-0 fw-bold">Listado de Colaboradores</p>
</div>
<div class="card-body">
<div class="table-responsive table mt-2" id="dataTable" role="grid" aria-describedby="dataTable_info">
<table class="table my-0" id="dataTable">
<thead>
<tr>
<th>ID</th>
<th>Nombre</th>
<th>Email</th>
<th>Rol</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php if (count($usuarios) > 0): ?>
<?php foreach ($usuarios as $usuario): ?>
<tr>
<td><?php echo htmlspecialchars($usuario['id']); ?></td>
<td><?php echo htmlspecialchars($usuario['nombre']); ?></td>
<td><?php echo htmlspecialchars($usuario['email']); ?></td>
<td><?php echo htmlspecialchars($usuario['rol']); ?></td>
<td>
<?php if (isset($_SESSION['user_rol']) && $_SESSION['user_rol'] === 'Administrador General'): ?>
<a href="editar_colaborador.php?id=<?php echo $usuario['id']; ?>" class="btn btn-warning btn-sm"><i class="fas fa-edit"></i> Editar</a>
<a href="eliminar_colaborador.php?id=<?php echo $usuario['id']; ?>" class="btn btn-danger btn-sm" onclick="return confirm('¿Estás seguro de que quieres eliminar este colaborador?');"><i class="fas fa-trash"></i> Eliminar</a>
<?php else: ?>
<span class="text-muted">No tienes permisos</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="5" class="text-center">No hay colaboradores que coincidan con la búsqueda.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Pagination Controls -->
<div class="d-flex justify-content-center">
<nav>
<ul class="pagination">
<?php if ($page > 1): ?>
<li class="page-item"><a class="page-link" href="?page=<?php echo $page - 1; ?><?php echo isset($_GET['search']) ? '&search=' . htmlspecialchars($_GET['search']) : ''; ?>">Anterior</a></li>
<?php endif; ?>
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
<li class="page-item <?php echo ($i == $page) ? 'active' : ''; ?>">
<a class="page-link" href="?page=<?php echo $i; ?><?php echo isset($_GET['search']) ? '&search=' . htmlspecialchars($_GET['search']) : ''; ?>"><?php echo $i; ?></a>
</li>
<?php endfor; ?>
<?php if ($page < $total_pages): ?>
<li class="page-item"><a class="page-link" href="?page=<?php echo $page + 1; ?><?php echo isset($_GET['search']) ? '&search=' . htmlspecialchars($_GET['search']) : ''; ?>">Siguiente</a></li>
<?php endif; ?>
</ul>
</nav>
</div>
</div>
</div>
</div>
<?php require_once 'includes/footer.php'; ?>

105
confirmacion_stock.php Normal file
View File

@ -0,0 +1,105 @@
<?php
require_once 'includes/header.php';
require_once 'db/config.php';
$pdo = db();
// Fetch users
$stmt_usuarios = $pdo->query('SELECT id, nombre FROM usuarios ORDER BY nombre');
$usuarios = $stmt_usuarios->fetchAll(PDO::FETCH_ASSOC);
// Fetch cities
$stmt_ciudades = $pdo->query('SELECT id, nombre FROM ciudades ORDER BY nombre');
$ciudades = $stmt_ciudades->fetchAll(PDO::FETCH_ASSOC);
// Fetch confirmations
$sql = "
SELECT
sc.fecha_hora,
sc.fecha_actualizacion,
u.nombre AS usuario_nombre,
GROUP_CONCAT(c.nombre SEPARATOR ', ') AS ciudades
FROM stock_confirmaciones sc
JOIN usuarios u ON sc.usuario_id = u.id
LEFT JOIN stock_confirmacion_ciudades scc ON sc.id = scc.confirmacion_id
LEFT JOIN ciudades c ON scc.ciudad_id = c.id
GROUP BY sc.id
ORDER BY sc.fecha_actualizacion DESC, sc.fecha_hora DESC
";
$stmt_confirmaciones = $pdo->prepare($sql);
$stmt_confirmaciones->execute();
$confirmaciones = $stmt_confirmaciones->fetchAll(PDO::FETCH_ASSOC);
?>
<div class="container mt-4">
<h1>Reporte de Entregas</h1>
<p>Seleccione su usuario, la fecha de entrega, las ciudades y presione confirmar para registrar la entrega.</p>
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">Registrar Entrega</h5>
<form action="handle_confirmacion_stock.php" method="POST">
<div class="form-group">
<label for="usuario_id">Usuario</label>
<select class="form-control" id="usuario_id" name="usuario_id" required>
<option value="">Seleccione un usuario</option>
<?php foreach ($usuarios as $usuario) : ?>
<option value="<?php echo htmlspecialchars($usuario['id']); ?>"><?php echo htmlspecialchars($usuario['nombre']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group mt-3">
<label for="fecha_actualizacion">Fecha de Entrega</label>
<input type="date" class="form-control" id="fecha_actualizacion" name="fecha_actualizacion" value="<?php echo date('Y-m-d'); ?>" required>
</div>
<div class="form-group mt-3">
<label for="ciudad_id">Ciudad</label>
<select class="form-control" id="ciudad_id" name="ciudad_id" required>
<option value="">Seleccione una ciudad</option>
<?php foreach ($ciudades as $ciudad) : ?>
<option value="<?php echo htmlspecialchars($ciudad['id']); ?>"><?php echo htmlspecialchars($ciudad['nombre']); ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary mt-3">Confirmar</button>
</form>
</div>
</div>
<div class="card">
<div class="card-body">
<h5 class="card-title">Historial de Entregas</h5>
<table class="table table-striped">
<thead>
<tr>
<th>Fecha de Entrega</th>
<th>Fecha y Hora de Registro</th>
<th>Usuario</th>
<th>Ciudades</th>
</tr>
</thead>
<tbody>
<?php if (empty($confirmaciones)) : ?>
<tr>
<td colspan="4" class="text-center">No hay entregas registradas.</td>
</tr>
<?php else : ?>
<?php foreach ($confirmaciones as $confirmacion) : ?>
<tr>
<td><?php echo htmlspecialchars($confirmacion['fecha_actualizacion'] ? date('d/m/Y', strtotime($confirmacion['fecha_actualizacion'])) : 'No especificada'); ?></td>
<td><?php echo htmlspecialchars(date('d/m/Y H:i:s', strtotime($confirmacion['fecha_hora']))); ?></td>
<td><?php echo htmlspecialchars($confirmacion['usuario_nombre']); ?></td>
<td><?php echo htmlspecialchars($confirmacion['ciudades'] ?? 'N/A'); ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php
require_once 'includes/footer.php';
?>

View File

@ -5,6 +5,11 @@ define('DB_NAME', 'app_30953');
define('DB_USER', 'app_30953'); define('DB_USER', 'app_30953');
define('DB_PASS', 'e45f2778-db1f-450c-99c6-29efb4601472'); define('DB_PASS', 'e45f2778-db1f-450c-99c6-29efb4601472');
// Iniciar la sesión si no está activa
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
function db() { function db() {
static $pdo; static $pdo;
if (!$pdo) { if (!$pdo) {
@ -12,6 +17,31 @@ function db() {
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]); ]);
$pdo->exec("SET time_zone = '-05:00'");
} }
return $pdo; return $pdo;
} }
/**
* Comprueba si el usuario ha iniciado sesión.
* @return bool
*/
function is_logged_in() {
return isset($_SESSION['user_id']);
}
/**
* Devuelve el ID del usuario que ha iniciado sesión.
* @return int|null
*/
function get_current_user_id() {
return $_SESSION['user_id'] ?? null;
}
/**
* Devuelve el nombre de usuario del usuario que ha iniciado sesión.
* @return string|null
*/
function get_current_user_name() {
return $_SESSION['user_name'] ?? null;
}

424
db/setup.php Normal file
View File

@ -0,0 +1,424 @@
<?php
require_once __DIR__ . '/config.php';
try {
$pdo = db();
echo "Conexión a la base de datos exitosa.\n";
$sql_statements = [
"CREATE TABLE IF NOT EXISTS `usuarios` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`nombre` VARCHAR(255) NOT NULL,
`email` VARCHAR(255) NOT NULL UNIQUE,
`password` VARCHAR(255) NOT NULL,
`rol` ENUM('Administrador General', 'Encargado de Stock') NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
"CREATE TABLE IF NOT EXISTS `productos` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`sku` VARCHAR(100) NOT NULL UNIQUE,
`nombre` VARCHAR(255) NOT NULL,
`categoria` VARCHAR(100),
`unidad` VARCHAR(50),
`precio_venta` DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
`costo` DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
`codigo_barras` VARCHAR(255),
`activo` BOOLEAN NOT NULL DEFAULT TRUE,
`descripcion` TEXT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
"CREATE TABLE IF NOT EXISTS `ciudades` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`nombre` VARCHAR(100) NOT NULL UNIQUE,
`codigo` VARCHAR(20)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
"CREATE TABLE IF NOT EXISTS `stock_por_ciudad` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`producto_id` INT NOT NULL,
`ciudad_id` INT NOT NULL,
`stock_actual` INT NOT NULL DEFAULT 0,
`stock_minimo` INT NOT NULL DEFAULT 0,
`stock_reservado` INT NOT NULL DEFAULT 0,
FOREIGN KEY (`producto_id`) REFERENCES `productos`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`ciudad_id`) REFERENCES `ciudades`(`id`) ON DELETE CASCADE,
UNIQUE KEY `producto_ciudad` (`producto_id`, `ciudad_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
"CREATE TABLE IF NOT EXISTS `movimientos` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`tipo` ENUM('Entrada', 'Salida', 'Transferencia', 'Reverso de Salida', 'Reverso de Entrada') NOT NULL,
`producto_id` INT NOT NULL,
`ciudad_origen_id` INT,
`ciudad_destino_id` INT,
`cantidad` INT NOT NULL,
`fecha` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`usuario_id` INT NOT NULL,
`documento_referencia` VARCHAR(255),
`observacion` TEXT,
FOREIGN KEY (`producto_id`) REFERENCES `productos`(`id`),
FOREIGN KEY (`ciudad_origen_id`) REFERENCES `ciudades`(`id`),
FOREIGN KEY (`ciudad_destino_id`) REFERENCES `ciudades`(`id`),
FOREIGN KEY (`usuario_id`) REFERENCES `usuarios`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
"CREATE TABLE IF NOT EXISTS `ventas` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`nro_doc` VARCHAR(50) UNIQUE,
`fecha` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`cliente` VARCHAR(255),
`ciudad_id` INT NOT NULL,
`vendedor_id` INT NOT NULL,
`estado` ENUM('Borrador', 'Confirmado', 'Anulado') NOT NULL DEFAULT 'Borrador',
`total` DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
FOREIGN KEY (`ciudad_id`) REFERENCES `ciudades`(`id`),
FOREIGN KEY (`vendedor_id`) REFERENCES `usuarios`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
"CREATE TABLE IF NOT EXISTS `ventas_items` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`venta_id` INT NOT NULL,
`producto_id` INT NOT NULL,
`cantidad` INT NOT NULL,
`precio` DECIMAL(10, 2) NOT NULL,
FOREIGN KEY (`venta_id`) REFERENCES `ventas`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`producto_id`) REFERENCES `productos`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
"CREATE TABLE IF NOT EXISTS `compras` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`nro_doc` VARCHAR(50) UNIQUE,
`fecha` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`proveedor` VARCHAR(255),
`ciudad_id` INT NOT NULL,
`usuario_id` INT NOT NULL,
`estado` ENUM('Borrador', 'Confirmado', 'Anulado') NOT NULL DEFAULT 'Borrador',
`total` DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
FOREIGN KEY (`ciudad_id`) REFERENCES `ciudades`(`id`),
FOREIGN KEY (`usuario_id`) REFERENCES `usuarios`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
"CREATE TABLE IF NOT EXISTS `compras_items` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`compra_id` INT NOT NULL,
`producto_id` INT NOT NULL,
`cantidad` INT NOT NULL,
`costo` DECIMAL(10, 2) NOT NULL,
FOREIGN KEY (`compra_id`) REFERENCES `compras`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`producto_id`) REFERENCES `productos`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
"CREATE TABLE IF NOT EXISTS `bitacora` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`fecha_hora` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`usuario_ejecutor` INT NOT NULL,
`tipo_operacion` VARCHAR(100) NOT NULL,
`entidad_origen` VARCHAR(100),
`id_referencia_origen` INT,
`ciudad_origen` INT,
`ciudad_destino` INT,
`before_after` JSON,
`observacion` TEXT,
FOREIGN KEY (`usuario_ejecutor`) REFERENCES `usuarios`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
"CREATE TABLE IF NOT EXISTS `stock_confirmaciones` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`usuario_id` INT NOT NULL,
`fecha_hora` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`usuario_id`) REFERENCES `usuarios`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
"CREATE TABLE IF NOT EXISTS `stock_confirmacion_ciudades` (
`confirmacion_id` INT NOT NULL,
`ciudad_id` INT NOT NULL,
PRIMARY KEY (`confirmacion_id`, `ciudad_id`),
FOREIGN KEY (`confirmacion_id`) REFERENCES `stock_confirmaciones`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`ciudad_id`) REFERENCES `ciudades`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
"CREATE TABLE IF NOT EXISTS `liquidaciones` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`fecha` DATE NOT NULL,
`monto` DECIMAL(10, 2) NOT NULL,
`ciudad_id` INT,
FOREIGN KEY (`ciudad_id`) REFERENCES `ciudades`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
"CREATE TABLE IF NOT EXISTS `inversiones_operativas` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`mercaderia` VARCHAR(255) NOT NULL,
`fecha` DATE NOT NULL,
`cantidad` INT NOT NULL,
`precio` DECIMAL(10, 2) NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
"CREATE TABLE IF NOT EXISTS `liquidaciones_ciudades_estados` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`liquidacion_id` INT NOT NULL,
`ciudad_id` INT NOT NULL,
`fecha` DATE NOT NULL,
`estado` ENUM('Pendiente', 'Pagado') NOT NULL DEFAULT 'Pendiente',
FOREIGN KEY (`liquidacion_id`) REFERENCES `liquidaciones`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`ciudad_id`) REFERENCES `ciudades`(`id`) ON DELETE CASCADE,
UNIQUE KEY `liquidacion_ciudad_fecha` (`liquidacion_id`, `ciudad_id`, `fecha`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
"CREATE TABLE IF NOT EXISTS `flujo_de_caja` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`fecha` DATE NOT NULL,
`bcp_yape` DECIMAL(10, 2) DEFAULT 0.00,
`banco_nacion` DECIMAL(10, 2) DEFAULT 0.00,
`interbank` DECIMAL(10, 2) DEFAULT 0.00,
`bbva` DECIMAL(10, 2) DEFAULT 0.00,
`otros_ingresos` DECIMAL(10, 2) DEFAULT 0.00,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
"CREATE TABLE IF NOT EXISTS `kanban_columns` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`nombre` VARCHAR(255) NOT NULL,
`orden` INT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
"CREATE TABLE IF NOT EXISTS `info_productos` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`producto_id` INT NOT NULL,
`imagen_url` VARCHAR(255) NOT NULL,
`texto_informativo` TEXT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`column_id` INT,
`orden` INT NOT NULL DEFAULT 0,
FOREIGN KEY (`producto_id`) REFERENCES `productos`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`column_id`) REFERENCES `kanban_columns`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"
];
foreach ($sql_statements as $sql) {
$pdo->exec($sql);
echo "Ejecutado: " . substr($sql, 0, 50) . "...\n";
}
echo "Todas las tablas fueron creadas exitosamente.\n";
// Add column_id to info_productos if it doesn't exist
$stmt = $pdo->prepare("SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'info_productos' AND column_name = 'column_id'");
$stmt->execute();
if ($stmt->fetchColumn() == 0) {
$pdo->exec("ALTER TABLE `info_productos` ADD `column_id` INT NULL DEFAULT NULL AFTER `texto_informativo`;");
$pdo->exec("ALTER TABLE `info_productos` ADD FOREIGN KEY (`column_id`) REFERENCES `kanban_columns`(`id`) ON DELETE SET NULL;");
echo "Columna 'column_id' añadida a la tabla 'info_productos'.\n";
} else {
echo "La columna 'column_id' ya existe en la tabla 'info_productos'.\n";
}
// Add orden to info_productos if it doesn't exist
$stmt = $pdo->prepare("SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'info_productos' AND column_name = 'orden'");
$stmt->execute();
if ($stmt->fetchColumn() == 0) {
$pdo->exec("ALTER TABLE `info_productos` ADD `orden` INT NOT NULL DEFAULT 0;");
echo "Columna 'orden' añadida a la tabla 'info_productos'.\n";
} else {
echo "La columna 'orden' ya existe en la tabla 'info_productos'.\n";
}
// Add fecha_actualizacion to stock_confirmaciones if it doesn't exist
$stmt = $pdo->prepare("SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'stock_confirmaciones' AND column_name = 'fecha_actualizacion'");
$stmt->execute();
if ($stmt->fetchColumn() == 0) {
$pdo->exec("ALTER TABLE `stock_confirmaciones` ADD `fecha_actualizacion` DATE DEFAULT NULL AFTER `fecha_hora`");
echo "Columna 'fecha_actualizacion' añadida a la tabla 'stock_confirmaciones'.\n";
} else {
echo "La columna 'fecha_actualizacion' ya existe en la tabla 'stock_confirmaciones'.\n";
}
// Add precio_liquidacion column to movimientos table if it doesn't exist
$stmt = $pdo->prepare("SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'movimientos' AND column_name = 'precio_liquidacion'");
$stmt->execute();
if ($stmt->fetchColumn() == 0) {
$pdo->exec("ALTER TABLE `movimientos` ADD `precio_liquidacion` DECIMAL(10, 2) DEFAULT NULL;");
echo "Columna 'precio_liquidacion' añadida a la tabla 'movimientos'.\n";
} else {
echo "La columna 'precio_liquidacion' ya existe en la tabla 'movimientos'.\n";
}
// Add cantidad_pedidos column to movimientos table if it doesn't exist
$stmt = $pdo->prepare("SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'movimientos' AND column_name = 'cantidad_pedidos'");
$stmt->execute();
if ($stmt->fetchColumn() == 0) {
$pdo->exec("ALTER TABLE `movimientos` ADD `cantidad_pedidos` INT DEFAULT NULL;");
echo "Columna 'cantidad_pedidos' añadida a la tabla 'movimientos'.\n";
} else {
echo "La columna 'cantidad_pedidos' ya existe en la tabla 'movimientos'.\n";
}
// Insertar ciudades iniciales
$ciudades = [
'Lima', 'Arequipa', 'Piura', 'Cusco', 'Trujillo',
'Chiclayo', 'Iquitos', 'Tacna', 'Puno', 'Huancayo'
];
$stmt = $pdo->prepare("INSERT IGNORE INTO `ciudades` (`nombre`) VALUES (:nombre)");
echo "Insertando ciudades...\n";
foreach ($ciudades as $ciudad) {
$stmt->execute(['nombre' => $ciudad]);
}
echo "Ciudades insertadas.\n";
// Añadir columna de orden si no existe
$stmt = $pdo->prepare("SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'ciudades' AND column_name = 'orden'");
$stmt->execute();
if ($stmt->fetchColumn() == 0) {
$pdo->exec("ALTER TABLE `ciudades` ADD `orden` INT NOT NULL DEFAULT 0;");
echo "Columna 'orden' añadida a la tabla 'ciudades'.\n";
// Asignar un orden inicial a las ciudades existentes
$pdo->exec("SET @i = 0; UPDATE `ciudades` SET `orden` = (@i:=@i+1) ORDER BY `nombre`;");
echo "Orden inicial asignado a las ciudades existentes.\n";
} else {
echo "La columna 'orden' ya existe en la tabla 'ciudades'.\n";
}
// Añadir columna de orden a productos si no existe
$stmt = $pdo->prepare("SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'productos' AND column_name = 'orden'");
$stmt->execute();
if ($stmt->fetchColumn() == 0) {
$pdo->exec("ALTER TABLE `productos` ADD `orden` INT NOT NULL DEFAULT 0;");
echo "Columna 'orden' añadida a la tabla 'productos'.\n";
// Asignar un orden inicial a los productos existentes
$pdo->exec("SET @i = 0; UPDATE `productos` SET `orden` = (@i:=@i+1) ORDER BY `nombre`;");
echo "Orden inicial asignado a los productos existentes.\n";
} else {
echo "La columna 'orden' ya existe en la tabla 'productos'.\n";
}
// --- Migration for liquidaciones_ciudades_estados ---
echo "Verificando estructura de 'liquidaciones_ciudades_estados'...\n";
// Ensure 'fecha' column exists
$stmt_col = $pdo->prepare("SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'liquidaciones_ciudades_estados' AND column_name = 'fecha'");
$stmt_col->execute();
if ($stmt_col->fetchColumn() == 0) {
// This block might run on a partially migrated DB
echo "Añadiendo columna 'fecha'...\n";
$pdo->exec("ALTER TABLE `liquidaciones_ciudades_estados` ADD `fecha` DATE NOT NULL AFTER `ciudad_id`;");
// Drop old key if it exists
$stmt_key = $pdo->prepare("SHOW KEYS FROM `liquidaciones_ciudades_estados` WHERE Key_name = 'liquidacion_ciudad'");
$stmt_key->execute();
if ($stmt_key->fetch()) {
$pdo->exec("ALTER TABLE `liquidaciones_ciudades_estados` DROP KEY `liquidacion_ciudad`;");
}
// Add new unique key
$pdo->exec("ALTER TABLE `liquidaciones_ciudades_estados` ADD UNIQUE KEY `liquidacion_ciudad_fecha` (`liquidacion_id`, `ciudad_id`, `fecha`);");
}
// Ensure foreign key to 'liquidaciones' exists
$stmt_fk = $pdo->prepare("SELECT COUNT(*) FROM information_schema.key_column_usage WHERE table_schema = DATABASE() AND table_name = 'liquidaciones_ciudades_estados' AND column_name = 'liquidacion_id' AND referenced_table_name = 'liquidaciones';");
$stmt_fk->execute();
if ($stmt_fk->fetchColumn() == 0) {
echo "Añadiendo clave foránea a 'liquidaciones'...\n";
// Truncate before adding FK to prevent integrity errors from old data
$pdo->exec("TRUNCATE TABLE `liquidaciones_ciudades_estados`;");
$pdo->exec("ALTER TABLE `liquidaciones_ciudades_estados` ADD FOREIGN KEY (`liquidacion_id`) REFERENCES `liquidaciones`(`id`) ON DELETE CASCADE;");
echo "Clave foránea añadida.\n";
}
echo "Verificación de 'liquidaciones_ciudades_estados' completada.\n";
// Find and drop the problematic foreign key if it exists
$stmt = $pdo->prepare("SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'liquidaciones_ciudades_estados' AND COLUMN_NAME = 'liquidacion_id' AND REFERENCED_TABLE_NAME = 'liquidaciones';");
$stmt->execute();
$constraint_name = $stmt->fetchColumn();
if ($constraint_name) {
$pdo->exec("ALTER TABLE `liquidaciones_ciudades_estados` DROP FOREIGN KEY `{$constraint_name}`;");
echo "Clave foránea '{$constraint_name}' eliminada de la tabla 'liquidaciones_ciudades_estados'.\n";
} else {
echo "La clave foránea de liquidacion_id en liquidaciones_ciudades_estados a liquidaciones no existe o ya fue eliminada.\n";
}
// Crear usuario administrador por defecto
$admin_email = 'admin@example.com';
$admin_pass = 'admin123';
$hashed_pass = password_hash($admin_pass, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("SELECT id FROM `usuarios` WHERE `email` = :email");
$stmt->execute(['email' => $admin_email]);
if ($stmt->fetchColumn()) {
echo "El usuario administrador ya existe.\n";
} else {
$stmt = $pdo->prepare("INSERT INTO `usuarios` (`nombre`, `email`, `password`, `rol`) VALUES (:nombre, :email, :password, :rol)");
$stmt->execute([
'nombre' => 'Administrador',
'email' => $admin_email,
'password' => $hashed_pass,
'rol' => 'Administrador General'
]);
echo "Usuario administrador creado exitosamente.\n";
echo "Email: " . $admin_email . "\n";
echo "Contraseña: " . $admin_pass . "\n";
}
// Rename 'otros' to 'otros_ingresos' in 'flujo_de_caja' if necessary
$stmt_check_otros = $pdo->prepare("SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'flujo_de_caja' AND column_name = 'otros'");
$stmt_check_otros->execute();
$otros_exists = $stmt_check_otros->fetchColumn() > 0;
$stmt_check_otros_ingresos = $pdo->prepare("SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'flujo_de_caja' AND column_name = 'otros_ingresos'");
$stmt_check_otros_ingresos->execute();
$otros_ingresos_exists = $stmt_check_otros_ingresos->fetchColumn() > 0;
if ($otros_exists && !$otros_ingresos_exists) {
$pdo->exec("ALTER TABLE `flujo_de_caja` CHANGE `otros` `otros_ingresos` DECIMAL(10, 2) DEFAULT 0.00;");
echo "Columna 'otros' renombrada a 'otros_ingresos' en la tabla 'flujo_de_caja'.\n";
} else {
echo "La columna 'otros_ingresos' ya existe o la columna 'otros' no fue encontrada en 'flujo_de_caja'.\n";
}
// Add new columns to flujo_de_caja if they don't exist
$new_columns = [
'fl_1' => 'DECIMAL(10, 2) DEFAULT 0.00',
'tu1' => 'DECIMAL(10, 2) DEFAULT 0.00',
'tu2' => 'DECIMAL(10, 2) DEFAULT 0.00',
'tu3' => 'DECIMAL(10, 2) DEFAULT 0.00',
'fl_2' => 'DECIMAL(10, 2) DEFAULT 0.00',
'fl_3' => 'DECIMAL(10, 2) DEFAULT 0.00',
'rc_envio' => 'DECIMAL(10, 2) NOT NULL DEFAULT 0.00'
];
foreach ($new_columns as $column_name => $column_definition) {
$stmt = $pdo->prepare("SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'flujo_de_caja' AND column_name = :column_name");
$stmt->execute(['column_name' => $column_name]);
if ($stmt->fetchColumn() == 0) {
$pdo->exec("ALTER TABLE `flujo_de_caja` ADD `{$column_name}` {$column_definition}");
echo "Columna '{$column_name}' añadida a la tabla 'flujo_de_caja'.\n";
} else {
echo "La columna '{$column_name}' ya existe en la tabla 'flujo_de_caja'.\n";
}
}
// --- Migration for inversiones_operativas ---
echo "Verificando estructura de 'inversiones_operativas'...";
$stmt_check_desc = $pdo->prepare("SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'inversiones_operativas' AND column_name = 'descripcion'");
$stmt_check_desc->execute();
if ($stmt_check_desc->fetchColumn() > 0) {
echo "Migrando tabla 'inversiones_operativas'. Cambiando columnas...";
$pdo->exec("ALTER TABLE `inversiones_operativas` DROP COLUMN `descripcion`;");
$pdo->exec("ALTER TABLE `inversiones_operativas` DROP COLUMN `monto`;");
$pdo->exec("ALTER TABLE `inversiones_operativas` ADD `mercaderia` VARCHAR(255) NOT NULL AFTER `id`;");
$pdo->exec("ALTER TABLE `inversiones_operativas` ADD `cantidad` INT NOT NULL AFTER `fecha`;");
$pdo->exec("ALTER TABLE `inversiones_operativas` ADD `precio` DECIMAL(10, 2) NOT NULL AFTER `cantidad`;");
echo "Tabla 'inversiones_operativas' actualizada a la nueva estructura.";
} else {
echo "La tabla 'inversiones_operativas' ya tiene la nueva estructura.";
}
} catch (PDOException $e) {
die("Error en la base de datos: " . $e->getMessage());
}

70
editar_ciudad.php Normal file
View File

@ -0,0 +1,70 @@
<?php
session_start();
if (!isset($_SESSION["user_id"])) {
header("Location: auth/login.php");
exit();
}
require_once 'includes/header.php';
require_once 'db/config.php';
$ciudad_id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($ciudad_id === 0) {
$_SESSION['error_message'] = "ID de ciudad no válido.";
header("Location: ciudades.php");
exit();
}
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT * FROM ciudades WHERE id = :id");
$stmt->bindParam(':id', $ciudad_id, PDO::PARAM_INT);
$stmt->execute();
$ciudad = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$ciudad) {
$_SESSION['error_message'] = "Ciudad no encontrada.";
header("Location: ciudades.php");
exit();
}
} catch (PDOException $e) {
$_SESSION['error_message'] = "Error de base de datos: " . $e->getMessage();
header("Location: ciudades.php");
exit();
}
?>
<div class="container-fluid">
<div class="d-sm-flex justify-content-between align-items-center mb-4">
<h3 class="text-dark mb-0">Editar Ciudad</h3>
</div>
<div class="card shadow">
<div class="card-header py-3">
<p class="text-primary m-0 fw-bold">Datos de la Ciudad</p>
</div>
<div class="card-body">
<form action="handle_editar_ciudad.php" method="post">
<input type="hidden" name="id" value="<?php echo htmlspecialchars($ciudad['id']); ?>">
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label for="nombre" class="form-label">Nombre de la Ciudad</label>
<input type="text" class="form-control" id="nombre" name="nombre" value="<?php echo htmlspecialchars($ciudad['nombre']); ?>" required>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="orden" class="form-label">Orden</label>
<input type="number" class="form-control" id="orden" name="orden" value="<?php echo htmlspecialchars($ciudad['orden']); ?>" required>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Actualizar Ciudad</button>
<a href="ciudades.php" class="btn btn-secondary">Cancelar</a>
</form>
</div>
</div>
</div>
<?php require_once 'includes/footer.php'; ?>

94
editar_colaborador.php Normal file
View File

@ -0,0 +1,94 @@
<?php
session_start();
if (!isset($_SESSION["user_id"])) {
header("Location: auth/login.php");
exit();
}
if (!isset($_SESSION['user_rol']) || $_SESSION['user_rol'] !== 'Administrador General') {
$_SESSION['error_message'] = 'No tienes permiso para acceder a esta página.';
header('Location: colaboradores.php');
exit();
}
require_once 'db/config.php';
require_once 'includes/header.php';
// Check for user ID
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
$_SESSION['error_message'] = "ID de colaborador no válido.";
header("Location: colaboradores.php");
exit();
}
$user_id = $_GET['id'];
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT id, nombre, email, rol FROM usuarios WHERE id = :id");
$stmt->execute([':id' => $user_id]);
$usuario = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$usuario) {
$_SESSION['error_message'] = "Colaborador no encontrado.";
header("Location: colaboradores.php");
exit();
}
} catch (PDOException $e) {
error_log("Error al buscar colaborador: " . $e->getMessage());
$_SESSION['error_message'] = "Error al conectar con la base de datos.";
header("Location: colaboradores.php");
exit();
}
$roles = ['Administrador General', 'Encargado de Stock'];
?>
<div class="container-fluid">
<div class="d-sm-flex justify-content-between align-items-center mb-4">
<h3 class="text-dark mb-0">Editar Colaborador</h3>
</div>
<div class="card shadow">
<div class="card-header py-3">
<p class="text-primary m-0 fw-bold">Datos del Colaborador</p>
</div>
<div class="card-body">
<form action="handle_editar_colaborador.php" method="post">
<input type="hidden" name="id" value="<?php echo htmlspecialchars($usuario['id']); ?>">
<div class="mb-3">
<label for="nombre" class="form-label">Nombre Completo</label>
<input type="text" class="form-control" id="nombre" name="nombre" value="<?php echo htmlspecialchars($usuario['nombre']); ?>" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Correo Electrónico</label>
<input type="email" class="form-control" id="email" name="email" value="<?php echo htmlspecialchars($usuario['email']); ?>" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Nueva Contraseña (opcional)</label>
<input type="password" class="form-control" id="password" name="password">
<small class="form-text text-muted">Deja este campo en blanco para no cambiar la contraseña.</small>
</div>
<div class="mb-3">
<label for="rol" class="form-label">Rol</label>
<select class="form-control" id="rol" name="rol" required>
<option value="" disabled>Selecciona un rol</option>
<?php foreach ($roles as $rol): ?>
<option value="<?php echo $rol; ?>" <?php echo ($usuario['rol'] == $rol) ? 'selected' : ''; ?>><?php echo $rol; ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary">Actualizar Colaborador</button>
<a href="colaboradores.php" class="btn btn-secondary">Cancelar</a>
</form>
</div>
</div>
</div>
<?php require_once 'includes/footer.php'; ?>

68
editar_flujo_de_caja.php Normal file
View File

@ -0,0 +1,68 @@
<?php
require_once 'includes/header.php';
require_once 'db/config.php';
if (!isset($_GET['id'])) {
header('Location: flujo_de_caja.php');
exit;
}
$id = $_GET['id'];
$pdo = db();
$stmt = $pdo->prepare('SELECT * FROM flujo_de_caja WHERE id = ?');
$stmt->execute([$id]);
$movimiento = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$movimiento) {
header('Location: flujo_de_caja.php');
exit;
}
?>
<div class="container-fluid">
<h1 class="h3 mb-4 text-gray-800">Editar Movimiento de Caja</h1>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Editor de Movimiento</h6>
</div>
<div class="card-body">
<form action="handle_editar_flujo_de_caja.php" method="POST">
<input type="hidden" name="id" value="<?php echo htmlspecialchars($movimiento['id']); ?>">
<div class="row">
<div class="col-md-3 form-group">
<label for="fecha">Fecha</label>
<input type="date" name="fecha" class="form-control" value="<?php echo htmlspecialchars($movimiento['fecha']); ?>" required>
</div>
<div class="col-md-3 form-group">
<label for="bcp_yape">BCP/YAPE</label>
<input type="number" step="0.01" name="bcp_yape" class="form-control" value="<?php echo htmlspecialchars($movimiento['bcp_yape']); ?>">
</div>
<div class="col-md-3 form-group">
<label for="banco_nacion">Banco Nación</label>
<input type="number" step="0.01" name="banco_nacion" class="form-control" value="<?php echo htmlspecialchars($movimiento['banco_nacion']); ?>">
</div>
<div class="col-md-3 form-group">
<label for="interbank">Interbank</label>
<input type="number" step="0.01" name="interbank" class="form-control" value="<?php echo htmlspecialchars($movimiento['interbank']); ?>">
</div>
</div>
<div class="row">
<div class="col-md-3 form-group">
<label for="bbva">BBVA</label>
<input type="number" step="0.01" name="bbva" class="form-control" value="<?php echo htmlspecialchars($movimiento['bbva']); ?>">
</div>
<div class="col-md-3 form-group">
<label for="otros">Otros</label>
<input type="number" step="0.01" name="otros" class="form-control" value="<?php echo htmlspecialchars($movimiento['otros']); ?>">
</div>
</div>
<button type="submit" class="btn btn-primary">Actualizar Movimiento</button>
<a href="flujo_de_caja.php" class="btn btn-secondary">Cancelar</a>
</form>
</div>
</div>
</div>
<?php require_once 'includes/footer.php'; ?>

73
editar_inversion.php Normal file
View File

@ -0,0 +1,73 @@
<?php
require_once 'includes/header.php';
require_once 'db/config.php';
// Verificar permisos
if (!isset($_SESSION['user_id'])) {
header('Location: /auth/login.php');
exit;
}
$id = $_GET['id'] ?? null;
if (!$id) {
header('Location: inversiones_operativas.php?error=missing_id');
exit;
}
// Obtener los datos de la inversión
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT * FROM inversiones WHERE id = ?");
$stmt->execute([$id]);
$inversion = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$inversion) {
header('Location: inversiones_operativas.php?error=not_found');
exit;
}
} catch (PDOException $e) {
die("Error al obtener los datos: " . $e->getMessage());
}
?>
<div class="container mt-5">
<h2>Editar Inversión</h2>
<hr>
<div class="card">
<div class="card-body">
<form action="handle_editar_inversion.php" method="POST">
<input type="hidden" name="id" value="<?= htmlspecialchars($inversion['id']) ?>">
<div class="mb-3">
<label for="fecha" class="form-label">Fecha</label>
<input type="date" class="form-control" id="fecha" name="fecha" value="<?= htmlspecialchars($inversion['fecha']) ?>" required>
</div>
<div class="mb-3">
<label for="descripcion" class="form-label">Descripción</label>
<input type="text" class="form-control" id="descripcion" name="descripcion" value="<?= htmlspecialchars($inversion['descripcion']) ?>" required>
</div>
<div class="mb-3">
<label for="monto" class="form-label">Monto (S/)</label>
<input type="number" step="0.01" class="form-control" id="monto" name="monto" value="<?= htmlspecialchars($inversion['monto']) ?>" required>
</div>
<div class="mb-3">
<label for="tipo" class="form-label">Tipo</label>
<select class="form-select" id="tipo" name="tipo" required>
<option value="operativa" <?= $inversion['tipo'] === 'operativa' ? 'selected' : '' ?>>Inversión Operativa</option>
<option value="operacional" <?= $inversion['tipo'] === 'operacional' ? 'selected' : '' ?>>Inversión Operacional</option>
<option value="ads" <?= $inversion['tipo'] === 'ads' ? 'selected' : '' ?>>Inversión en Ads</option>
</select>
</div>
<button type="submit" class="btn btn-primary">Guardar Cambios</button>
<a href="inversiones_operativas.php" class="btn btn-secondary">Cancelar</a>
</form>
</div>
</div>
</div>
<?php require_once 'includes/footer.php'; ?>

96
editar_liquidacion.php Normal file
View File

@ -0,0 +1,96 @@
<?php
require_once 'includes/header.php';
// Verificar si se proporcionó un ID
if (!isset($_GET['id']) || empty($_GET['id'])) {
header('Location: liquidaciones.php?error=invalid_id');
exit();
}
$movimiento_id = $_GET['id'];
$pdo = db();
// Obtener los datos de la liquidación actual
$stmt = $pdo->prepare("SELECT * FROM movimientos WHERE id = ? AND tipo = 'Salida'");
$stmt->execute([$movimiento_id]);
$liquidacion = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$liquidacion) {
header('Location: liquidaciones.php?error=not_found');
exit();
}
// Obtener productos y ciudades para los menús desplegables
$productos = $pdo->query("SELECT id, nombre FROM productos ORDER BY nombre")->fetchAll(PDO::FETCH_ASSOC);
$ciudades = $pdo->query("SELECT id, nombre FROM ciudades ORDER BY nombre")->fetchAll(PDO::FETCH_ASSOC);
?>
<h1 class="mb-4">Editar Liquidación</h1>
<?php
if (isset($_GET['error'])) {
$error_msg = 'Ocurrió un error.';
if ($_GET['error'] === 'missing_fields') {
$error_msg = 'Por favor, complete todos los campos requeridos.';
}
echo '<div class="alert alert-danger">' . $error_msg . '</div>';
}
?>
<div class="card">
<div class="card-body">
<form action="handle_editar_liquidacion.php" method="POST">
<input type="hidden" name="movimiento_id" value="<?php echo htmlspecialchars($liquidacion['id']); ?>">
<div class="mb-3">
<label for="producto_id" class="form-label">Producto</label>
<select class="form-control" id="producto_id" name="producto_id" required>
<option value="">Seleccione un producto</option>
<?php foreach ($productos as $producto): ?>
<option value="<?php echo $producto['id']; ?>" <?php echo ($producto['id'] == $liquidacion['producto_id']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($producto['nombre']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="ciudad_id" class="form-label">Ciudad</label>
<select class="form-control" id="ciudad_id" name="ciudad_id" required>
<option value="">Seleccione una ciudad</option>
<?php foreach ($ciudades as $ciudad): ?>
<option value="<?php echo $ciudad['id']; ?>" <?php echo ($ciudad['id'] == $liquidacion['ciudad_origen_id']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($ciudad['nombre']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="cantidad" class="form-label">Cantidad</label>
<input type="number" class="form-control" id="cantidad" name="cantidad" value="<?php echo htmlspecialchars($liquidacion['cantidad']); ?>" required>
</div>
<div class="mb-3">
<label for="cantidad_pedidos" class="form-label">Cantidad de Pedidos</label>
<input type="number" class="form-control" id="cantidad_pedidos" name="cantidad_pedidos" value="<?php echo htmlspecialchars($liquidacion['cantidad_pedidos']); ?>" required>
</div>
<div class="mb-3">
<label for="precio_liquidacion" class="form-label">Precio Liquidación</label>
<input type="number" step="0.01" class="form-control" id="precio_liquidacion" name="precio_liquidacion" value="<?php echo htmlspecialchars($liquidacion['precio_liquidacion']); ?>" required>
</div>
<div class="mb-3">
<label for="fecha" class="form-label">Fecha</label>
<input type="datetime-local" class="form-control" id="fecha" name="fecha" value="<?php echo date('Y-m-d\TH:i', strtotime($liquidacion['fecha'])); ?>" required>
</div>
<button type="submit" class="btn btn-primary">Guardar Cambios</button>
<a href="liquidaciones.php" class="btn btn-secondary">Cancelar</a>
</form>
</div>
</div>
<?php require_once 'includes/footer.php'; ?>

65
editar_producto.php Normal file
View File

@ -0,0 +1,65 @@
<?php
require_once 'includes/header.php';
$product_id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($product_id === 0) {
$_SESSION['error_message'] = "ID de producto no válido.";
header("Location: productos.php");
exit();
}
$pdo = db();
$stmt = $pdo->prepare("SELECT * FROM productos WHERE id = ?");
$stmt->execute([$product_id]);
$product = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$product) {
$_SESSION['error_message'] = "Producto no encontrado.";
header("Location: productos.php");
exit();
}
?>
<div class="container-fluid">
<div class="d-sm-flex justify-content-between align-items-center mb-4">
<h3 class="text-dark mb-0">Editar Producto</h3>
</div>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Detalles del Producto</h6>
</div>
<div class="card-body">
<form action="handle_editar_producto.php" method="post">
<input type="hidden" name="id" value="<?php echo htmlspecialchars($product['id']); ?>">
<div class="mb-3">
<label for="nombre" class="form-label">Nombre del Producto</label>
<input type="text" class="form-control" id="nombre" name="nombre" value="<?php echo htmlspecialchars($product['nombre']); ?>" required>
</div>
<div class="mb-3">
<label for="descripcion" class="form-label">Descripción</label>
<textarea class="form-control" id="descripcion" name="descripcion" rows="3"><?php echo htmlspecialchars($product['descripcion']); ?></textarea>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="costo" class="form-label">Precio de Compra</label>
<input type="number" class="form-control" id="costo" name="costo" step="0.01" value="<?php echo htmlspecialchars($product['costo']); ?>" required>
</div>
<div class="col-md-6 mb-3">
<label for="precio_venta" class="form-label">Precio de Venta</label>
<input type="number" class="form-control" id="precio_venta" name="precio_venta" step="0.01" value="<?php echo htmlspecialchars($product['precio_venta']); ?>" required>
</div>
</div>
<button type="submit" class="btn btn-primary">Guardar Cambios</button>
<a href="productos.php" class="btn btn-secondary">Cancelar</a>
</form>
</div>
</div>
</div>
<?php require_once 'includes/footer.php'; ?>

50
eliminar_ciudad.php Normal file
View File

@ -0,0 +1,50 @@
<?php
session_start();
if (!isset($_SESSION["user_id"])) {
header("Location: auth/login.php");
exit();
}
require_once 'db/config.php';
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id === 0) {
$_SESSION['error_message'] = "ID de ciudad no válido.";
header("Location: ciudades.php");
exit();
}
try {
$pdo = db();
// Check if the city is being used in stock_por_ciudad
$check_sql = "SELECT COUNT(*) FROM stock_por_ciudad WHERE ciudad_id = :id";
$check_stmt = $pdo->prepare($check_sql);
$check_stmt->bindParam(':id', $id, PDO::PARAM_INT);
$check_stmt->execute();
$usage_count = $check_stmt->fetchColumn();
if ($usage_count > 0) {
$_SESSION['error_message'] = "No se puede eliminar la ciudad porque tiene stock asociado. Por favor, reasigne o elimine el stock primero.";
header("Location: ciudades.php");
exit();
}
// If not in use, proceed with deletion
$sql = "DELETE FROM ciudades WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
if ($stmt->execute()) {
$_SESSION['success_message'] = "Ciudad eliminada exitosamente.";
} else {
$_SESSION['error_message'] = "Error al eliminar la ciudad.";
}
} catch (PDOException $e) {
$_SESSION['error_message'] = "Error de base de datos: " . $e->getMessage();
}
header("Location: ciudades.php");
exit();
?>

70
eliminar_colaborador.php Normal file
View File

@ -0,0 +1,70 @@
<?php
session_start();
if (!isset($_SESSION["user_id"])) {
header("Location: auth/login.php");
exit();
}
if (!isset($_SESSION['user_rol']) || $_SESSION['user_rol'] !== 'Administrador General') {
$_SESSION['error_message'] = 'No tienes permiso para realizar esta acción.';
header('Location: colaboradores.php');
exit();
}
require_once 'db/config.php';
// Check for user ID
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
$_SESSION['error_message'] = "ID de colaborador no válido.";
header("Location: colaboradores.php");
exit();
}
$id_to_delete = (int)$_GET['id'];
$current_user_id = (int)$_SESSION['user_id'];
// Security checks
if ($id_to_delete === $current_user_id) {
$_SESSION['error_message'] = "No puedes eliminar tu propia cuenta.";
header("Location: colaboradores.php");
exit();
}
if ($id_to_delete === 1) {
$_SESSION['error_message'] = "No se puede eliminar al administrador principal del sistema.";
header("Location: colaboradores.php");
exit();
}
try {
$pdo = db();
// Check if the user exists before trying to delete
$stmt = $pdo->prepare("SELECT id FROM usuarios WHERE id = :id");
$stmt->execute([':id' => $id_to_delete]);
if (!$stmt->fetch()) {
$_SESSION['error_message'] = "El colaborador que intentas eliminar no existe.";
header("Location: colaboradores.php");
exit();
}
// Proceed with deletion
$stmt = $pdo->prepare("DELETE FROM usuarios WHERE id = :id");
$stmt->execute([':id' => $id_to_delete]);
$_SESSION['success_message'] = "Colaborador eliminado exitosamente.";
header("Location: colaboradores.php");
exit();
} catch (PDOException $e) {
error_log("Error al eliminar colaborador: " . $e->getMessage());
// Foreign key constraint check
if ($e->getCode() == '23000') {
$_SESSION['error_message'] = "No se puede eliminar este colaborador porque tiene registros asociados (movimientos, ventas, etc.).";
} else {
$_SESSION['error_message'] = "Error al conectar con la base de datos.";
}
header("Location: colaboradores.php");
exit();
}
?>

View File

@ -0,0 +1,34 @@
<?php
require_once 'db/config.php';
if (!isset($_GET['id']) || empty($_GET['id'])) {
header('Location: info_producto.php');
exit;
}
$info_id = $_GET['id'];
$pdo = db();
// First, get the image URL to delete the file
$stmt_select = $pdo->prepare('SELECT imagen_url FROM info_productos WHERE id = :id');
$stmt_select->execute([':id' => $info_id]);
$info = $stmt_select->fetch(PDO::FETCH_ASSOC);
if ($info) {
// Delete the database record
$stmt_delete = $pdo->prepare('DELETE FROM info_productos WHERE id = :id');
if ($stmt_delete->execute([':id' => $info_id])) {
// If DB deletion is successful, delete the image file
if (file_exists($info['imagen_url'])) {
unlink($info['imagen_url']);
}
$_SESSION['success_message'] = 'La tarjeta de información ha sido eliminada.';
} else {
$_SESSION['error_message'] = 'No se pudo eliminar la tarjeta de la base de datos.';
}
} else {
$_SESSION['error_message'] = 'No se encontró la tarjeta especificada.';
}
header('Location: info_producto.php');
exit;

29
eliminar_inversion.php Normal file
View File

@ -0,0 +1,29 @@
<?php
require_once 'db/config.php';
require_once 'includes/header.php'; // Para la sesión y seguridad
// Verificar permisos
if (!isset($_SESSION['user_id'])) {
header('Location: /auth/login.php');
exit;
}
$id = $_GET['id'] ?? null;
if (!$id) {
header('Location: inversiones_operativas.php?error=missing_id');
exit;
}
try {
$pdo = db();
$stmt = $pdo->prepare("DELETE FROM inversiones WHERE id = ?");
$stmt->execute([$id]);
header('Location: inversiones_operativas.php?success=deleted');
exit;
} catch (PDOException $e) {
header('Location: inversiones_operativas.php?error=db_error');
exit;
}
?>

62
eliminar_producto.php Normal file
View File

@ -0,0 +1,62 @@
<?php
session_start();
if (!isset($_SESSION["user_id"])) {
header("Location: auth/login.php");
exit();
}
require_once 'db/config.php';
$product_id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($product_id > 0) {
$pdo = db();
try {
// 1. Verificar si hay movimientos asociados
$sql_check = "SELECT COUNT(*) FROM movimientos WHERE producto_id = ?";
$stmt_check = $pdo->prepare($sql_check);
$stmt_check->execute([$product_id]);
$movement_count = $stmt_check->fetchColumn();
if ($movement_count > 0) {
// Si hay movimientos, no permitir eliminar y mostrar un error amigable
$_SESSION['error_message'] = "No se puede eliminar: este producto ya tiene un historial de entradas y salidas.";
} else {
// Si no hay movimientos, proceder con la eliminación
$pdo->beginTransaction();
// Primero, eliminar el stock asociado (si existe)
$sql_stock = "DELETE FROM stock_por_ciudad WHERE producto_id = ?";
$stmt_stock = $pdo->prepare($sql_stock);
$stmt_stock->execute([$product_id]);
// Luego, eliminar el producto
$sql_product = "DELETE FROM productos WHERE id = ?";
$stmt_product = $pdo->prepare($sql_product);
$stmt_product->execute([$product_id]);
if ($stmt_product->rowCount() > 0) {
$pdo->commit();
$_SESSION['success_message'] = "Producto eliminado correctamente.";
} else {
$pdo->rollBack();
$_SESSION['error_message'] = "El producto no pudo ser encontrado o ya fue eliminado.";
}
}
} catch (PDOException $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
// Ocultamos el mensaje técnico y mostramos uno más amigable
$_SESSION['error_message'] = "Ocurrió un error al intentar eliminar el producto. Por favor, inténtalo de nuevo.";
// Opcional: podrías loggear el error real para depuración
// error_log("Error al eliminar producto: " . $e->getMessage());
}
} else {
$_SESSION['error_message'] = "ID de producto no válido.";
}
header("Location: productos.php");
exit();
?>

345
flujo_de_caja.php Normal file
View File

@ -0,0 +1,345 @@
<?php
require_once 'includes/header.php';
if (isset($_SESSION['user_rol']) && $_SESSION['user_rol'] === 'Encargado de Stock') {
header('Location: /index.php?error=unauthorized');
exit;
}
require_once 'db/config.php';
$db = db();
// Lógica para obtener el mes y año seleccionados
$selected_month = isset($_GET['month']) ? (int)$_GET['month'] : date('m');
$selected_year = isset($_GET['year']) ? (int)$_GET['year'] : date('Y');
$days_in_month = cal_days_in_month(CAL_GREGORIAN, $selected_month, $selected_year);
$start_date = "$selected_year-$selected_month-01";
$end_date = "$selected_year-$selected_month-$days_in_month";
// --- Obtener Recaudos Contraentrega ---
$stmt_recaudos = $db->prepare(
"SELECT DATE(fecha) as fecha_recaudo, SUM(precio_liquidacion) as total_recaudado
FROM movimientos
WHERE tipo = 'salida' AND fecha BETWEEN ? AND ?
GROUP BY DATE(fecha)"
);
$stmt_recaudos->execute([$start_date, $end_date]);
$recaudos_por_fecha = $stmt_recaudos->fetchAll(PDO::FETCH_KEY_PAIR);
// --- Obtener datos del flujo de caja para el mes ---
$stmt_flujo = $db->prepare("SELECT * FROM flujo_de_caja WHERE YEAR(fecha) = ? AND MONTH(fecha) = ? ORDER BY fecha ASC");
$stmt_flujo->execute([$selected_year, $selected_month]);
$flujo_data = $stmt_flujo->fetchAll(PDO::FETCH_ASSOC);
$data_por_dia = [];
foreach ($flujo_data as $row) {
$data_por_dia[$row['fecha']] = $row;
}
$dias_del_mes = [];
for ($day = 1; $day <= $days_in_month; $day++) {
$fecha_str = sprintf("%s-%s-%02d", $selected_year, str_pad($selected_month, 2, '0', STR_PAD_LEFT), $day);
$default_row = [
'bcp_yape' => 0, 'banco_nacion' => 0, 'interbank' => 0, 'bbva' => 0, 'otros_ingresos' => 0, 'rc_envio' => 0,
'tu_1' => 0, 'tu_2' => 0, 'tu_3' => 0, 'fl_1' => 0, 'fl_2' => 0, 'fl_3' => 0
];
$dia_data = $data_por_dia[$fecha_str] ?? $default_row;
// RC ENVIO es la suma de los otros ingresos
$rc_envio_dia =
(float)($dia_data['bcp_yape'] ?? 0) + (float)($dia_data['banco_nacion'] ?? 0) +
(float)($dia_data['interbank'] ?? 0) + (float)($dia_data['bbva'] ?? 0) +
(float)($dia_data['otros_ingresos'] ?? 0);
$dia_data['rc_envio'] = $rc_envio_dia;
$recaudo_contraentrega = $recaudos_por_fecha[$fecha_str] ?? 0;
$total_ingresos_dia = $rc_envio_dia + (float)$recaudo_contraentrega;
$total_egresos_dia =
(float)($dia_data['tu_1'] ?? 0) + (float)($dia_data['tu_2'] ?? 0) +
(float)($dia_data['tu_3'] ?? 0) + (float)($dia_data['fl_1'] ?? 0) +
(float)($dia_data['fl_2'] ?? 0) + (float)($dia_data['fl_3'] ?? 0);
// El "Recaudo final" es la diferencia del día
$recaudo_final_dia = $total_ingresos_dia - $total_egresos_dia;
$dias_del_mes[] = [
'fecha' => $fecha_str,
'data' => $dia_data,
'recaudo_contraentrega' => $recaudo_contraentrega,
'total_ingresos' => $total_ingresos_dia,
'total_egresos' => $total_egresos_dia,
'recaudo_final' => $recaudo_final_dia
];
}
?>
<style>
.table-responsive { max-height: 80vh; overflow-y: auto; }
.table th, .table td { white-space: nowrap; padding: 0.5rem; vertical-align: middle; }
.editable { cursor: pointer; background-color: #f9f9f9; }
.editable:hover { background-color: #e9ecef; }
.ingreso { background-color: #d4edda; }
.egreso { background-color: #f8d7da; }
.total-ingreso, .total-egreso, .saldo { font-weight: bold; }
/* Estilos para el encabezado anclado */
thead.table-dark th {
position: sticky;
top: 0;
z-index: 1;
background-color: #343a40 !important; /* Fondo oscuro */
color: #fff !important; /* Texto blanco */
}
tfoot { position: sticky; bottom: 0; background-color: #fff; z-index: 10; }
</style>
<div class="container-fluid mt-4">
<h2>Flujo de Caja</h2>
<form method="get" class="row g-3 align-items-center mb-3">
<div class="col-auto"><label for="month" class="col-form-label">Mes</label></div>
<div class="col-auto">
<select name="month" id="month" class="form-select">
<?php for ($i = 1; $i <= 12; $i++): ?>
<option value="<?= $i ?>" <?= $i == $selected_month ? 'selected' : '' ?>><?= ucfirst(strftime('%B', mktime(0, 0, 0, $i, 1))) ?></option>
<?php endfor; ?>
</select>
</div>
<div class="col-auto"><label for="year" class="col-form-label">Año</label></div>
<div class="col-auto">
<select name="year" id="year" class="form-select">
<?php for ($y = date('Y'); $y >= 2020; $y--): ?>
<option value="<?= $y ?>" <?= $y == $selected_year ? 'selected' : '' ?>><?= $y ?></option>
<?php endfor; ?>
</select>
</div>
<div class="col-auto"><button type="submit" class="btn btn-primary">Ver</button></div>
</form>
<div class="table-responsive">
<table class="table table-bordered table-striped table-hover">
<thead class="table-dark">
<tr>
<th rowspan="2" style="text-align:center; vertical-align: middle;">Fecha</th>
<th colspan="5">Ingresos</th>
<th colspan="6">Inversion publicitaria</th>
<th rowspan="2" class="ingreso" style="text-align:center; vertical-align: middle;">RC ENVIO</th>
<th rowspan="2" class="ingreso" style="text-align:center; vertical-align: middle;">RC CONTRAENT</th>
<th rowspan="2" style="text-align:center; vertical-align: middle;">Total Ingresos</th>
<th rowspan="2" style="text-align:center; vertical-align: middle;">Total Inversion Publicitaria</th>
<th rowspan="2" style="text-align:center; vertical-align: middle;">Recaudo final</th>
</tr>
<tr>
<!-- Ingresos -->
<th class="ingreso">BCP/YAPE</th>
<th class="ingreso">B. NACION</th>
<th class="ingreso">INTERBANK</th>
<th class="ingreso">BBVA</th>
<th class="ingreso">Otros Ingresos</th>
<!-- Egresos -->
<th class="egreso">TU 1</th>
<th class="egreso">TU 2</th>
<th class="egreso">TU 3</th>
<th class="egreso">FL1</th>
<th class="egreso">FL2</th>
<th class="egreso">FL3</th>
</tr>
</thead>
<tbody>
<?php foreach ($dias_del_mes as $dia): ?>
<tr data-fecha="<?= $dia['fecha'] ?>">
<td><?= date("d/m/Y", strtotime($dia['fecha'])) ?></td>
<!-- Ingresos -->
<td class="editable ingreso" data-field="bcp_yape"><?= number_format($dia['data']['bcp_yape'] ?? 0, 2) ?></td>
<td class="editable ingreso" data-field="banco_nacion"><?= number_format($dia['data']['banco_nacion'] ?? 0, 2) ?></td>
<td class="editable ingreso" data-field="interbank"><?= number_format($dia['data']['interbank'] ?? 0, 2) ?></td>
<td class="editable ingreso" data-field="bbva"><?= number_format($dia['data']['bbva'] ?? 0, 2) ?></td>
<td class="editable ingreso" data-field="otros_ingresos"><?= number_format($dia['data']['otros_ingresos'] ?? 0, 2) ?></td>
<!-- Egresos -->
<td class="editable egreso" data-field="tu_1"><?= number_format($dia['data']['tu_1'] ?? 0, 2) ?></td>
<td class="editable egreso" data-field="tu_2"><?= number_format($dia['data']['tu_2'] ?? 0, 2) ?></td>
<td class="editable egreso" data-field="tu_3"><?= number_format($dia['data']['tu_3'] ?? 0, 2) ?></td>
<td class="editable egreso" data-field="fl_1"><?= number_format($dia['data']['fl_1'] ?? 0, 2) ?></td>
<td class="editable egreso" data-field="fl_2"><?= number_format($dia['data']['fl_2'] ?? 0, 2) ?></td>
<td class="editable egreso" data-field="fl_3"><?= number_format($dia['data']['fl_3'] ?? 0, 2) ?></td>
<!-- RC (ya no es editable) -->
<td class="ingreso" data-field="rc_envio"><?= number_format($dia['data']['rc_envio'] ?? 0, 2) ?></td>
<td class="ingreso" data-field="rc_contraent"><?= number_format($dia['recaudo_contraentrega'], 2) ?></td>
<!-- Totales -->
<td class="total-ingreso" data-field="total_ingresos"><?= number_format($dia['total_ingresos'], 2) ?></td>
<td class="total-egreso" data-field="total_egresos"><?= number_format($dia['total_egresos'], 2) ?></td>
<td class="saldo" data-field="recaudo_final"><?= number_format($dia['recaudo_final'], 2) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr class="table-dark" style="font-weight: bold;">
<td>TOTALES</td>
<td data-total="bcp_yape">0.00</td>
<td data-total="banco_nacion">0.00</td>
<td data-total="interbank">0.00</td>
<td data-total="bbva">0.00</td>
<td data-total="otros_ingresos">0.00</td>
<td data-total="tu_1">0.00</td>
<td data-total="tu_2">0.00</td>
<td data-total="tu_3">0.00</td>
<td data-total="fl_1">0.00</td>
<td data-total="fl_2">0.00</td>
<td data-total="fl_3">0.00</td>
<td data-total="rc_envio">0.00</td>
<td data-total="rc_contraent">0.00</td>
<td data-total="total_ingresos">0.00</td>
<td data-total="total_egresos">0.00</td>
<td data-total="recaudo_final">0.00</td>
</tr>
</tfoot>
</table>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const table = document.querySelector('.table');
table.addEventListener('dblclick', function (e) {
if (e.target.classList.contains('editable')) {
makeCellEditable(e.target);
}
});
function makeCellEditable(cell) {
const originalValue = parseFloat(cell.textContent.trim().replace(/,/g, '')) || 0;
if (cell.querySelector('input')) return;
const input = document.createElement('input');
input.type = 'number';
input.step = '0.01';
input.value = originalValue.toFixed(2);
input.className = 'form-control';
cell.innerHTML = '';
cell.appendChild(input);
input.focus();
input.select();
const onFinish = () => {
const newValue = parseFloat(input.value) || 0;
cell.textContent = newValue.toFixed(2);
saveData(cell, newValue, originalValue);
};
input.addEventListener('blur', onFinish, { once: true });
input.addEventListener('keydown', e => {
if (e.key === 'Enter') input.blur();
if (e.key === 'Escape') {
cell.textContent = originalValue.toFixed(2);
input.removeEventListener('blur', onFinish);
}
});
}
function saveData(cell, value, originalValue) {
const row = cell.closest('tr');
updateUI(row); // Optimistic UI update
const formData = new FormData();
formData.append('fecha', row.dataset.fecha);
formData.append('field', cell.dataset.field);
formData.append('value', value);
fetch('handle_editar_flujo_de_caja.php', { method: 'POST', body: formData })
.then(res => res.json())
.then(data => {
if (!data.success) {
console.error('Error al guardar:', data.message);
cell.textContent = originalValue.toFixed(2);
updateUI(row); // Revert on failure
}
})
.catch(error => {
console.error('Error de red:', error);
cell.textContent = originalValue.toFixed(2);
updateUI(row); // Revert on failure
});
}
function getCellValue(row, field) {
const cell = row.querySelector(`[data-field="${field}"]`);
return cell ? parseFloat(cell.textContent.replace(/,/g, '')) : 0;
}
function setCellValue(row, field, value) {
const cell = row.querySelector(`[data-field="${field}"]`);
if (cell) cell.textContent = value.toFixed(2);
}
function recalculateRow(row) {
// Calcular rc_envio a partir de sus componentes
const rc_envio_calc =
getCellValue(row, 'bcp_yape') + getCellValue(row, 'banco_nacion') +
getCellValue(row, 'interbank') + getCellValue(row, 'bbva') +
getCellValue(row, 'otros_ingresos');
setCellValue(row, 'rc_envio', rc_envio_calc);
const ingresos = rc_envio_calc + getCellValue(row, 'rc_contraent');
const egresos =
getCellValue(row, 'tu_1') + getCellValue(row, 'tu_2') +
getCellValue(row, 'tu_3') + getCellValue(row, 'fl_1') +
getCellValue(row, 'fl_2') + getCellValue(row, 'fl_3');
const recaudoFinal = ingresos - egresos;
setCellValue(row, 'total_ingresos', ingresos);
setCellValue(row, 'total_egresos', egresos);
setCellValue(row, 'recaudo_final', recaudoFinal);
}
function updateUI(row) {
recalculateRow(row);
updateFooterTotals();
}
function updateFooterTotals() {
const footer = document.querySelector('tfoot tr');
const totals = {};
footer.querySelectorAll('[data-total]').forEach(cell => {
totals[cell.dataset.total] = 0;
});
document.querySelectorAll('tbody tr').forEach(row => {
for (const key in totals) {
totals[key] += getCellValue(row, key);
}
});
// Recalcular totales derivados que no son una suma directa de columnas
totals['rc_envio'] = totals['bcp_yape'] + totals['banco_nacion'] + totals['interbank'] + totals['bbva'] + totals['otros_ingresos'];
totals['total_ingresos'] = totals['rc_envio'] + totals['rc_contraent'];
totals['total_egresos'] = totals['tu_1'] + totals['tu_2'] + totals['tu_3'] + totals['fl_1'] + totals['fl_2'] + totals['fl_3'];
totals['recaudo_final'] = totals['total_ingresos'] - totals['total_egresos'];
for (const key in totals) {
const cell = footer.querySelector(`[data-total="${key}"]`);
if(cell) cell.textContent = totals[key].toFixed(2);
}
}
// Initial calculation on load
document.querySelectorAll('tbody tr').forEach(row => recalculateRow(row));
updateFooterTotals();
});
</script>
<?php require_once 'includes/footer.php'; ?>

52
handle_agregar_ciudad.php Normal file
View File

@ -0,0 +1,52 @@
<?php
session_start();
if (!isset($_SESSION["user_id"])) {
header("Location: auth/login.php");
exit();
}
require_once 'db/config.php';
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$nombre = trim($_POST['nombre']);
if (empty($nombre)) {
$_SESSION['error_message'] = "El nombre de la ciudad no puede estar vacío.";
header("Location: agregar_ciudad.php");
exit();
}
try {
$pdo = db();
// Obtener el máximo valor de orden actual
$stmt_max = $pdo->query("SELECT MAX(orden) AS max_orden FROM ciudades");
$max_orden = $stmt_max->fetchColumn();
$nuevo_orden = ($max_orden !== null) ? $max_orden + 1 : 0;
$sql = "INSERT INTO ciudades (nombre, orden) VALUES (:nombre, :orden)";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':nombre', $nombre, PDO::PARAM_STR);
$stmt->bindParam(':orden', $nuevo_orden, PDO::PARAM_INT);
if ($stmt->execute()) {
$_SESSION['success_message'] = "Ciudad agregada exitosamente.";
} else {
$_SESSION['error_message'] = "Error al agregar la ciudad.";
}
} catch (PDOException $e) {
// Check for duplicate entry
if ($e->errorInfo[1] == 1062) {
$_SESSION['error_message'] = "Error: Ya existe una ciudad con ese nombre.";
} else {
$_SESSION['error_message'] = "Error de base de datos: " . $e->getMessage();
}
}
header("Location: ciudades.php");
exit();
} else {
header("Location: agregar_ciudad.php");
exit();
}
?>

View File

@ -0,0 +1,83 @@
<?php
session_start();
if (!isset($_SESSION["user_id"])) {
header("Location: auth/login.php");
exit();
}
if (!isset($_SESSION['user_rol']) || $_SESSION['user_rol'] !== 'Administrador General') {
$_SESSION['error_message'] = 'No tienes permiso para realizar esta acción.';
header('Location: colaboradores.php');
exit();
}
require_once 'db/config.php';
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$nombre = trim($_POST['nombre']);
$email = trim($_POST['email']);
$password = $_POST['password'];
$rol = $_POST['rol'];
// Validations
if (empty($nombre) || empty($email) || empty($password) || empty($rol)) {
$_SESSION['error_message'] = "Todos los campos son obligatorios.";
header("Location: agregar_colaborador.php");
exit();
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$_SESSION['error_message'] = "El formato del correo electrónico no es válido.";
header("Location: agregar_colaborador.php");
exit();
}
if (!in_array($rol, ['Administrador General', 'Encargado de Stock'])) {
$_SESSION['error_message'] = "El rol seleccionado no es válido.";
header("Location: agregar_colaborador.php");
exit();
}
try {
$pdo = db();
// Check if email already exists
$stmt = $pdo->prepare("SELECT id FROM usuarios WHERE email = :email");
$stmt->execute([':email' => $email]);
if ($stmt->fetch()) {
$_SESSION['error_message'] = "El correo electrónico ya está registrado.";
header("Location: agregar_colaborador.php");
exit();
}
// Hash the password
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
// Insert new user
$sql = "INSERT INTO usuarios (nombre, email, password, rol) VALUES (:nombre, :email, :password, :rol)";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':nombre' => $nombre,
':email' => $email,
':password' => $hashed_password,
':rol' => $rol
]);
$_SESSION['success_message'] = "Colaborador agregado exitosamente.";
header("Location: colaboradores.php");
exit();
} catch (PDOException $e) {
error_log("Error al agregar colaborador: " . $e->getMessage());
$_SESSION['error_message'] = "Error al conectar con la base de datos. Por favor, inténtelo de nuevo.";
header("Location: agregar_colaborador.php");
exit();
}
} else {
// Redirect if not a POST request
header("Location: agregar_colaborador.php");
exit();
}
?>

View File

@ -0,0 +1,47 @@
<?php
require_once 'db/config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$producto_id = $_POST['producto_id'] ?? null;
$texto_informativo = $_POST['texto_informativo'] ?? null;
$column_id = $_POST['column_id'] ?? null;
$imagen = $_FILES['imagen'] ?? null;
if (!$producto_id || !$texto_informativo || !$column_id || !$imagen || $imagen['error'] !== UPLOAD_ERR_OK) {
$_SESSION['error_message'] = 'Por favor, completa todos los campos, incluyendo la columna Kanban, y sube una imagen válida.';
header('Location: info_producto.php');
exit;
}
$upload_dir = 'assets/images/info_productos/';
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0777, true);
}
$imagen_nombre = uniqid('prod_', true) . '_' . basename($imagen['name']);
$imagen_path = $upload_dir . $imagen_nombre;
if (move_uploaded_file($imagen['tmp_name'], $imagen_path)) {
try {
$pdo = db();
$sql = 'INSERT INTO info_productos (producto_id, imagen_url, texto_informativo, column_id) VALUES (:producto_id, :imagen_url, :texto_informativo, :column_id)';
$stmt = $pdo->prepare($sql);
$stmt->execute([
':producto_id' => $producto_id,
':imagen_url' => $imagen_path,
':texto_informativo' => $texto_informativo,
':column_id' => $column_id
]);
$_SESSION['success_message'] = 'Tarjeta de información creada exitosamente.';
} catch (PDOException $e) {
// If DB insert fails, delete the uploaded image
unlink($imagen_path);
$_SESSION['error_message'] = 'Error al guardar en la base de datos: ' . $e->getMessage();
}
} else {
$_SESSION['error_message'] = 'Error al subir la imagen.';
}
}
header('Location: info_producto.php');
exit;

View File

@ -0,0 +1,84 @@
<?php
session_start();
require_once 'db/config.php';
// Verificar que el usuario sea administrador
if (!isset($_SESSION['user_rol']) || ($_SESSION['user_rol'] !== 'admin' && $_SESSION['user_rol'] !== 'administrador')) {
// Si no es admin, redirigir o mostrar un error
header('Location: /liquidaciones.php?error=auth');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$producto_id = $_POST['producto_id'] ?? null;
$ciudad_id = $_POST['ciudad_id'] ?? null;
$cantidad = $_POST['cantidad'] ?? null;
$cantidad_pedidos = $_POST['cantidad_pedidos'] ?? null;
$precio_liquidacion = $_POST['precio_liquidacion'] ?? null;
// Validación simple
if (empty($producto_id) || empty($ciudad_id) || empty($cantidad) || $cantidad <= 0 || empty($cantidad_pedidos) || empty($precio_liquidacion)) {
header('Location: /liquidaciones.php?error=missing_fields');
exit;
}
$pdo = db();
try {
$pdo->beginTransaction();
// 1. Verificar stock actual
$stmt = $pdo->prepare("SELECT stock_actual FROM stock_por_ciudad WHERE producto_id = :producto_id AND ciudad_id = :ciudad_id FOR UPDATE");
$stmt->execute(['producto_id' => $producto_id, 'ciudad_id' => $ciudad_id]);
$stock_disponible = $stmt->fetchColumn();
if ($stock_disponible === false || $stock_disponible < $cantidad) {
header('Location: /liquidaciones.php?error=no_stock');
$pdo->rollBack();
exit();
}
// 2. Actualizar el stock en la ciudad
$stmt_stock = $pdo->prepare("UPDATE stock_por_ciudad SET stock_actual = stock_actual - :cantidad WHERE producto_id = :producto_id AND ciudad_id = :ciudad_id");
$stmt_stock->execute([':cantidad' => $cantidad, ':producto_id' => $producto_id, ':ciudad_id' => $ciudad_id]);
// 3. Insertar el movimiento de salida
$stmt = $pdo->prepare("
INSERT INTO movimientos
(tipo, producto_id, cantidad, ciudad_origen_id, cantidad_pedidos, precio_liquidacion, fecha, usuario_id)
VALUES ('Salida', :producto_id, :cantidad, :ciudad_id, :cantidad_pedidos, :precio_liquidacion, NOW(), :usuario_id)
");
$user_id = get_current_user_id();
$stmt->execute([
':producto_id' => $producto_id,
':cantidad' => $cantidad,
':ciudad_id' => $ciudad_id,
':cantidad_pedidos' => $cantidad_pedidos,
':precio_liquidacion' => $precio_liquidacion,
':usuario_id' => $user_id
]);
// Confirmar transacción
$pdo->commit();
header('Location: /liquidaciones.php?success=added');
exit;
} catch (PDOException $e) {
// Revertir transacción en caso de error
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
// Manejo de errores
error_log($e->getMessage());
header('Location: /liquidaciones.php?error=db_error');
exit;
}
} else {
// Si no es POST, redirigir
header('Location: /liquidaciones.php');
exit;
}
?>

View File

@ -0,0 +1,72 @@
<?php
session_start();
if (!isset($_SESSION["user_id"])) {
header("Location: auth/login.php");
exit();
}
require_once 'db/config.php';
if ($_SERVER["REQUEST_METHOD"] == "POST") {
// Sanitize and retrieve product data
$nombre = filter_input(INPUT_POST, 'nombre', FILTER_SANITIZE_STRING);
$descripcion = filter_input(INPUT_POST, 'descripcion', FILTER_SANITIZE_STRING);
$costo = filter_input(INPUT_POST, 'costo', FILTER_VALIDATE_FLOAT);
$precio_venta = filter_input(INPUT_POST, 'precio_venta', FILTER_VALIDATE_FLOAT);
// Retrieve stock data array
$stocks_por_ciudad = isset($_POST['stock_ciudad']) ? $_POST['stock_ciudad'] : [];
// Basic validation
if ($nombre && $costo !== false && $precio_venta !== false) {
$pdo = db();
try {
$pdo->beginTransaction();
// 1. Insert product
$sku = 'SKU-' . strtoupper(substr(uniqid(), -8));
$sql_producto = "INSERT INTO productos (sku, nombre, descripcion, costo, precio_venta, created_at) VALUES (?, ?, ?, ?, ?, NOW())";
$stmt_producto = $pdo->prepare($sql_producto);
$stmt_producto->execute([$sku, $nombre, $descripcion, $costo, $precio_venta]);
// Get the ID of the new product
$producto_id = $pdo->lastInsertId();
// 2. Insert stock for each city
$sql_stock = "INSERT INTO stock_por_ciudad (producto_id, ciudad_id, stock_actual) VALUES (?, ?, ?)";
$stmt_stock = $pdo->prepare($sql_stock);
foreach ($stocks_por_ciudad as $ciudad_id => $cantidad) {
$ciudad_id_int = filter_var($ciudad_id, FILTER_VALIDATE_INT);
$cantidad_int = filter_var($cantidad, FILTER_VALIDATE_INT);
// Only insert if stock is greater than 0 and IDs are valid
if ($ciudad_id_int && $cantidad_int > 0) {
$stmt_stock->execute([$producto_id, $ciudad_id_int, $cantidad_int]);
}
}
// If all is well, commit the transaction
$pdo->commit();
$_SESSION['success_message'] = "Producto y stock por ciudad agregados exitosamente.";
} catch (PDOException $e) {
// If something goes wrong, roll back the transaction
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
// Log error properly in a real application
$_SESSION['error_message'] = "Error al agregar el producto. Detalles: " . $e->getMessage();
}
} else {
$_SESSION['error_message'] = "Datos de producto inválidos. Por favor, revisa el formulario.";
}
} else {
$_SESSION['error_message'] = "Método no permitido.";
}
// Redirect back to the products page
header("Location: productos.php");
exit();

View File

@ -0,0 +1,47 @@
<?php
session_start();
require_once 'db/config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
date_default_timezone_set('America/Lima');
$usuario_id = filter_input(INPUT_POST, 'usuario_id', FILTER_VALIDATE_INT);
$fecha_actualizacion = $_POST['fecha_actualizacion'] ?? null;
$ciudad_id = filter_input(INPUT_POST, 'ciudad_id', FILTER_VALIDATE_INT);
if ($usuario_id && $fecha_actualizacion && $ciudad_id) {
$pdo = db();
try {
$pdo->beginTransaction();
$fecha_hora_actual = date('Y-m-d H:i:s');
$stmt = $pdo->prepare('INSERT INTO stock_confirmaciones (usuario_id, fecha_hora, fecha_actualizacion) VALUES (:usuario_id, :fecha_hora, :fecha_actualizacion)');
$stmt->execute([
'usuario_id' => $usuario_id,
'fecha_hora' => $fecha_hora_actual,
'fecha_actualizacion' => $fecha_actualizacion
]);
$confirmacion_id = $pdo->lastInsertId();
$stmt_ciudad = $pdo->prepare('INSERT INTO stock_confirmacion_ciudades (confirmacion_id, ciudad_id) VALUES (:confirmacion_id, :ciudad_id)');
$stmt_ciudad->execute([
'confirmacion_id' => $confirmacion_id,
'ciudad_id' => $ciudad_id
]);
$pdo->commit();
$_SESSION['flash_message'] = ['type' => 'success', 'message' => 'Entrega registrada exitosamente.'];
} catch (PDOException $e) {
$pdo->rollBack();
error_log('Error en handle_confirmacion_stock.php: ' . $e->getMessage());
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'Error al registrar la entrega.'];
}
} else {
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'Por favor, complete todos los campos.'];
}
}
header('Location: confirmacion_stock.php');
exit;
?>

18
handle_create_column.php Normal file
View File

@ -0,0 +1,18 @@
<?php
require_once 'db/config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$column_name = trim($_POST['column_name']);
if (!empty($column_name)) {
$pdo = db();
// Find the current max order value
$stmt_max = $pdo->query('SELECT MAX(orden) as max_orden FROM kanban_columns');
$max_orden = $stmt_max->fetchColumn() ?? 0;
$stmt = $pdo->prepare('INSERT INTO kanban_columns (nombre, orden) VALUES (:nombre, :orden)');
$stmt->execute([':nombre' => $column_name, ':orden' => $max_orden + 1]);
}
}
header('Location: kanban.php');
exit;

22
handle_delete_column.php Normal file
View File

@ -0,0 +1,22 @@
<?php
require_once 'db/config.php';
if (isset($_GET['id'])) {
$column_id = $_GET['id'];
$pdo = db();
// Set column_id to NULL for all cards in this column
$stmt_update = $pdo->prepare('UPDATE info_productos SET column_id = NULL WHERE column_id = :column_id');
$stmt_update->execute([':column_id' => $column_id]);
// Delete the column
$stmt_delete = $pdo->prepare('DELETE FROM kanban_columns WHERE id = :id');
$stmt_delete->execute([':id' => $column_id]);
$_SESSION['success_message'] = 'Columna eliminada. Las tarjetas han sido desasignadas.';
} else {
$_SESSION['error_message'] = 'ID de columna no especificado.';
}
header('Location: kanban.php');
exit;

48
handle_editar_ciudad.php Normal file
View File

@ -0,0 +1,48 @@
<?php
session_start();
if (!isset($_SESSION["user_id"])) {
header("Location: auth/login.php");
exit();
}
require_once 'db/config.php';
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
$nombre = trim($_POST['nombre']);
$orden = filter_input(INPUT_POST, 'orden', FILTER_VALIDATE_INT);
if ($id === 0 || empty($nombre) || $orden === false || $orden < 0) {
$_SESSION['error_message'] = "Datos inválidos para actualizar la ciudad.";
header("Location: ciudades.php");
exit();
}
try {
$pdo = db();
$sql = "UPDATE ciudades SET nombre = :nombre, orden = :orden WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':nombre', $nombre, PDO::PARAM_STR);
$stmt->bindParam(':orden', $orden, PDO::PARAM_INT);
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
if ($stmt->execute()) {
$_SESSION['success_message'] = "Ciudad actualizada exitosamente.";
} else {
$_SESSION['error_message'] = "Error al actualizar la ciudad.";
}
} catch (PDOException $e) {
if ($e->errorInfo[1] == 1062) {
$_SESSION['error_message'] = "Error: Ya existe una ciudad con ese nombre.";
} else {
$_SESSION['error_message'] = "Error de base de datos: " . $e->getMessage();
}
}
header("Location: ciudades.php");
exit();
} else {
header("Location: ciudades.php");
exit();
}
?>

View File

@ -0,0 +1,94 @@
<?php
session_start();
if (!isset($_SESSION["user_id"])) {
header("Location: auth/login.php");
exit();
}
if (!isset($_SESSION['user_rol']) || $_SESSION['user_rol'] !== 'Administrador General') {
$_SESSION['error_message'] = 'No tienes permiso para realizar esta acción.';
header('Location: colaboradores.php');
exit();
}
require_once 'db/config.php';
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$id = $_POST['id'];
$nombre = trim($_POST['nombre']);
$email = trim($_POST['email']);
$password = $_POST['password'];
$rol = $_POST['rol'];
// Validations
if (empty($id) || empty($nombre) || empty($email) || empty($rol)) {
$_SESSION['error_message'] = "Todos los campos, excepto la contraseña, son obligatorios.";
header("Location: editar_colaborador.php?id=" . $id);
exit();
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$_SESSION['error_message'] = "El formato del correo electrónico no es válido.";
header("Location: editar_colaborador.php?id=" . $id);
exit();
}
if (!in_array($rol, ['Administrador General', 'Encargado de Stock'])) {
$_SESSION['error_message'] = "El rol seleccionado no es válido.";
header("Location: editar_colaborador.php?id=" . $id);
exit();
}
try {
$pdo = db();
// Check if email already exists for another user
$stmt = $pdo->prepare("SELECT id FROM usuarios WHERE email = :email AND id != :id");
$stmt->execute([':email' => $email, ':id' => $id]);
if ($stmt->fetch()) {
$_SESSION['error_message'] = "El correo electrónico ya está registrado por otro usuario.";
header("Location: editar_colaborador.php?id=" . $id);
exit();
}
// Build the query
$sql_parts = [
"nombre = :nombre",
"email = :email",
"rol = :rol"
];
$params = [
':nombre' => $nombre,
':email' => $email,
':rol' => $rol,
':id' => $id
];
// If password is provided, add it to the query
if (!empty($password)) {
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
$sql_parts[] = "password = :password";
$params[':password'] = $hashed_password;
}
$sql = "UPDATE usuarios SET " . implode(", ", $sql_parts) . " WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$_SESSION['success_message'] = "Colaborador actualizado exitosamente.";
header("Location: colaboradores.php");
exit();
} catch (PDOException $e) {
error_log("Error al editar colaborador: " . $e->getMessage());
$_SESSION['error_message'] = "Error al conectar con la base de datos. Por favor, inténtelo de nuevo.";
header("Location: editar_colaborador.php?id=" . $id);
exit();
}
} else {
header("Location: colaboradores.php");
exit();
}
?>

View File

@ -0,0 +1,73 @@
<?php
require_once 'db/config.php';
// Habilitar el reporte de errores para depuración
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
header('Content-Type: application/json');
$response = ['success' => false, 'message' => 'Error desconocido.'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$fecha = $_POST['fecha'] ?? null;
$field = $_POST['field'] ?? null;
$value = $_POST['value'] ?? null;
if (!$fecha || !$field || $value === null) {
$response['message'] = 'Datos incompletos: fecha, campo o valor no proporcionados.';
echo json_encode($response);
exit;
}
// Lista blanca de campos editables para seguridad
$allowed_fields = [
'bcp_yape', 'banco_nacion', 'interbank', 'bbva', 'otros_ingresos',
'tu_1', 'tu_2', 'tu_3',
'fl_1', 'fl_2', 'fl_3'
];
if (!in_array($field, $allowed_fields)) {
$response['message'] = "El campo '{$field}' no es editable.";
echo json_encode($response);
exit;
}
try {
$db = db();
// Verificar si ya existe una fila para esa fecha
$stmt_check = $db->prepare("SELECT id FROM flujo_de_caja WHERE fecha = ?");
$stmt_check->execute([$fecha]);
$existing_id = $stmt_check->fetchColumn();
if ($existing_id) {
// Si existe, actualizar el campo específico
$sql = "UPDATE flujo_de_caja SET {$field} = ? WHERE id = ?";
$stmt = $db->prepare($sql);
$stmt->execute([$value, $existing_id]);
} else {
// Si no existe, crear una nueva fila con el valor
$sql = "INSERT INTO flujo_de_caja (fecha, {$field}) VALUES (?, ?)";
$stmt = $db->prepare($sql);
$stmt->execute([$fecha, $value]);
}
if ($stmt->rowCount() > 0 || $existing_id) {
$response['success'] = true;
$response['message'] = 'Dato guardado correctamente.';
} else {
$response['message'] = 'No se realizaron cambios.';
}
} catch (PDOException $e) {
// Capturar errores de la base de datos
$response['message'] = "Error de base de datos: " . $e->getMessage();
}
} else {
$response['message'] = 'Método de solicitud no válido.';
}
echo json_encode($response);
?>

View File

@ -0,0 +1,38 @@
<?php
require_once 'db/config.php';
require_once 'includes/header.php'; // Para la sesión y seguridad
// Verificar permisos
if (!isset($_SESSION['user_id'])) {
header('Location: /auth/login.php');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = $_POST['id'] ?? null;
$fecha = $_POST['fecha'] ?? null;
$descripcion = trim($_POST['descripcion'] ?? '');
$monto = $_POST['monto'] ?? null;
$tipo = $_POST['tipo'] ?? null;
if (!$id || !$fecha || empty($descripcion) || !is_numeric($monto) || empty($tipo)) {
header('Location: inversiones_operativas.php?error=invalid_data');
exit;
}
try {
$pdo = db();
$stmt = $pdo->prepare("UPDATE inversiones SET fecha = ?, descripcion = ?, monto = ?, tipo = ? WHERE id = ?");
$stmt->execute([$fecha, $descripcion, $monto, $tipo, $id]);
header('Location: inversiones_operativas.php?success=updated#section-' . $tipo);
exit;
} catch (PDOException $e) {
header('Location: inversiones_operativas.php?error=db_error');
exit;
}
} else {
header('Location: inversiones_operativas.php');
exit;
}
?>

View File

@ -0,0 +1,95 @@
<?php
require_once 'includes/header.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: liquidaciones.php');
exit();
}
// Validar que el usuario sea administrador
if (!isset($_SESSION['user_rol']) || ($_SESSION['user_rol'] !== 'admin' && $_SESSION['user_rol'] !== 'administrador')) {
header('Location: login.php?error=unauthorized');
exit();
}
// Validar campos
$movimiento_id = $_POST['movimiento_id'] ?? null;
$producto_id = $_POST['producto_id'] ?? null;
$ciudad_id = $_POST['ciudad_id'] ?? null;
$cantidad = $_POST['cantidad'] ?? null;
$cantidad_pedidos = $_POST['cantidad_pedidos'] ?? null;
$precio_liquidacion = $_POST['precio_liquidacion'] ?? null;
$fecha = $_POST['fecha'] ?? null;
if (empty($movimiento_id) || empty($producto_id) || empty($ciudad_id) || empty($cantidad) || !isset($cantidad_pedidos) || !isset($precio_liquidacion) || empty($fecha)) {
header('Location: editar_liquidacion.php?id=' . $movimiento_id . '&error=missing_fields');
exit();
}
$pdo = db();
// Iniciar transacción
$pdo->beginTransaction();
try {
// 1. Obtener el movimiento original para revertir el stock
$stmt = $pdo->prepare("SELECT producto_id, ciudad_origen_id, cantidad FROM movimientos WHERE id = ?");
$stmt->execute([$movimiento_id]);
$movimiento_original = $stmt->fetch(PDO::FETCH_ASSOC);
if ($movimiento_original) {
// Revertir el stock del producto original en la ciudad de origen
$stmt_revert = $pdo->prepare("UPDATE stock_por_ciudad SET stock_actual = stock_actual + ? WHERE producto_id = ? AND ciudad_id = ?");
$stmt_revert->execute([$movimiento_original['cantidad'], $movimiento_original['producto_id'], $movimiento_original['ciudad_origen_id']]);
}
// 2. Verificar si hay suficiente stock para el nuevo producto/cantidad en la nueva ciudad
$stmt_stock = $pdo->prepare("SELECT stock_actual FROM stock_por_ciudad WHERE producto_id = ? AND ciudad_id = ?");
$stmt_stock->execute([$producto_id, $ciudad_id]);
$stock_actual = $stmt_stock->fetchColumn();
if ($stock_actual === false || $stock_actual < $cantidad) {
// Si no hay suficiente stock, revertir la transacción y redirigir con error
$pdo->rollBack();
header('Location: editar_liquidacion.php?id=' . $movimiento_id . '&error=no_stock');
exit();
}
// 3. Actualizar el movimiento
$sql = "UPDATE movimientos
SET producto_id = ?,
ciudad_origen_id = ?,
cantidad = ?,
cantidad_pedidos = ?,
precio_liquidacion = ?,
fecha = ?
WHERE id = ?";
$stmt_update = $pdo->prepare($sql);
$stmt_update->execute([
$producto_id,
$ciudad_id,
$cantidad,
$cantidad_pedidos,
$precio_liquidacion,
$fecha,
$movimiento_id
]);
// 4. Actualizar el stock para el nuevo producto/ciudad
$stmt_update_stock = $pdo->prepare("UPDATE stock_por_ciudad SET stock_actual = stock_actual - ? WHERE producto_id = ? AND ciudad_id = ?");
$stmt_update_stock->execute([$cantidad, $producto_id, $ciudad_id]);
// Confirmar la transacción
$pdo->commit();
header('Location: liquidaciones.php?success=updated');
exit();
} catch (Exception $e) {
// Si hay un error, revertir la transacción
$pdo->rollBack();
// Log del error (opcional)
// error_log($e->getMessage());
header('Location: editar_liquidacion.php?id=' . $movimiento_id . '&error=db_error');
exit();
}

View File

@ -0,0 +1,40 @@
<?php
session_start();
if (!isset($_SESSION["user_id"])) {
header("Location: auth/login.php");
exit();
}
require_once 'db/config.php';
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$product_id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
$nombre = trim($_POST['nombre']);
$descripcion = trim($_POST['descripcion']);
$costo = filter_input(INPUT_POST, 'costo', FILTER_VALIDATE_FLOAT);
$precio_venta = filter_input(INPUT_POST, 'precio_venta', FILTER_VALIDATE_FLOAT);
if ($product_id > 0 && !empty($nombre) && $costo !== false && $precio_venta !== false) {
try {
$pdo = db();
$sql = "UPDATE productos SET nombre = ?, descripcion = ?, costo = ?, precio_venta = ? WHERE id = ?";
$stmt = $pdo->prepare($sql);
if ($stmt->execute([$nombre, $descripcion, $costo, $precio_venta, $product_id])) {
$_SESSION['success_message'] = "Producto actualizado correctamente.";
} else {
$_SESSION['error_message'] = "Error al actualizar el producto.";
}
} catch (PDOException $e) {
$_SESSION['error_message'] = "Error de base de datos: " . $e->getMessage();
}
} else {
$_SESSION['error_message'] = "Por favor, complete todos los campos requeridos correctamente.";
}
} else {
$_SESSION['error_message'] = "Método de solicitud no válido.";
}
header("Location: productos.php");
exit();
?>

76
handle_entrada.php Normal file
View File

@ -0,0 +1,76 @@
<?php
date_default_timezone_set('America/Lima');
require_once 'db/config.php';
if (!is_logged_in()) {
$_SESSION['error'] = "Debe iniciar sesión para realizar esta acción.";
header("Location: auth/login.php");
exit();
}
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$producto_id = $_POST['producto_id'] ?? null;
$ciudad_id = $_POST['ciudad_id'] ?? null;
$cantidad = $_POST['cantidad'] ?? null;
$observacion = $_POST['observacion'] ?? '';
$usuario_id = get_current_user_id();
if (empty($producto_id) || empty($ciudad_id) || empty($cantidad) || !is_numeric($cantidad) || $cantidad <= 0) {
$_SESSION['error'] = "Por favor, complete todos los campos correctamente.";
header("Location: registrar_entrada.php");
exit();
}
$pdo = db();
try {
$pdo->beginTransaction();
// 1. Verificar si ya existe stock para este producto en esta ciudad
$stmt = $pdo->prepare("SELECT id FROM stock_por_ciudad WHERE producto_id = :producto_id AND ciudad_id = :ciudad_id FOR UPDATE");
$stmt->execute(['producto_id' => $producto_id, 'ciudad_id' => $ciudad_id]);
$stock_id = $stmt->fetchColumn();
// 2. Actualizar o insertar el stock
if ($stock_id) {
// Si existe, actualizar
$stmt = $pdo->prepare("UPDATE stock_por_ciudad SET stock_actual = stock_actual + :cantidad WHERE id = :stock_id");
$stmt->execute(['cantidad' => $cantidad, 'stock_id' => $stock_id]);
} else {
// Si no existe, insertar
$stmt = $pdo->prepare("INSERT INTO stock_por_ciudad (producto_id, ciudad_id, stock_actual) VALUES (:producto_id, :ciudad_id, :stock_actual)");
$stmt->execute(['producto_id' => $producto_id, 'ciudad_id' => $ciudad_id, 'stock_actual' => $cantidad]);
}
// 3. Registrar el movimiento de entrada
$fecha = $_POST['fecha'];
$stmt = $pdo->prepare(
"INSERT INTO movimientos (tipo, producto_id, ciudad_destino_id, cantidad, usuario_id, observacion, fecha, fecha_registro)
VALUES ('Entrada', :producto_id, :ciudad_destino_id, :cantidad, :usuario_id, :observacion, :fecha, NOW())"
);
$stmt->execute([
'producto_id' => $producto_id,
'ciudad_destino_id' => $ciudad_id,
'cantidad' => $cantidad,
'usuario_id' => $usuario_id,
'observacion' => $observacion,
'fecha' => $fecha
]);
$pdo->commit();
$_SESSION['success'] = "Entrada de stock registrada correctamente.";
header("Location: productos.php");
exit();
} catch (Exception $e) {
$pdo->rollBack();
$_SESSION['error'] = "Error al registrar la entrada: " . $e->getMessage();
header("Location: registrar_entrada.php");
exit();
}
} else {
header("Location: registrar_entrada.php");
exit();
}
?>

24
handle_flujo_de_caja.php Normal file
View File

@ -0,0 +1,24 @@
<?php
require_once 'db/config.php';
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$fecha = $_POST['fecha'];
$bcp_yape = $_POST['bcp_yape'] ?? 0;
$banco_nacion = $_POST['banco_nacion'] ?? 0;
$interbank = $_POST['interbank'] ?? 0;
$bbva = $_POST['bbva'] ?? 0;
$otros = $_POST['otros'] ?? 0;
try {
$pdo = db();
$stmt = $pdo->prepare('INSERT INTO flujo_de_caja (fecha, bcp_yape, banco_nacion, interbank, bbva, otros) VALUES (?, ?, ?, ?, ?, ?)');
$stmt->execute([$fecha, $bcp_yape, $banco_nacion, $interbank, $bbva, $otros]);
header('Location: flujo_de_caja.php?status=success');
} catch (PDOException $e) {
// Log error or handle it appropriately
header('Location: flujo_de_caja.php?status=error');
}
} else {
header('Location: flujo_de_caja.php');
}
?>

32
handle_move_card.php Normal file
View File

@ -0,0 +1,32 @@
<?php
require_once 'db/config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$card_id = $_POST['card_id'] ?? null;
$column_id = $_POST['column_id'] ?? null;
if ($card_id && $column_id) {
$pdo = db();
// Update the card's column
$stmt = $pdo->prepare('UPDATE info_productos SET column_id = :column_id WHERE id = :card_id');
$stmt->execute([':column_id' => $column_id, ':card_id' => $card_id]);
// Optional: Handle card order within the new column
// For simplicity, we are not reordering, but you could add that logic here.
// For example, set the moved card to be the last in the new column.
$stmt_max_order = $pdo->prepare('SELECT MAX(orden) FROM info_productos WHERE column_id = :column_id');
$stmt_max_order->execute([':column_id' => $column_id]);
$max_order = $stmt_max_order->fetchColumn() ?? 0;
$stmt_order = $pdo->prepare('UPDATE info_productos SET orden = :orden WHERE id = :card_id');
$stmt_order->execute([':orden' => $max_order + 1, ':card_id' => $card_id]);
$_SESSION['success_message'] = 'Tarjeta movida exitosamente.';
} else {
$_SESSION['error_message'] = 'Faltan datos para mover la tarjeta.';
}
}
header('Location: kanban.php');
exit;

42
handle_orden_ciudades.php Normal file
View File

@ -0,0 +1,42 @@
<?php
session_start();
require_once 'db/config.php';
// Proteger contra acceso no autorizado
if (!isset($_SESSION['user_rol']) || $_SESSION['user_rol'] !== 'Administrador General') {
$_SESSION['error_message'] = 'No tienes permiso para realizar esta acción.';
header('Location: ciudades.php');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['orden'])) {
$orden_array = $_POST['orden'];
$pdo = db();
try {
$pdo->beginTransaction();
$stmt = $pdo->prepare('UPDATE ciudades SET orden = :orden WHERE id = :id');
foreach ($orden_array as $id => $orden) {
$stmt->execute([
'orden' => (int)$orden,
'id' => (int)$id
]);
}
$pdo->commit();
$_SESSION['success_message'] = 'El orden de las ciudades ha sido actualizado correctamente.';
} catch (Exception $e) {
$pdo->rollBack();
$_SESSION['error_message'] = 'Error al actualizar el orden: ' . $e->getMessage();
}
} else {
$_SESSION['error_message'] = 'Petición no válida.';
}
header('Location: ciudades.php');
exit;
?>

View File

@ -0,0 +1,68 @@
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: auth/login.php');
exit;
}
require_once 'db/config.php';
if (!isset($_GET['id']) || !isset($_GET['dir'])) {
header('Location: resumen_stock.php');
exit;
}
$producto_id = (int)$_GET['id'];
$direction = $_GET['dir'];
$pdo = db();
$pdo->beginTransaction();
try {
// Obtener el orden actual del producto a mover
$stmt = $pdo->prepare("SELECT orden FROM productos WHERE id = :id");
$stmt->execute(['id' => $producto_id]);
$current_orden = $stmt->fetchColumn();
if ($current_orden === false) {
throw new Exception("Producto no encontrado.");
}
$producto_a_intercambiar = null;
if ($direction === 'up') {
// Mover hacia arriba (menor valor de 'orden')
$stmt = $pdo->prepare("SELECT id, orden FROM productos WHERE orden < :orden ORDER BY orden DESC LIMIT 1");
$stmt->execute(['orden' => $current_orden]);
$producto_a_intercambiar = $stmt->fetch(PDO::FETCH_ASSOC);
} elseif ($direction === 'down') {
// Mover hacia abajo (mayor valor de 'orden')
$stmt = $pdo->prepare("SELECT id, orden FROM productos WHERE orden > :orden ORDER BY orden ASC LIMIT 1");
$stmt->execute(['orden' => $current_orden]);
$producto_a_intercambiar = $stmt->fetch(PDO::FETCH_ASSOC);
}
if ($producto_a_intercambiar) {
// Intercambiar los valores de 'orden'
$nuevo_orden_actual = $producto_a_intercambiar['orden'];
$nuevo_orden_otro = $current_orden;
$stmt = $pdo->prepare("UPDATE productos SET orden = :orden WHERE id = :id");
// Actualizar el producto que se mueve
$stmt->execute(['orden' => $nuevo_orden_actual, 'id' => $producto_id]);
// Actualizar el otro producto
$stmt->execute(['orden' => $nuevo_orden_otro, 'id' => $producto_a_intercambiar['id']]);
}
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
// Opcional: registrar el error o mostrar un mensaje
// error_log($e->getMessage());
}
header('Location: resumen_stock.php');
exit;

43
handle_pago_ciudad.php Normal file
View File

@ -0,0 +1,43 @@
<?php
require_once __DIR__ . '/db/config.php';
header('Content-Type: application/json');
$response = ['success' => false, 'message' => 'Petición inválida.'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = json_decode(file_get_contents('php://input'), true);
$liquidacion_id = $data['liquidacion_id'] ?? null;
$ciudad_id = $data['ciudad_id'] ?? null;
$fecha = $data['fecha'] ?? null;
$estado = $data['estado'] ?? null;
if ($liquidacion_id && $ciudad_id && $fecha && $estado) {
if (!in_array($estado, ['Pendiente', 'Pagado'])) {
$response['message'] = 'Estado no válido.';
} else {
try {
$pdo = db();
$sql = "INSERT INTO liquidaciones_ciudades_estados (liquidacion_id, ciudad_id, fecha, estado)
VALUES (:liquidacion_id, :ciudad_id, :fecha, :estado)
ON DUPLICATE KEY UPDATE estado = VALUES(estado)";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':liquidacion_id' => $liquidacion_id,
':ciudad_id' => $ciudad_id,
':fecha' => $fecha,
':estado' => $estado
]);
$response['success'] = true;
$response['message'] = 'Estado actualizado correctamente.';
} catch (PDOException $e) {
$response['message'] = 'Error al actualizar la base de datos: ' . $e->getMessage();
}
}
} else {
$response['message'] = 'Faltan datos para realizar la operación.';
}
}
echo json_encode($response);

19
handle_rename_column.php Normal file
View File

@ -0,0 +1,19 @@
<?php
require_once 'db/config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$column_id = $_POST['column_id'] ?? null;
$column_name = trim($_POST['column_name']);
if ($column_id && !empty($column_name)) {
$pdo = db();
$stmt = $pdo->prepare('UPDATE kanban_columns SET nombre = :nombre WHERE id = :id');
$stmt->execute([':nombre' => $column_name, ':id' => $column_id]);
$_SESSION['success_message'] = 'Columna renombrada exitosamente.';
} else {
$_SESSION['error_message'] = 'Faltan datos para renombrar la columna.';
}
}
header('Location: kanban.php');
exit;

81
handle_salida.php Normal file
View File

@ -0,0 +1,81 @@
<?php
date_default_timezone_set('America/Lima');
require_once 'db/config.php';
// Asegurarse de que el usuario esté logueado
if (!isset($_SESSION['user_id'])) {
$_SESSION['error'] = "Debe iniciar sesión para realizar esta acción.";
header("Location: auth/login.php");
exit();
}
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$producto_id = $_POST['producto_id'];
$ciudad_id = $_POST['ciudad_id'];
$cantidad = $_POST['cantidad'];
$observacion = $_POST['descripcion']; // El campo del formulario se llama 'descripcion'
$usuario_id = $_SESSION['user_id'];
$fecha = $_POST['fecha'];
if (empty($producto_id) || empty($ciudad_id) || empty($cantidad) || $cantidad <= 0) {
$_SESSION['error'] = "Por favor, complete todos los campos obligatorios.";
header("Location: registrar_salida.php");
exit();
}
$pdo = db();
try {
$pdo->beginTransaction();
// 1. Verificar stock actual
$stmt = $pdo->prepare("SELECT stock_actual FROM stock_por_ciudad WHERE producto_id = :producto_id AND ciudad_id = :ciudad_id FOR UPDATE");
$stmt->execute(['producto_id' => $producto_id, 'ciudad_id' => $ciudad_id]);
$stock_disponible = $stmt->fetchColumn();
if ($stock_disponible === false || $stock_disponible < $cantidad) {
$_SESSION['error'] = "No hay stock suficiente para realizar esta operación. Stock actual: " . ($stock_disponible ?: 0);
header("Location: registrar_salida.php");
$pdo->rollBack();
exit();
}
// 2. Actualizar el stock en la ciudad
$stmt = $pdo->prepare("UPDATE stock_por_ciudad SET stock_actual = stock_actual - :cantidad WHERE producto_id = :producto_id AND ciudad_id = :ciudad_id");
$stmt->execute([
'cantidad' => $cantidad,
'producto_id' => $producto_id,
'ciudad_id' => $ciudad_id
]);
// 3. Registrar el movimiento de salida
$stmt = $pdo->prepare(
"INSERT INTO movimientos (tipo, producto_id, ciudad_origen_id, cantidad, usuario_id, observacion, fecha, fecha_registro)
VALUES ('Salida', :producto_id, :ciudad_origen_id, :cantidad, :usuario_id, :observacion, :fecha, NOW())"
);
$stmt->execute([
'producto_id' => $producto_id,
'ciudad_origen_id' => $ciudad_id,
'cantidad' => $cantidad,
'usuario_id' => $usuario_id,
'observacion' => $observacion,
'fecha' => $fecha
]);
$pdo->commit();
$_SESSION['success'] = "Salida de producto registrada correctamente.";
header("Location: productos.php");
exit();
} catch (Exception $e) {
$pdo->rollBack();
$_SESSION['error'] = "Error al registrar la salida: " . $e->getMessage();
header("Location: registrar_salida.php");
exit();
}
} else {
header("Location: registrar_salida.php");
exit();
}
?>

186
historial.php Normal file
View File

@ -0,0 +1,186 @@
<?php
date_default_timezone_set('America/Lima');
require_once 'includes/header.php';
require_once 'db/config.php'; // Conexión a la base de datos
// Obtener todas las ciudades para el filtro
try {
$stmt_ciudades = db()->query("SELECT id, nombre FROM ciudades ORDER BY nombre");
$ciudades = $stmt_ciudades->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
die("Error al obtener las ciudades: " . $e->getMessage());
}
// Obtener todos los productos para el filtro
try {
$stmt_productos = db()->query("SELECT id, nombre FROM productos ORDER BY nombre");
$productos = $stmt_productos->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
die("Error al obtener los productos: " . $e->getMessage());
}
// Filtrar por ciudad y/o producto si se han seleccionado
$ciudad_seleccionada_id = isset($_GET['ciudad_id']) ? (int)$_GET['ciudad_id'] : 0;
$producto_seleccionado_id = isset($_GET['producto_id']) ? (int)$_GET['producto_id'] : 0;
// Consulta para obtener el historial de movimientos
try {
$sql = "
SELECT
m.id,
p.nombre AS producto_nombre,
m.tipo AS tipo_movimiento,
m.cantidad,
COALESCE(c_origen.nombre, c_destino.nombre) AS ciudad_nombre,
u.nombre AS usuario_nombre,
m.fecha AS fecha_movimiento,
m.fecha_registro,
m.observacion
FROM
movimientos m
JOIN
productos p ON m.producto_id = p.id
LEFT JOIN
ciudades c_origen ON m.ciudad_origen_id = c_origen.id
LEFT JOIN
ciudades c_destino ON m.ciudad_destino_id = c_destino.id
JOIN
usuarios u ON m.usuario_id = u.id
";
$conditions = [];
$params = [];
if ($ciudad_seleccionada_id > 0) {
$conditions[] = "(c_origen.id = :ciudad_id OR c_destino.id = :ciudad_id)";
$params[':ciudad_id'] = $ciudad_seleccionada_id;
}
if ($producto_seleccionado_id > 0) {
$conditions[] = "m.producto_id = :producto_id";
$params[':producto_id'] = $producto_seleccionado_id;
}
if (!empty($conditions)) {
$sql .= " WHERE " . implode(" AND ", $conditions);
}
$sql .= " ORDER BY m.fecha_registro DESC";
$stmt = db()->prepare($sql);
$stmt->execute($params);
$movimientos = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
// En caso de un error de base de datos, muestra un mensaje en lugar de una página en blanco
die("Error al obtener el historial de movimientos: " . $e->getMessage());
}
?>
<div class="container mt-5">
<h2>Historial de Movimientos de Stock</h2>
<hr>
<!-- Filtros -->
<form action="historial.php" method="get" class="mb-4">
<div class="form-row align-items-end">
<div class="col-md-4">
<label for="ciudad_id">Filtrar por Ciudad:</label>
<select name="ciudad_id" id="ciudad_id" class="form-control">
<option value="0">Todas las ciudades</option>
<?php foreach ($ciudades as $ciudad): ?>
<option value="<?php echo $ciudad['id']; ?>" <?php echo ($ciudad_seleccionada_id == $ciudad['id']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($ciudad['nombre']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label for="producto_id">Filtrar por Producto:</label>
<select name="producto_id" id="producto_id" class="form-control">
<option value="0">Todos los productos</option>
<?php foreach ($productos as $producto): ?>
<option value="<?php echo $producto['id']; ?>" <?php echo ($producto_seleccionado_id == $producto['id']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($producto['nombre']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary mt-4">Filtrar</button>
</div>
</div>
</form>
<div class="table-responsive">
<table class="table table-striped table-bordered table-hover">
<thead class="thead-dark">
<tr>
<th>Producto</th>
<th>Tipo</th>
<th>Cantidad</th>
<th>Ciudad</th>
<th>Usuario</th>
<th>Fecha Movimiento</th>
<th>Fecha Registro</th>
<th>Observación</th>
</tr>
</thead>
<tbody>
<?php if (!empty($movimientos)): ?>
<?php foreach ($movimientos as $movimiento): ?>
<tr>
<td><?php echo htmlspecialchars($movimiento['producto_nombre']); ?></td>
<td>
<?php
$tipo = $movimiento['tipo_movimiento'];
$clase_color = '';
if ($tipo == 'Entrada') {
$clase_color = 'bg-success';
} elseif ($tipo == 'Salida') {
$clase_color = 'bg-danger';
} else {
$clase_color = 'bg-secondary';
}
echo '<span class="badge ' . $clase_color . '">' . htmlspecialchars($tipo) . '</span>';
?>
</td>
<td><?php echo htmlspecialchars($movimiento['cantidad']); ?></td>
<td><?php echo htmlspecialchars($movimiento['ciudad_nombre']); ?></td>
<td><?php echo htmlspecialchars($movimiento['usuario_nombre']); ?></td>
<td>
<?php
try {
$fecha = new DateTime($movimiento['fecha_movimiento']);
echo htmlspecialchars($fecha->format('d/m/Y'));
} catch (Exception $e) {
echo htmlspecialchars($movimiento['fecha_movimiento']);
}
?>
</td>
<td>
<?php
try {
$fecha = new DateTime($movimiento['fecha_registro'], new DateTimeZone('UTC'));
$fecha->setTimezone(new DateTimeZone('America/Lima'));
echo htmlspecialchars($fecha->format('d/m/Y H:i'));
} catch (Exception $e) {
echo htmlspecialchars($movimiento['fecha_registro']);
}
?>
</td>
<td><?php echo htmlspecialchars($movimiento['observacion']); ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="8" class="text-center">No hay movimientos registrados para los filtros seleccionados.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php
require_once 'includes/footer.php';
?>

10
includes/footer.php Normal file
View File

@ -0,0 +1,10 @@
</div>
</div>
</div>
<!-- Scripts JS de Bootstrap y personalizados -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

118
includes/header.php Normal file
View File

@ -0,0 +1,118 @@
<?php
date_default_timezone_set('America/Lima');
require_once __DIR__ . '/../db/config.php';
// Validar sesión activa
if (!is_logged_in()) {
header('Location: /auth/login.php');
exit;
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sistema de Inventario</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<link rel="stylesheet" href="/assets/css/style.css?v=<?php echo time(); ?>">
</head>
<body>
<div class="d-flex">
<!-- Menú Lateral (Sidebar) -->
<div class="sidebar">
<h3 class="sidebar-heading">Panel de control</h3>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" href="/">
<i class="fas fa-tachometer-alt"></i> Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/productos.php">
<i class="fas fa-box"></i> Productos
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/ciudades.php">
<i class="fas fa-city"></i> Ciudades
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/colaboradores.php">
<i class="fas fa-users"></i> Colaboradores
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/registrar_entrada.php">
<i class="fas fa-plus-circle"></i> Registrar Entrada
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/registrar_salida.php">
<i class="fas fa-minus-circle"></i> Registrar Salida
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/historial.php">
<i class="fas fa-history"></i> Historial
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/resumen_stock.php">
<i class="fas fa-clipboard-list"></i> Resumen de Stock
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/liquidaciones.php">
<i class="fas fa-dollar-sign"></i> Liquidaciones
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/liquidaciones_por_fecha.php">
<i class="fas fa-calendar-alt"></i> Liquidaciones por Fecha
</a>
</li>
<?php if ($_SESSION['user_rol'] !== 'Encargado de Stock') : ?>
<li class="nav-item">
<a class="nav-link" href="/reporte_de_pagos.php">
<i class="fas fa-chart-bar"></i> Validaciones De Pagos
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/flujo_de_caja.php">
<i class="fas fa-wallet"></i> Flujo de Caja
</a>
</li>
<?php endif; ?>
<?php if (isset($_SESSION['user_rol']) && $_SESSION['user_rol'] === 'Administrador General') : ?>
<li class="nav-item">
<a class="nav-link" href="/inversiones_operativas.php">
<i class="fas fa-chart-line"></i> Gestión de Inversiones
</a>
</li>
<?php endif; ?>
<?php if ($_SESSION['user_rol'] !== 'Encargado de Stock') : ?>
<?php endif; ?>
<li class="nav-item">
<a class="nav-link" href="/confirmacion_stock.php">
<i class="fas fa-truck"></i> Reporte de Entregas
</a>
</li>
<li class="nav-item mt-auto">
<a class="nav-link" href="/auth/logout.php">
<i class="fas fa-sign-out-alt"></i> Cerrar Sesión
</a>
</li>
</ul>
</div>
<!-- Contenido Principal -->
<div class="main-content">
<div class="container-fluid">

400
index.php
View File

@ -1,150 +1,258 @@
<?php <?php
declare(strict_types=1); $page_title = 'Panel de Control';
@ini_set('display_errors', '1'); require_once __DIR__ . '/includes/header.php';
@error_reporting(E_ALL); require_once __DIR__ . '/db/config.php';
@date_default_timezone_set('UTC');
// Proteger la página: si el usuario no ha iniciado sesión, redirigir a login
if (!is_logged_in()) {
header('Location: /auth/login.php');
exit();
}
// Obtener el número total de productos
$pdo = db();
$stmt = $pdo->query("SELECT COUNT(*) FROM productos");
$total_products = $stmt->fetchColumn();
// Obtener stock por ciudad y producto, ordenando las ciudades por stock total
$stock_stmt = $pdo->query("
SELECT
c.nombre AS ciudad,
p.nombre AS producto,
s.stock_actual
FROM
stock_por_ciudad s
JOIN
ciudades c ON s.ciudad_id = c.id
JOIN
productos p ON s.producto_id = p.id
JOIN
(SELECT ciudad_id, SUM(stock_actual) as total_stock FROM stock_por_ciudad GROUP BY ciudad_id) as stock_totales
ON c.id = stock_totales.ciudad_id
WHERE
s.stock_actual > 0
ORDER BY
stock_totales.total_stock DESC, p.nombre;
");
$stock_data = $stock_stmt->fetchAll(PDO::FETCH_ASSOC);
// Agrupar por ciudad
$stock_por_ciudad = [];
foreach ($stock_data as $row) {
$stock_por_ciudad[$row['ciudad']][] = [
'producto' => $row['producto'],
'stock' => $row['stock_actual']
];
}
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
?> ?>
<!doctype html>
<html lang="en"> <div class="container-fluid">
<head> <!-- Mensaje de Bienvenida -->
<meta charset="utf-8" /> <div class="row mb-4">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <div class="col-12 text-center">
<title>New Style</title> <h1 class="welcome-title">Bienvenido a tu Panel de Control</h1>
<?php <p class="welcome-subtitle">Aquí tienes un resumen de tu aplicación.</p>
// Read project preview data from environment </div>
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<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=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
</head>
<body>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
</div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div> </div>
</main>
<footer> <div class="row justify-content-center">
Page updated: <?= htmlspecialchars($now) ?> (UTC) <!-- Tarjeta de Total de Productos -->
</footer> <div class="col-md-6 col-lg-4 mb-4">
</body> <div class="card info-card h-100">
</html> <div class="card-body">
<div class="d-flex align-items-center">
<div class="icon-circle">
<!-- Icono de Caja (Productos) -->
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-box"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>
</div>
<div class="ms-3">
<h5 class="card-title">Total de Productos</h5>
<p class="card-text"><?php echo $total_products; ?></p>
</div>
</div>
</div>
</div>
</div>
<!-- Tarjeta de Acceso a Productos -->
<div class="col-md-6 col-lg-4 mb-4">
<a href="productos.php" class="card-link">
<div class="card info-card h-100">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="icon-circle">
<!-- Icono de Tareas (Administrar) -->
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-settings"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
</div>
<div class="ms-3">
<h5 class="card-title">Administrar Productos</h5>
<p class="card-text">Ir a la lista de productos</p>
</div>
</div>
</div>
</div>
</a>
</div>
<!-- Tarjeta de Gráfico de Stock -->
<div class="col-md-12 col-lg-4 mb-4">
<div class="card info-card h-100">
<div class="card-body">
<h5 class="card-title text-center">Distribución de Stock</h5>
<div style="height: 300px;">
<canvas id="stockChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="row justify-content-center">
<!-- Tarjeta de Stock por Ciudad y Producto -->
<div class="col-md-12 col-lg-8 mb-4">
<div class="card info-card h-100">
<div class="card-body">
<h5 class="card-title">Stock por Ciudad y Producto</h5>
<div class="accordion" id="stockAccordion">
<?php if (empty($stock_por_ciudad)): ?>
<p class="text-center mt-3">No hay datos de stock disponibles.</p>
<?php else: ?>
<?php
$i = 0;
foreach ($stock_por_ciudad as $ciudad => $productos):
$ciudad_id = 'ciudad_' . $i++;
?>
<div class="accordion-item">
<h2 class="accordion-header" id="heading-<?php echo $ciudad_id; ?>">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-<?php echo $ciudad_id; ?>" aria-expanded="false" aria-controls="collapse-<?php echo $ciudad_id; ?>">
<?php echo htmlspecialchars($ciudad); ?>
</button>
</h2>
<div id="collapse-<?php echo $ciudad_id; ?>" class="accordion-collapse collapse" aria-labelledby="heading-<?php echo $ciudad_id; ?>" data-bs-parent="#stockAccordion">
<div class="accordion-body">
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr>
<th>Producto</th>
<th>Stock</th>
</tr>
</thead>
<tbody>
<?php foreach ($productos as $producto): ?>
<tr>
<td><?php echo htmlspecialchars($producto['producto']); ?></td>
<td><?php echo htmlspecialchars($producto['stock']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
</div>
<?php
// Preparar datos para el gráfico
$chart_labels = [];
$chart_data = [];
$chart_bg_colors = []; // Array para los colores de fondo
$chart_border_colors = []; // Array para los colores de borde
// Paleta de colores (puedes añadir más si tienes muchas ciudades)
$color_palette_bg = [
'rgba(54, 162, 235, 0.8)', // Azul
'rgba(255, 206, 86, 0.8)', // Amarillo
'rgba(75, 192, 192, 0.8)', // Turquesa
'rgba(153, 102, 255, 0.8)',// Morado
'rgba(255, 99, 132, 0.8)', // Rojo
'rgba(255, 159, 64, 0.8)' // Naranja
];
$color_palette_border = [
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 99, 132, 1)',
'rgba(255, 159, 64, 1)'
];
// Asignar verde a Lima
$city_color_map = [
'Lima' => [
'bg' => 'rgba(40, 167, 69, 0.8)', // Verde fondo
'border' => 'rgba(40, 167, 69, 1)' // Verde borde
]
];
if (!empty($stock_por_ciudad)) {
$color_index = 0;
foreach ($stock_por_ciudad as $ciudad => $productos) {
$chart_labels[] = $ciudad;
// Asignar color
if (isset($city_color_map[$ciudad])) {
$chart_bg_colors[] = $city_color_map[$ciudad]['bg'];
$chart_border_colors[] = $city_color_map[$ciudad]['border'];
} else {
// Asignar un color de la paleta
$chart_bg_colors[] = $color_palette_bg[$color_index % count($color_palette_bg)];
$chart_border_colors[] = $color_palette_border[$color_index % count($color_palette_border)];
$color_index++;
}
$total_stock = 0;
foreach ($productos as $producto) {
$total_stock += $producto['stock'];
}
$chart_data[] = $total_stock;
}
}
?>
<script>
document.addEventListener('DOMContentLoaded', function () {
const ctx = document.getElementById('stockChart');
if (ctx && <?php echo json_encode(!empty($chart_data)); ?>) {
new Chart(ctx, {
type: 'pie',
data: {
labels: <?php echo json_encode($chart_labels); ?>,
datasets: [{
label: 'Stock Total',
data: <?php echo json_encode($chart_data); ?>,
backgroundColor: <?php echo json_encode($chart_bg_colors); ?>,
borderColor: <?php echo json_encode($chart_border_colors); ?>,
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom',
},
title: {
display: false,
}
}
}
});
} else if (ctx) {
ctx.getContext('2d').fillText("No hay datos de stock para mostrar.", ctx.width / 2 - 50, ctx.height / 2);
}
});
</script>
<?php
require_once __DIR__ . '/includes/footer.php';
?>

100
info_producto.php Normal file
View File

@ -0,0 +1,100 @@
<?php
require_once 'includes/header.php';
require_once 'db/config.php';
$pdo = db();
// Fetch products for the dropdown
$stmt_productos = $pdo->query('SELECT id, nombre FROM productos ORDER BY nombre ASC');
$productos = $stmt_productos->fetchAll(PDO::FETCH_ASSOC);
// Fetch Kanban columns for the dropdown
$stmt_columns = $pdo->query('SELECT id, nombre FROM kanban_columns ORDER BY orden ASC');
$columns = $stmt_columns->fetchAll(PDO::FETCH_ASSOC);
// If no columns exist, create default ones to ensure the dropdown is never empty
if (empty($columns)) {
$default_columns = ['Para empezar', 'En proceso', 'Terminado'];
$stmt_insert = $pdo->prepare('INSERT INTO kanban_columns (nombre, orden) VALUES (?, ?)');
foreach ($default_columns as $index => $name) {
$stmt_insert->execute([$name, $index + 1]);
}
// Re-fetch columns so they appear on this page load
$stmt_columns = $pdo->query('SELECT id, nombre FROM kanban_columns ORDER BY orden ASC');
$columns = $stmt_columns->fetchAll(PDO::FETCH_ASSOC);
}
// Fetch existing info cards
$stmt_info = $pdo->query('SELECT info_productos.id, productos.nombre as producto_nombre, info_productos.imagen_url, info_productos.texto_informativo, kanban_columns.nombre as columna_nombre FROM info_productos JOIN productos ON info_productos.producto_id = productos.id LEFT JOIN kanban_columns ON info_productos.column_id = kanban_columns.id ORDER BY info_productos.created_at DESC');
$info_cards = $stmt_info->fetchAll(PDO::FETCH_ASSOC);
?>
<div class="container mt-4">
<h2>Info de Productos</h2>
<p>Crea y gestiona tarjetas con información y fotos de tus productos.</p>
<div class="card mb-4">
<div class="card-header">Agregar Nueva Tarjeta de Información</div>
<div class="card-body">
<form action="handle_agregar_info_producto.php" method="POST" enctype="multipart/form-data">
<div class="form-group">
<label for="producto_id">Producto</label>
<select class="form-control" id="producto_id" name="producto_id" required>
<option value="">Selecciona un producto</option>
<?php foreach ($productos as $producto): ?>
<option value="<?php echo htmlspecialchars($producto['id']); ?>">
<?php echo htmlspecialchars($producto['nombre']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group mt-3">
<label for="column_id">Columna Kanban</label>
<select class="form-control" id="column_id" name="column_id" required>
<option value="">Selecciona una columna</option>
<?php foreach ($columns as $column): ?>
<option value="<?php echo htmlspecialchars($column['id']); ?>">
<?php echo htmlspecialchars($column['nombre']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group mt-3">
<label for="imagen">Imagen del Producto</label>
<input type="file" class="form-control-file" id="imagen" name="imagen" accept="image/*" required>
</div>
<div class="form-group mt-3">
<label for="texto_informativo">Texto Informativo</label>
<textarea class="form-control" id="texto_informativo" name="texto_informativo" rows="3" required></textarea>
</div>
<button type="submit" class="btn btn-primary mt-3">Guardar Tarjeta</button>
</form>
</div>
</div>
<div class="row">
<?php if (empty($info_cards)): ?>
<div class="col">
<p class="text-center">Aún no has creado ninguna tarjeta de información.</p>
</div>
<?php else: ?>
<?php foreach ($info_cards as $card): ?>
<div class="col-md-4 mb-4">
<div class="card h-100">
<img src="<?php echo htmlspecialchars($card['imagen_url']); ?>" class="card-img-top" alt="Imagen de <?php echo htmlspecialchars($card['producto_nombre']); ?>">
<div class="card-body">
<h5 class="card-title"><?php echo htmlspecialchars($card['producto_nombre']); ?></h5>
<p class="card-text"><small class="text-muted">Columna: <?php echo htmlspecialchars($card['columna_nombre'] ?? 'Sin asignar'); ?></small></p>
<p class="card-text"><?php echo nl2br(htmlspecialchars($card['texto_informativo'])); ?></p>
</div>
<div class="card-footer">
<a href="eliminar_info_producto.php?id=<?php echo $card['id']; ?>" class="btn btn-danger btn-sm" onclick="return confirm('¿Estás seguro de que quieres eliminar esta tarjeta?');">Eliminar</a>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<?php require_once 'includes/footer.php'; ?>

223
inversiones_operativas.php Normal file
View File

@ -0,0 +1,223 @@
<?php
require_once 'includes/header.php';
require_once 'db/config.php';
// Redirigir si el usuario no tiene el rol de Administrador General
if (!isset($_SESSION['user_rol']) || $_SESSION['user_rol'] !== 'Administrador General') {
header('Location: /index.php?error=unauthorized');
exit;
}
$pdo = db();
// --- Manejar Formularios (Agregar Inversión) ---
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_investment'])) {
$fecha = $_POST['fecha'] ?? date('Y-m-d');
$descripcion = trim($_POST['descripcion'] ?? '');
$monto = $_POST['monto'] ?? 0;
$tipo = $_POST['tipo'] ?? '';
if (!empty($descripcion) && is_numeric($monto) && $monto > 0 && !empty($tipo)) {
try {
$stmt = $pdo->prepare("INSERT INTO inversiones (fecha, descripcion, monto, tipo) VALUES (?, ?, ?, ?)");
$stmt->execute([$fecha, $descripcion, $monto, $tipo]);
// Redirigir para evitar reenvío de formulario
header("Location: " . $_SERVER['PHP_SELF'] . "?success=true#section-" . $tipo);
exit;
} catch (PDOException $e) {
$error_message = "Error al guardar en la base de datos: " . $e->getMessage();
}
} else {
$error_message = "Por favor, complete todos los campos correctamente.";
}
}
// --- Filtrado por Mes ---
$selected_month = $_GET['mes'] ?? null;
$selected_year = $_GET['anio'] ?? null;
$filter_active = $selected_month && $selected_year;
// --- Obtener todos los datos de inversiones (con filtro si aplica) ---
try {
$sql = "SELECT * FROM inversiones";
$params = [];
if ($filter_active) {
$sql .= " WHERE MONTH(fecha) = ? AND YEAR(fecha) = ?";
$params = [$selected_month, $selected_year];
}
$sql .= " ORDER BY fecha DESC, id DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$all_inversiones = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
die("Error al obtener los datos: " . $e->getMessage());
}
// --- Obtener totales por mes (esto no se filtra) ---
try {
$stmt_totals = $pdo->query("
SELECT
DATE_FORMAT(fecha, '%Y') as anio,
DATE_FORMAT(fecha, '%m') as mes_num,
DATE_FORMAT(fecha, '%M %Y') as mes_nombre,
SUM(monto) as total_monto
FROM
inversiones
GROUP BY
anio, mes_num, mes_nombre
ORDER BY
anio DESC, mes_num DESC
");
$monthly_totals = $stmt_totals->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
die("Error al obtener los totales mensuales: " . $e->getMessage());
}
// Separar por tipo
$inversiones_operativas = array_filter($all_inversiones, fn($inv) => $inv['tipo'] === 'operativa');
$inversiones_operacionales = array_filter($all_inversiones, fn($inv) => $inv['tipo'] === 'operacional');
$inversiones_ads = array_filter($all_inversiones, fn($inv) => $inv['tipo'] === 'ads');
// Función para renderizar una sección de inversión
function render_investment_section($title, $type, $data, $description_label) {
$total = array_sum(array_column($data, 'monto'));
?>
<div class="container mt-5" id="section-<?= htmlspecialchars($type) ?>">
<h2><?= htmlspecialchars($title) ?></h2>
<div class="card">
<div class="card-body">
<!-- Formulario para agregar -->
<form method="POST" action="<?= $_SERVER['PHP_SELF'] ?>#section-<?= htmlspecialchars($type) ?>" class="mb-4">
<input type="hidden" name="tipo" value="<?= htmlspecialchars($type) ?>">
<div class="row align-items-end g-3">
<div class="col-md-3">
<label for="fecha_<?= htmlspecialchars($type) ?>" class="form-label">Fecha</label>
<input type="date" class="form-control" id="fecha_<?= htmlspecialchars($type) ?>" name="fecha" value="<?= date('Y-m-d') ?>" required>
</div>
<div class="col-md-5">
<label for="desc_<?= htmlspecialchars($type) ?>" class="form-label"><?= htmlspecialchars($description_label) ?></label>
<input type="text" class="form-control" id="desc_<?= htmlspecialchars($type) ?>" name="descripcion" required>
</div>
<div class="col-md-2">
<label for="monto_<?= htmlspecialchars($type) ?>" class="form-label">Monto (S/)</label>
<input type="number" step="0.01" class="form-control" id="monto_<?= htmlspecialchars($type) ?>" name="monto" required>
</div>
<div class="col-md-2">
<button type="submit" name="add_investment" class="btn btn-primary w-100">Agregar</button>
</div>
</div>
</form>
<!-- Tabla de datos -->
<div class="table-responsive">
<table class="table table-bordered table-striped table-hover">
<thead class="table-dark">
<tr>
<th style="width: 15%;">Fecha</th>
<th>Descripción</th>
<th style="width: 15%;">Monto (S/)</th>
<th style="width: 15%;">Acciones</th>
</tr>
</thead>
<tbody>
<?php if (empty($data)): ?>
<tr>
<td colspan="4" class="text-center">No hay registros.</td>
</tr>
<?php else: ?>
<?php foreach ($data as $item): ?>
<tr>
<td><?= htmlspecialchars(date("d/m/Y", strtotime($item['fecha']))) ?></td>
<td><?= htmlspecialchars($item['descripcion']) ?></td>
<td><?= number_format($item['monto'], 2) ?></td>
<td>
<a href="editar_inversion.php?id=<?= $item['id'] ?>" class="btn btn-warning btn-sm">Editar</a>
<a href="eliminar_inversion.php?id=<?= $item['id'] ?>" class="btn btn-danger btn-sm" onclick="return confirm('¿Estás seguro de que quieres eliminar este registro?');">Eliminar</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
<tfoot>
<tr class="table-dark fw-bold">
<td colspan="2" class="text-end">TOTAL:</td>
<td><?= number_format($total, 2) ?></td>
<td></td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
<?php
}
?>
<div class="container-fluid mt-4">
<h1>Gestión de Inversiones</h1>
<hr>
<?php if (isset($error_message)): ?>
<div class="alert alert-danger"><?= htmlspecialchars($error_message) ?></div>
<?php endif; ?>
<?php if (isset($_GET['success'])): ?>
<div class="alert alert-success">Registro guardado exitosamente.</div>
<?php endif; ?>
<!-- Resumen de Totales por Mes -->
<div class="container mt-4 mb-5">
<h2>Resumen de Inversiones por Mes</h2>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-hover">
<thead class="table-dark">
<tr>
<th>Mes</th>
<th>Monto Total (S/)</th>
</tr>
</thead>
<tbody>
<?php if (empty($monthly_totals)): ?>
<tr>
<td colspan="2" class="text-center">No hay datos para mostrar.</td>
</tr>
<?php else: ?>
<?php foreach ($monthly_totals as $total): ?>
<tr style="cursor: pointer;" onclick="window.location='inversiones_operativas.php?mes=<?= $total['mes_num'] ?>&anio=<?= $total['anio'] ?>'">
<td><?= htmlspecialchars(ucfirst($total['mes_nombre'])) ?></td>
<td><?= number_format($total['total_monto'], 2) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php if ($filter_active): ?>
<div class="container mb-4">
<div class="d-flex justify-content-between align-items-center">
<h3 class="mb-0">Mostrando Inversiones de: <strong><?= htmlspecialchars(ucfirst(strftime('%B %Y', mktime(0, 0, 0, $selected_month, 1, $selected_year)))) ?></strong></h3>
<a href="inversiones_operativas.php" class="btn btn-info">Mostrar Todos los Meses</a>
</div>
<hr>
</div>
<?php endif; ?>
</div>
<?php
// Renderizar las tres secciones
render_investment_section("Inversion Mercaderia", "operativa", $inversiones_operativas, "Descripción de la Inversión");
render_investment_section("Inversiones en Ads", "ads", $inversiones_ads, "Descripción del Gasto de Publicidad");
render_investment_section("Inversiones Operacionales", "operacional", $inversiones_operacionales, "Descripción del Gasto");
require_once 'includes/footer.php';
?>

244
liquidaciones.php Normal file
View File

@ -0,0 +1,244 @@
<?php
require_once 'includes/header.php';
// Lógica para manejar el formulario de actualización
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['precios'])) {
$pdo = db();
$success = false;
// Actualizar precios de liquidación
if (isset($_POST['precios'])) {
$stmt_precio = $pdo->prepare("UPDATE movimientos SET precio_liquidacion = :precio WHERE id = :id AND tipo = 'Salida'");
foreach ($_POST['precios'] as $movimiento_id => $precio) {
if (!empty($precio)) {
$stmt_precio->execute([':precio' => $precio, ':id' => $movimiento_id]);
$success = true;
}
}
}
// Actualizar cantidad de pedidos
if (isset($_POST['pedidos'])) {
$stmt_pedidos = $pdo->prepare("UPDATE movimientos SET cantidad_pedidos = :pedidos WHERE id = :id AND tipo = 'Salida'");
foreach ($_POST['pedidos'] as $movimiento_id => $pedidos) {
if (!empty($pedidos)) {
$stmt_pedidos->execute([':pedidos' => $pedidos, ':id' => $movimiento_id]);
$success = true;
}
}
}
if ($success) {
echo '<div class="alert alert-success">Liquidaciones actualizadas correctamente.</div>';
}
}
// Obtener todos los movimientos de salida
$pdo = db();
$stmt = $pdo->query("
SELECT
m.id,
m.fecha,
p.nombre as producto,
m.cantidad,
c.nombre as ciudad,
m.precio_liquidacion,
m.cantidad_pedidos
FROM movimientos m
JOIN productos p ON m.producto_id = p.id
JOIN ciudades c ON m.ciudad_origen_id = c.id
WHERE m.tipo = 'Salida'
ORDER BY DATE(m.fecha) DESC, c.nombre, p.nombre
");
$salidas = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Agrupar salidas por fecha
$salidas_por_fecha = [];
foreach ($salidas as $salida) {
$fecha = date('Y-m-d', strtotime($salida['fecha']));
$salidas_por_fecha[$fecha][] = $salida;
}
// Obtener productos y ciudades para el formulario
$productos = $pdo->query("SELECT id, nombre FROM productos ORDER BY nombre")->fetchAll();
$ciudades = $pdo->query("SELECT id, nombre FROM ciudades ORDER BY nombre")->fetchAll();
$user_rol = $_SESSION['user_rol'] ?? '';
?>
<h1 class="mb-4">Liquidaciones de Salidas</h1>
<?php
if (isset($_GET['success']) && $_GET['success'] === 'added') {
echo '<div class="alert alert-success">Liquidación agregada correctamente.</div>';
}
if (isset($_GET['error'])) {
$error_msg = 'Ocurrió un error.';
if ($_GET['error'] === 'missing_fields') {
$error_msg = 'Por favor, complete todos los campos.';
} elseif ($_GET['error'] === 'db_error') {
$error_msg = 'Error al guardar en la base de datos.';
} elseif ($_GET['error'] === 'no_stock') {
$error_msg = 'No hay stock suficiente para realizar esta operación.';
}
echo '<div class="alert alert-danger">'.$error_msg.'</div>';
}
?>
<p>Aquí puedes ver todas las salidas de productos y asignar un precio de liquidación y la cantidad de pedidos a cada una.</p>
<?php if ($user_rol === 'admin' || $user_rol === 'administrador'): ?>
<!-- Botón para abrir el modal -->
<button type="button" class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#agregarLiquidacionModal">
<i class="fas fa-plus"></i> Agregar Liquidación
</button>
<!-- Modal para agregar liquidación -->
<div class="modal fade" id="agregarLiquidacionModal" tabindex="-1" aria-labelledby="agregarLiquidacionModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="agregarLiquidacionModalLabel">Agregar Nueva Liquidación</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form action="handle_agregar_liquidacion.php" method="POST">
<div class="mb-3">
<label for="producto_id" class="form-label">Producto</label>
<select class="form-control" id="producto_id" name="producto_id" required>
<option value="">Seleccione un producto</option>
<?php foreach ($productos as $producto): ?>
<option value="<?php echo $producto['id']; ?>"><?php echo htmlspecialchars($producto['nombre']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="ciudad_id" class="form-label">Ciudad</label>
<select class="form-control" id="ciudad_id" name="ciudad_id" required>
<option value="">Seleccione una ciudad</option>
<?php foreach ($ciudades as $ciudad): ?>
<option value="<?php echo $ciudad['id']; ?>"><?php echo htmlspecialchars($ciudad['nombre']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="cantidad" class="form-label">Cantidad</label>
<input type="number" class="form-control" id="cantidad" name="cantidad" required>
</div>
<div class="mb-3">
<label for="cantidad_pedidos" class="form-label">Cantidad de Pedidos</label>
<input type="number" class="form-control" id="cantidad_pedidos" name="cantidad_pedidos" required>
</div>
<div class="mb-3">
<label for="precio_liquidacion" class="form-label">Precio Liquidación</label>
<input type="number" step="0.01" class="form-control" id="precio_liquidacion" name="precio_liquidacion" required>
</div>
<button type="submit" class="btn btn-primary">Guardar Liquidación</button>
</form>
</div>
</div>
</div>
</div>
<?php endif; ?>
<form method="POST" action="liquidaciones.php">
<?php if (empty($salidas_por_fecha)): ?>
<div class="alert alert-info">No hay salidas registradas para liquidar.</div>
<?php else: ?>
<div class="accordion" id="accordionLiquidaciones">
<?php foreach ($salidas_por_fecha as $fecha => $salidas_del_dia): ?>
<div class="accordion-item">
<h2 class="accordion-header" id="heading-<?php echo $fecha; ?>">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-<?php echo $fecha; ?>" aria-expanded="false" aria-controls="collapse-<?php echo $fecha; ?>">
<?php echo date('d/m/Y', strtotime($fecha)); ?> (<?php echo count($salidas_del_dia); ?> salidas)
</button>
</h2>
<div id="collapse-<?php echo $fecha; ?>" class="accordion-collapse collapse" aria-labelledby="heading-<?php echo $fecha; ?>" data-bs-parent="#accordionLiquidaciones">
<div class="accordion-body">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Producto</th>
<th>Ciudad</th>
<th>Cantidad de Pedidos</th>
<th>Cantidad</th>
<th>Precio Liquidación</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php
$total_pedidos_dia = 0;
$total_precio_dia = 0;
$totales_por_ciudad = [];
foreach ($salidas_del_dia as $salida):
$total_pedidos_dia += (int)$salida['cantidad_pedidos'];
$total_precio_dia += (float)$salida['precio_liquidacion'];
if (!isset($totales_por_ciudad[$salida['ciudad']])) {
$totales_por_ciudad[$salida['ciudad']] = ['pedidos' => 0, 'precio' => 0];
}
$totales_por_ciudad[$salida['ciudad']]['pedidos'] += (int)$salida['cantidad_pedidos'];
$totales_por_ciudad[$salida['ciudad']]['precio'] += (float)$salida['precio_liquidacion'];
?>
<tr>
<td><?php echo htmlspecialchars($salida['producto']); ?></td>
<td><?php echo htmlspecialchars($salida['ciudad']); ?></td>
<td>
<input type="number" step="1" name="pedidos[<?php echo $salida['id']; ?>]" class="form-control" value="<?php echo htmlspecialchars($salida['cantidad_pedidos']); ?>">
</td>
<td><?php echo $salida['cantidad']; ?></td>
<td>
<input type="number" step="0.01" name="precios[<?php echo $salida['id']; ?>]" class="form-control" value="<?php echo htmlspecialchars($salida['precio_liquidacion']); ?>">
</td>
<td>
<a href="editar_liquidacion.php?id=<?php echo $salida['id']; ?>" class="btn btn-sm btn-warning">
<i class="fas fa-edit"></i>
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr>
<td colspan="2" class="text-end"><strong>Total del día:</strong></td>
<td><strong><?php echo $total_pedidos_dia; ?></strong></td>
<td></td>
<td><strong><?php echo number_format($total_precio_dia, 2); ?></strong></td>
</tr>
</tfoot>
</table>
<h5 class="mt-4">Resumen de Liquidación por Ciudad</h5>
<table class="table table-sm table-bordered">
<thead>
<tr>
<th>Ciudad</th>
<th>Total Pedidos</th>
<th>Monto Total Liquidado</th>
</tr>
</thead>
<tbody>
<?php foreach ($totales_por_ciudad as $ciudad => $totales): ?>
<tr>
<td><?php echo htmlspecialchars($ciudad); ?></td>
<td><?php echo $totales['pedidos']; ?></td>
<td><?php echo number_format($totales['precio'], 2); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<button type="submit" class="btn btn-primary mt-3">Guardar Cambios</button>
<?php endif; ?>
</form>
<?php require_once 'includes/footer.php'; ?>

108
liquidaciones_por_fecha.php Normal file
View File

@ -0,0 +1,108 @@
<?php
require_once 'includes/header.php';
$fecha_inicio = isset($_GET['fecha_inicio']) ? $_GET['fecha_inicio'] : '';
$fecha_fin = isset($_GET['fecha_fin']) ? $_GET['fecha_fin'] : '';
$resultados = [];
if ($fecha_inicio && $fecha_fin) {
$pdo = db();
$stmt = $pdo->prepare("
SELECT
p.nombre AS producto,
c.nombre AS ciudad,
SUM(m.cantidad_pedidos) AS cantidad_pedidos,
SUM(m.cantidad) AS cantidad,
SUM(m.precio_liquidacion) AS precio_liquidaciones
FROM
movimientos m
JOIN
productos p ON m.producto_id = p.id
JOIN
ciudades c ON m.ciudad_origen_id = c.id
WHERE
m.tipo = 'Salida' AND DATE(m.fecha) BETWEEN :fecha_inicio AND :fecha_fin
GROUP BY
p.nombre, c.nombre
ORDER BY
precio_liquidaciones DESC
");
$stmt->execute([
':fecha_inicio' => $fecha_inicio,
':fecha_fin' => $fecha_fin
]);
$resultados = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
?>
<h1 class="mb-4">Liquidaciones por Fecha</h1>
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">Filtrar por Fecha</h5>
<form method="GET" action="liquidaciones_por_fecha.php" class="row g-3">
<div class="col-md-5">
<label for="fecha_inicio" class="form-label">Fecha de Inicio</label>
<input type="date" class="form-control" id="fecha_inicio" name="fecha_inicio" value="<?php echo htmlspecialchars($fecha_inicio); ?>" required>
</div>
<div class="col-md-5">
<label for="fecha_fin" class="form-label">Fecha de Fin</label>
<input type="date" class="form-control" id="fecha_fin" name="fecha_fin" value="<?php echo htmlspecialchars($fecha_fin); ?>" required>
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">Calcular</button>
</div>
</form>
</div>
</div>
<?php if ($fecha_inicio && $fecha_fin): ?>
<h2 class="mb-3">Resultados del <?php echo htmlspecialchars(date('d/m/Y', strtotime($fecha_inicio))); ?> al <?php echo htmlspecialchars(date('d/m/Y', strtotime($fecha_fin))); ?></h2>
<?php if (empty($resultados)): ?>
<div class="alert alert-info">No se encontraron liquidaciones para el rango de fechas seleccionado.</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-bordered table-striped">
<thead class="table-dark">
<tr>
<th>Producto</th>
<th>Ciudad</th>
<th>Cantidad de Pedidos</th>
<th>Cantidad</th>
<th>Precio Liquidaciones</th>
</tr>
</thead>
<tbody>
<?php
$total_pedidos = 0;
$total_cantidad = 0;
$total_precio = 0;
foreach ($resultados as $fila):
$total_pedidos += $fila['cantidad_pedidos'];
$total_cantidad += $fila['cantidad'];
$total_precio += $fila['precio_liquidaciones'];
?>
<tr>
<td><?php echo htmlspecialchars($fila['producto']); ?></td>
<td><?php echo htmlspecialchars($fila['ciudad']); ?></td>
<td><?php echo htmlspecialchars($fila['cantidad_pedidos']); ?></td>
<td><?php echo htmlspecialchars($fila['cantidad']); ?></td>
<td><?php echo htmlspecialchars(number_format($fila['precio_liquidaciones'], 2)); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr class="table-secondary">
<th colspan="2" class="text-end">Totales:</th>
<th><?php echo $total_pedidos; ?></th>
<th><?php echo $total_cantidad; ?></th>
<th><?php echo number_format($total_precio, 2); ?></th>
</tr>
</tfoot>
</table>
</div>
<?php endif; ?>
<?php endif; ?>
<?php require_once 'includes/footer.php'; ?>

166
productos.php Normal file
View File

@ -0,0 +1,166 @@
<?php
session_start();
if (!isset($_SESSION["user_id"])) {
header("Location: auth/login.php");
exit();
}
require_once 'includes/header.php';
// Search term
$search_term = isset($_GET['search']) ? trim($_GET['search']) : '';
// Pagination settings
$products_per_page = 10;
$page = isset($_GET['page']) && is_numeric($_GET['page']) ? (int)$_GET['page'] : 1;
$offset = ($page - 1) * $products_per_page;
$pdo = db();
// Base query
$sql_count = "SELECT COUNT(*) FROM productos";
$sql = "SELECT
p.*,
GROUP_CONCAT(c.nombre, ': ', spc.stock_actual SEPARATOR '<br>') as stock_por_ciudad
FROM
productos p
LEFT JOIN
stock_por_ciudad spc ON p.id = spc.producto_id
LEFT JOIN
ciudades c ON spc.ciudad_id = c.id";
$params = [];
// Apply search filter
if (!empty($search_term)) {
$sql_count .= " WHERE nombre LIKE :search";
$sql .= " WHERE p.nombre LIKE :search";
$params[':search'] = '%' . $search_term . '%';
}
// Get total number of products (filtered or not)
$total_stmt = $pdo->prepare($sql_count);
$total_stmt->execute($params);
$total_products = $total_stmt->fetchColumn();
$total_pages = ceil($total_products / $products_per_page);
// Add grouping, ordering and pagination to the main query
$sql .= " GROUP BY p.id
ORDER BY p.id DESC
LIMIT :limit OFFSET :offset";
$stmt = $pdo->prepare($sql);
// Bind search param if it exists
if (!empty($search_term)) {
$stmt->bindValue(':search', $params[':search'], PDO::PARAM_STR);
}
$stmt->bindValue(':limit', $products_per_page, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<div class="container-fluid">
<!-- Page Heading -->
<div class="d-sm-flex justify-content-between align-items-center mb-4">
<h3 class="text-dark mb-0">Gestión de Productos</h3>
<a class="btn btn-primary btn-sm" role="button" href="agregar_producto.php">
<i class="fas fa-plus fa-sm text-white-50"></i>&nbsp;Agregar Producto
</a>
</div>
<!-- Success/Error Messages -->
<?php if (isset($_SESSION['success_message'])): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?php echo $_SESSION['success_message']; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php unset($_SESSION['success_message']); ?>
<?php endif; ?>
<?php if (isset($_SESSION['error_message'])): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?php echo $_SESSION['error_message']; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php unset($_SESSION['error_message']); ?>
<?php endif; ?>
<!-- Search Form -->
<div class="card shadow mb-4">
<div class="card-body">
<form action="productos.php" method="get" class="form-inline">
<div class="input-group">
<input type="text" name="search" class="form-control" placeholder="Buscar por nombre..." value="<?php echo isset($_GET['search']) ? htmlspecialchars($_GET['search']) : ''; ?>">
<button class="btn btn-primary" type="submit">Buscar</button>
</div>
</form>
</div>
</div>
<div class="card shadow">
<div class="card-header py-3">
<p class="text-primary m-0 fw-bold">Listado de Productos</p>
</div>
<div class="card-body">
<div class="table-responsive table mt-2" id="dataTable" role="grid" aria-describedby="dataTable_info">
<table class="table my-0" id="dataTable">
<thead>
<tr>
<th>ID</th>
<th>Nombre</th>
<th>Descripción</th>
<th>Precio</th>
<th>Stock por Ciudad</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php if (count($products) > 0): ?>
<?php foreach ($products as $product): ?>
<tr>
<td><?php echo htmlspecialchars($product['id']); ?></td>
<td><?php echo htmlspecialchars($product['nombre']); ?></td>
<td><?php echo htmlspecialchars($product['descripcion']); ?></td>
<td>$<?php echo htmlspecialchars(number_format($product['precio_venta'], 2)); ?></td>
<td><?php echo $product['stock_por_ciudad'] ? $product['stock_por_ciudad'] : 'Sin stock'; ?></td>
<td>
<a href="editar_producto.php?id=<?php echo $product['id']; ?>" class="btn btn-warning btn-sm"><i class="fas fa-edit"></i> Editar</a>
<a href="eliminar_producto.php?id=<?php echo $product['id']; ?>" class="btn btn-danger btn-sm" onclick="return confirm('¿Estás seguro de que quieres eliminar este producto?');"><i class="fas fa-trash"></i> Eliminar</a>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="6" class="text-center">No hay productos que coincidan con la búsqueda.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Pagination Controls -->
<div class="d-flex justify-content-center">
<nav>
<ul class="pagination">
<?php if ($page > 1): ?>
<li class="page-item"><a class="page-link" href="?page=<?php echo $page - 1; ?><?php echo isset($_GET['search']) ? '&search=' . htmlspecialchars($_GET['search']) : ''; ?>">Anterior</a></li>
<?php endif; ?>
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
<li class="page-item <?php echo ($i == $page) ? 'active' : ''; ?>">
<a class="page-link" href="?page=<?php echo $i; ?><?php echo isset($_GET['search']) ? '&search=' . htmlspecialchars($_GET['search']) : ''; ?>"><?php echo $i; ?></a>
</li>
<?php endfor; ?>
<?php if ($page < $total_pages): ?>
<li class="page-item"><a class="page-link" href="?page=<?php echo $page + 1; ?><?php echo isset($_GET['search']) ? '&search=' . htmlspecialchars($_GET['search']) : ''; ?>">Siguiente</a></li>
<?php endif; ?>
</ul>
</nav>
</div>
</div>
</div>
</div>
<?php require_once 'includes/footer.php'; ?>

86
registrar_entrada.php Normal file
View File

@ -0,0 +1,86 @@
<?php
require_once 'includes/header.php';
// Fetch products and cities for the dropdowns
try {
$pdo = db();
// Fetch products
$stmt_productos = $pdo->query("SELECT id, nombre, sku FROM productos WHERE activo = 1 ORDER BY nombre ASC");
$productos = $stmt_productos->fetchAll(PDO::FETCH_ASSOC);
// Fetch cities
$stmt_ciudades = $pdo->query("SELECT id, nombre FROM ciudades ORDER BY nombre ASC");
$ciudades = $stmt_ciudades->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
echo '<div class="alert alert-danger" role="alert">Error al conectar con la base de datos: ' . htmlspecialchars($e->getMessage()) . '</div>';
$productos = [];
$ciudades = [];
}
?>
<div class="container mt-5">
<h2>Registrar Entrada de Stock</h2>
<p>Utiliza este formulario para añadir nuevas unidades de un producto a una ciudad específica.</p>
<?php if (isset($_SESSION['error'])): ?>
<div class="alert alert-danger" role="alert">
<?php echo htmlspecialchars($_SESSION['error']); unset($_SESSION['error']); ?>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['success'])): ?>
<div class="alert alert-success" role="alert">
<?php echo htmlspecialchars($_SESSION['success']); unset($_SESSION['success']); ?>
</div>
<?php endif; ?>
<form action="handle_entrada.php" method="POST" class="mt-4">
<div class="mb-3">
<label for="producto_id" class="form-label">Producto</label>
<select class="form-select" id="producto_id" name="producto_id" required>
<option value="">Selecciona un producto</option>
<?php foreach ($productos as $producto): ?>
<option value="<?php echo htmlspecialchars($producto['id']); ?>">
<?php echo htmlspecialchars($producto['nombre']) . ' (SKU: ' . htmlspecialchars($producto['sku']) . ')'; ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="ciudad_id" class="form-label">Ciudad de Destino</label>
<select class="form-select" id="ciudad_id" name="ciudad_id" required>
<option value="">Selecciona una ciudad</option>
<?php foreach ($ciudades as $ciudad): ?>
<option value="<?php echo htmlspecialchars($ciudad['id']); ?>">
<?php echo htmlspecialchars($ciudad['nombre']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="cantidad" class="form-label">Cantidad</label>
<input type="number" class="form-control" id="cantidad" name="cantidad" min="1" required>
</div>
<div class="mb-3">
<label for="fecha" class="form-label">Fecha de Entrada</label>
<input type="date" class="form-control" id="fecha" name="fecha" required>
</div>
<div class="mb-3">
<label for="observacion" class="form-label">Observación (Opcional)</label>
<textarea class="form-control" id="observacion" name="observacion" rows="3" placeholder="Ej: Compra a proveedor, ajuste de inventario, etc."></textarea>
</div>
<button type="submit" class="btn btn-success">Registrar Entrada</button>
<a href="productos.php" class="btn btn-secondary">Cancelar</a>
</form>
</div>
<?php
require_once 'includes/footer.php';
?>

112
registrar_salida.php Normal file
View File

@ -0,0 +1,112 @@
<?php
// registrar_salida.php
require_once 'includes/header.php';
// Restringir acceso solo a roles autorizados
$allowed_roles = ['Administrador General', 'Encargado de Stock'];
if (!isset($_SESSION['user_rol']) || !in_array($_SESSION['user_rol'], $allowed_roles)) {
$_SESSION['error'] = 'No tienes permiso para acceder a esta página.';
header('Location: index.php');
exit;
}
// Fetch products and cities for the dropdowns
try {
$pdo = db();
// Fetch products
$stmt_productos = $pdo->query("SELECT id, nombre FROM productos ORDER BY nombre ASC");
$productos = $stmt_productos->fetchAll(PDO::FETCH_ASSOC);
// Fetch cities
$stmt_ciudades = $pdo->query("SELECT id, nombre FROM ciudades ORDER BY nombre ASC");
$ciudades = $stmt_ciudades->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
// Handle DB error
echo '<div class="alert alert-danger" role="alert">Error al conectar con la base de datos: ' . htmlspecialchars($e->getMessage()) . '</div>';
$productos = [];
$ciudades = [];
}
?>
<div class="container mt-5">
<h1>Registrar Salida de Producto</h1>
<p>Selecciona el producto, la ciudad y la cantidad que deseas dar de baja del inventario.</p>
<?php
if (isset($_SESSION['error'])):
?>
<div class="alert alert-danger" role="alert">
<?php
echo htmlspecialchars($_SESSION['error']);
unset($_SESSION['error']);
?>
</div>
<?php
endif;
?>
<?php
if (isset($_SESSION['success'])):
?>
<div class="alert alert-success" role="alert">
<?php
echo htmlspecialchars($_SESSION['success']);
unset($_SESSION['success']);
?>
</div>
<?php
endif;
?>
<form action="handle_salida.php" method="POST" class="mt-4">
<div class="mb-3">
<label for="producto_id" class="form-label">Producto</label>
<select class="form-select" id="producto_id" name="producto_id" required>
<option value="">Selecciona un producto</option>
<?php foreach ($productos as $producto): ?>
<option value="<?php echo htmlspecialchars($producto['id']); ?>">
<?php echo htmlspecialchars($producto['nombre']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="ciudad_id" class="form-label">Ciudad</label>
<select class="form-select" id="ciudad_id" name="ciudad_id" required>
<option value="">Selecciona una ciudad</option>
<?php foreach ($ciudades as $ciudad): ?>
<option value="<?php echo htmlspecialchars($ciudad['id']); ?>">
<?php echo htmlspecialchars($ciudad['nombre']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="cantidad" class="form-label">Cantidad</label>
<input type="number" class="form-control" id="cantidad" name="cantidad" min="1" required>
</div>
<div class="mb-3">
<label for="fecha" class="form-label">Fecha de Salida</label>
<input type="date" class="form-control" id="fecha" name="fecha" required>
</div>
<div class="mb-3">
<label for="descripcion" class="form-label">Descripción (Opcional)</label>
<textarea class="form-control" id="descripcion" name="descripcion" rows="3" placeholder="Ej: Venta a cliente, traslado a otra tienda, etc."></textarea>
</div>
<button type="submit" class="btn btn-primary">Registrar Salida</button>
<a href="productos.php" class="btn btn-secondary">Cancelar</a>
</form>
</div>
<?php
require_once 'includes/footer.php';
?>

255
reporte_de_pagos.php Normal file
View File

@ -0,0 +1,255 @@
<?php
require_once 'includes/header.php';
// Restringir acceso solo a administradores
if (isset($_SESSION['user_rol']) && $_SESSION['user_rol'] === 'Encargado de Stock') {
header('Location: /index.php?error=unauthorized');
exit;
}
require_once 'db/config.php';
// Get current month and year, or from query string
$month = isset($_GET['month']) ? (int)$_GET['month'] : date('m');
$year = isset($_GET['year']) ? (int)$_GET['year'] : date('Y');
// Handle month overflow/underflow
if ($month > 12) {
$month = 1;
$year++;
} elseif ($month < 1) {
$month = 12;
$year--;
}
$liquidacion_id = (int)($year . str_pad($month, 2, '0', STR_PAD_LEFT));
// Get city and sales data
$db = db();
$cities_stmt = $db->query("SELECT id, nombre FROM ciudades ORDER BY orden ASC");
$cities = $cities_stmt->fetchAll(PDO::FETCH_ASSOC);
// Get payment statuses for each city and day
$estados_stmt = $db->prepare("SELECT ciudad_id, fecha, estado FROM liquidaciones_ciudades_estados WHERE liquidacion_id = :liquidacion_id");
$estados_stmt->execute(['liquidacion_id' => $liquidacion_id]);
$estados_raw = $estados_stmt->fetchAll(PDO::FETCH_ASSOC);
$estados = [];
foreach ($estados_raw as $row) {
$estados[$row['ciudad_id']][$row['fecha']] = $row['estado'];
}
$sales_stmt = $db->prepare("
SELECT
DAY(m.fecha) as dia,
m.ciudad_origen_id,
SUM(m.precio_liquidacion) as monto_total
FROM movimientos m
WHERE m.tipo = 'Salida'
AND MONTH(m.fecha) = :month
AND YEAR(m.fecha) = :year
GROUP BY DAY(m.fecha), m.ciudad_origen_id
");
$sales_stmt->execute(['month' => $month, 'year' => $year]);
$sales_data = $sales_stmt->fetchAll(PDO::FETCH_ASSOC);
// Organize sales data in a structured way: [day][city_id] => amount
$sales_by_day_city = [];
foreach ($sales_data as $sale) {
$sales_by_day_city[$sale['dia']][$sale['ciudad_origen_id']] = $sale['monto_total'];
}
$days_in_month = cal_days_in_month(CAL_GREGORIAN, $month, $year);
$month_name = DateTime::createFromFormat('!m', $month)->format('F');
?>
<div class="container-fluid">
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800">Validaciones De Pagos</h1>
</div>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Resumen de Ventas por Ciudad</h6>
</div>
<div class="card-body">
<div class="d-flex justify-content-center align-items-center mb-3">
<a href="?month=<?php echo $month - 1; ?>&year=<?php echo $year; ?>" class="btn btn-secondary btn-sm mr-2">&lt; Mes Anterior</a>
<h4 class="mb-0"><?php echo ucfirst($month_name) . ' ' . $year; ?></h4>
<a href="?month=<?php echo $month + 1; ?>&year=<?php echo $year; ?>" class="btn btn-secondary btn-sm ml-2">Mes Siguiente &gt;</a>
</div>
<style>
#dataTable td,
#dataTable th {
padding-top: 0.2rem;
padding-bottom: 0.2rem;
vertical-align: middle;
}
#dataTable .form-control-sm {
padding-top: 0.1rem;
padding-bottom: 0.1rem;
height: auto;
}
.table-responsive {
max-height: 70vh; /* Ajusta la altura máxima según necesites */
overflow-y: auto;
}
#dataTable thead th {
position: sticky;
background-color: #343a40 !important;
color: #fff !important;
z-index: 1;
}
#dataTable thead tr:first-child th {
top: 0;
z-index: 2; /* Prioridad para la fila superior */
}
#dataTable thead tr:nth-child(2) th {
top: 38px; /* Ajusta si es necesario a la altura de la primera fila */
}
</style>
<div class="table-responsive">
<table class="table table-bordered table-hover" id="dataTable" width="100%" cellspacing="0">
<thead class="thead-dark">
<tr>
<th rowspan="2" class="align-middle text-center">Día</th>
<?php foreach ($cities as $city) : ?>
<th colspan="2" class="text-center"><?php echo htmlspecialchars($city['nombre']); ?></th>
<?php endforeach; ?>
<th rowspan="2" class="text-center table-primary align-middle">Total Diario</th>
</tr>
<tr>
<?php foreach ($cities as $city) : ?>
<th class="text-center">Recaudo</th>
<th class="text-center">Estado</th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php
$grand_total = 0;
$city_totals = array_fill_keys(array_column($cities, 'id'), 0);
for ($day = 1; $day <= $days_in_month; $day++) :
$daily_total = 0;
$current_date = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT) . '-' . str_pad($day, 2, '0', STR_PAD_LEFT);
?>
<tr>
<td class="text-center align-middle"><?php echo htmlspecialchars(date('d-m-Y', strtotime($current_date))); ?></td>
<?php foreach ($cities as $city) :
$amount = $sales_by_day_city[$day][$city['id']] ?? 0;
$daily_total += $amount;
$city_totals[$city['id']] += $amount;
?>
<td class="text-right align-middle">
<?php echo $amount > 0 ? number_format($amount, 2) : '-'; ?>
</td>
<?php
$estado_actual = $estados[$city['id']][$current_date] ?? 'Pendiente';
?>
<td>
<select class="form-control form-control-sm estado-pago"
data-ciudad-id="<?php echo $city['id']; ?>"
data-liquidacion-id="<?php echo $liquidacion_id; ?>"
data-fecha="<?php echo $current_date; ?>">
<option value="Pendiente" <?php echo $estado_actual === 'Pendiente' ? 'selected' : ''; ?>>Pendiente</option>
<option value="Pagado" <?php echo $estado_actual === 'Pagado' ? 'selected' : ''; ?>>Pagado</option>
</select>
</td>
<?php endforeach; ?>
<td class="text-right font-weight-bold table-primary align-middle">
<?php echo number_format($daily_total, 2); ?>
</td>
</tr>
<?php
$grand_total += $daily_total;
endfor;
?>
</tbody>
<tfoot class="thead-dark">
<tr>
<th>Total por Ciudad</th>
<?php foreach ($cities as $city) : ?>
<th class="text-right">
<?php echo number_format($city_totals[$city['id']], 2); ?>
</th>
<th></th>
<?php endforeach; ?>
<th class="text-right table-success font-weight-bolder">
<?php echo number_format($grand_total, 2); ?>
</th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const selects = document.querySelectorAll('.estado-pago');
function setSelectStyle(select, estado) {
const td = select.parentElement;
if (estado === 'Pagado') {
td.classList.add('bg-success');
select.classList.add('bg-success', 'text-white');
select.classList.remove('bg-light');
} else {
td.classList.remove('bg-success');
select.classList.remove('bg-success', 'text-white');
select.classList.add('bg-light');
}
}
selects.forEach(select => {
// Set initial style based on the loaded value
setSelectStyle(select, select.value);
select.addEventListener('change', function() {
const ciudadId = this.dataset.ciudadId;
const liquidacionId = this.dataset.liquidacionId;
const fecha = this.dataset.fecha;
const estado = this.value;
const originalState = estado === 'Pagado' ? 'Pendiente' : 'Pagado';
// Optimistically update UI
setSelectStyle(this, estado);
fetch('handle_pago_ciudad.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
liquidacion_id: liquidacionId,
ciudad_id: ciudadId,
fecha: fecha,
estado: estado
})
})
.then(response => response.json())
.then(data => {
if (!data.success) {
alert('Error al actualizar el estado: ' + data.message);
// Revert UI on error
this.value = originalState;
setSelectStyle(this, originalState);
}
})
.catch(error => {
console.error('Error en la petición:', error);
alert('Ocurrió un error de red. Inténtalo de nuevo.');
// Revert UI on error
this.value = originalState;
setSelectStyle(this, originalState);
});
});
});
});
</script>
<?php require_once 'includes/footer.php'; ?>

120
resumen_stock.php Normal file
View File

@ -0,0 +1,120 @@
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: auth/login.php');
exit;
}
require_once 'db/config.php';
require_once 'includes/header.php';
$pdo = db();
// 1. Obtener todas las ciudades, ordenadas por stock total descendente
$query_ciudades = '
SELECT
c.id,
c.nombre,
SUM(COALESCE(spc.stock_actual, 0)) as total_stock
FROM
ciudades c
LEFT JOIN
stock_por_ciudad spc ON c.id = spc.ciudad_id
GROUP BY
c.id, c.nombre
ORDER BY
total_stock DESC, c.orden ASC
';
$stmt_ciudades = $pdo->query($query_ciudades);
$ciudades = $stmt_ciudades->fetchAll(PDO::FETCH_ASSOC);
// 2. Obtener todos los productos ordenados por la nueva columna 'orden'
$stmt_productos = $pdo->query('SELECT id, nombre FROM productos ORDER BY orden ASC');
$productos = $stmt_productos->fetchAll(PDO::FETCH_ASSOC);
// 3. Obtener todo el stock y organizarlo en un array para acceso rápido
$stmt_stock = $pdo->query('SELECT producto_id, ciudad_id, stock_actual FROM stock_por_ciudad');
$stock_data = [];
while ($row = $stmt_stock->fetch(PDO::FETCH_ASSOC)) {
$stock_data[$row['producto_id']][$row['ciudad_id']] = $row['stock_actual'];
}
?>
<div class="container-fluid px-4">
<h1 class="mt-4">Resumen de Stock</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="index.php">Dashboard</a></li>
<li class="breadcrumb-item active">Resumen de Stock</li>
</ol>
<div class="card mb-4">
<div class="card-header">
<i class="fas fa-table me-1"></i>
Inventario por Producto y Ciudad
</div>
<div class="card-body">
<style>
.table-compact th,
.table-compact td {
padding: 0.25rem 0.5rem;
vertical-align: middle;
}
.bg-success-soft {
background-color: #d1e7dd !important; /* Verde suave */
}
.bg-danger-soft {
background-color: #f8d7da !important; /* Rojo suave */
}
</style>
<div class="table-responsive">
<table class="table table-bordered table-striped table-hover table-compact">
<thead class="table-dark">
<tr>
<th style="width: 250px;">Producto</th>
<th class="text-center" style="width: 100px;">Orden</th>
<?php foreach ($ciudades as $ciudad): ?>
<th class="text-center"><?php echo htmlspecialchars($ciudad['nombre']); ?></th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php if (empty($productos)): ?>
<tr>
<td colspan="<?php echo count($ciudades) + 2; ?>" class="text-center">No hay productos para mostrar.</td>
</tr>
<?php else: ?>
<?php foreach ($productos as $producto): ?>
<tr>
<td><?php echo htmlspecialchars($producto['nombre']); ?></td>
<td class="text-center">
<a href="handle_orden_productos.php?id=<?php echo $producto['id']; ?>&dir=up" class="btn btn-sm btn-outline-secondary py-0 px-1"><i class="fas fa-arrow-up"></i></a>
<a href="handle_orden_productos.php?id=<?php echo $producto['id']; ?>&dir=down" class="btn btn-sm btn-outline-secondary py-0 px-1"><i class="fas fa-arrow-down"></i></a>
</td>
<?php foreach ($ciudades as $ciudad): ?>
<?php
$stock = isset($stock_data[$producto['id']][$ciudad['id']]) ? $stock_data[$producto['id']][$ciudad['id']] : 0;
$cell_class = '';
if ($stock >= 10) {
$cell_class = 'bg-success-soft';
} elseif ($stock > 0 && $stock < 10) {
$cell_class = 'bg-danger-soft';
}
?>
<td class="text-center <?php echo $cell_class; ?>">
<?php echo $stock; ?>
</td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php
require_once 'includes/footer.php';
?>

1
test_page.php Normal file
View File

@ -0,0 +1 @@
<?php echo 'Esta es una página de prueba.'; ?>