Compare commits

...

122 Commits

Author SHA1 Message Date
Flatlogic Bot
4b4a7e2b8b Use VM database environment 2026-05-26 16:23:08 +00:00
Flatlogic Support
c234c7d5ad Exclude runtime uploads from project backup 2026-05-26 15:54:09 +00:00
Flatlogic Support
a630d16063 Manual code backup after billing restore 2026-05-26 2026-05-26 15:51:40 +00:00
Flatlogic Bot
10a0221760 Autosave: 20260525-104355 2026-05-25 10:43:59 +00:00
Flatlogic Bot
2c651b72fb Autosave: 20260525-095403 2026-05-25 09:54:07 +00:00
Flatlogic Bot
bd9aa664a9 Autosave: 20260525-090633 2026-05-25 09:06:38 +00:00
Flatlogic Bot
891893f221 Autosave: 20260525-070710 2026-05-25 07:07:14 +00:00
Flatlogic Bot
623d4cd358 Autosave: 20260525-031836 2026-05-25 03:18:40 +00:00
Flatlogic Bot
00437625c8 Autosave: 20260525-012131 2026-05-25 01:22:01 +00:00
Flatlogic Bot
2c3b594aa0 Autosave: 20260524-024040 2026-05-24 02:40:44 +00:00
Flatlogic Bot
4671e9246d Autosave: 20260524-013507 2026-05-24 01:35:11 +00:00
Flatlogic Bot
e71ff2d7a4 Autosave: 20260524-001517 2026-05-24 00:20:06 +00:00
Flatlogic Bot
f587c9e57c Autosave: 20260523-234035 2026-05-23 23:40:36 +00:00
Flatlogic Bot
7b765bd9d0 Autosave: 20260523-213523 2026-05-23 21:35:24 +00:00
Flatlogic Bot
6800ed2d90 Autosave: 20260523-193336 2026-05-23 19:33:36 +00:00
Flatlogic Bot
37cd8f837c Autosave: 20260523-181632 2026-05-23 18:16:33 +00:00
Flatlogic Bot
709ff2f0d0 Autosave: 20260523-170233 2026-05-23 17:02:34 +00:00
Flatlogic Bot
491b21a0f8 Autosave: 20260523-020012 2026-05-23 02:00:13 +00:00
Flatlogic Bot
b959c55838 Autosave: 20260522-181929 2026-05-22 18:19:30 +00:00
Flatlogic Bot
7df520a3f9 Autosave: 20260522-172015 2026-05-22 17:20:15 +00:00
Flatlogic Bot
5707981abe Autosave: 20260522-165916 2026-05-22 16:59:17 +00:00
Flatlogic Bot
8dc1fb0062 Autosave: 20260522-040104 2026-05-22 04:01:05 +00:00
Flatlogic Bot
8073e5e1b3 Autosave: 20260522-025905 2026-05-22 02:59:06 +00:00
Flatlogic Bot
603ef2ccb9 Autosave: 20260522-022729 2026-05-22 02:27:30 +00:00
Flatlogic Bot
621d2a48d9 Autosave: 20260521-213229 2026-05-21 21:32:30 +00:00
Flatlogic Bot
cd09784a88 Autosave: 20260521-205255 2026-05-21 20:52:55 +00:00
Flatlogic Bot
1a7ea8d1c8 Autosave: 20260521-201042 2026-05-21 20:10:43 +00:00
Flatlogic Bot
f06e0c415c Autosave: 20260521-193525 2026-05-21 19:35:26 +00:00
Flatlogic Bot
0eaaa1c343 Autosave: 20260521-190918 2026-05-21 19:09:21 +00:00
Flatlogic Bot
370e45f837 Autosave: 20260521-183802 2026-05-21 18:38:03 +00:00
Flatlogic Bot
b4284d6ec3 Autosave: 20260521-172923 2026-05-21 17:29:24 +00:00
Flatlogic Bot
29814f9aa8 Autosave: 20260521-170036 2026-05-21 17:00:39 +00:00
Flatlogic Bot
4bc4e0b695 Autosave: 20260519-183100 2026-05-19 18:31:00 +00:00
Flatlogic Bot
2454573a89 Autosave: 20260519-173441 2026-05-19 17:34:42 +00:00
Flatlogic Bot
8d1b45365f Autosave: 20260519-165135 2026-05-19 16:51:35 +00:00
Flatlogic Bot
26b4617b8a Autosave: 20260519-012209 2026-05-19 01:22:09 +00:00
Flatlogic Bot
a8d2f9cb0a Autosave: 20260518-233649 2026-05-18 23:36:50 +00:00
Flatlogic Bot
bbd26aa303 Autosave: 20260518-225855 2026-05-18 22:58:55 +00:00
Flatlogic Bot
faacccd59f Autosave: 20260518-221627 2026-05-18 22:16:28 +00:00
Flatlogic Bot
ff10c06eaf Autosave: 20260518-192226 2026-05-18 19:22:27 +00:00
Flatlogic Bot
b928365afa Autosave: 20260518-185358 2026-05-18 18:53:59 +00:00
Flatlogic Bot
515395635b Autosave: 20260518-171536 2026-05-18 17:15:37 +00:00
Flatlogic Bot
4048403247 Autosave: 20260518-164428 2026-05-18 16:44:29 +00:00
Flatlogic Bot
5336ec4122 Autosave: 20260517-004332 2026-05-17 00:43:33 +00:00
Flatlogic Bot
ca33865e0a Autosave: 20260516-060447 2026-05-16 06:04:47 +00:00
Flatlogic Bot
40389434b1 Autosave: 20260516-005835 2026-05-16 00:58:36 +00:00
Flatlogic Bot
16e55cb151 Autosave: 20260516-003633 2026-05-16 00:36:34 +00:00
Flatlogic Bot
a897557f9b Autosave: 20260514-223800 2026-05-14 22:38:03 +00:00
Flatlogic Bot
5c9514cbb3 Autosave: 20260512-114702 2026-05-12 11:47:02 +00:00
Flatlogic Bot
667d0238fa Autosave: 20260512-105548 2026-05-12 10:55:49 +00:00
Flatlogic Bot
77cd3da5bc Autosave: 20260510-162255 2026-05-10 16:22:58 +00:00
Flatlogic Bot
4f3867406b Autosave: 20260505-030332 2026-05-05 03:03:33 +00:00
Flatlogic Bot
5a30279f6c Autosave: 20260505-022427 2026-05-05 02:24:28 +00:00
Flatlogic Bot
5b04629711 Autosave: 20260430-222744 2026-04-30 22:27:45 +00:00
Flatlogic Bot
62877072fb Autosave: 20260430-180545 2026-04-30 18:05:46 +00:00
Flatlogic Bot
b7f5f0e7d2 Autosave: 20260430-162110 2026-04-30 16:21:18 +00:00
Flatlogic Bot
1e1e9e674a Autosave: 20260428-085118 2026-04-28 08:51:18 +00:00
Flatlogic Bot
faa1a9f2b3 Autosave: 20260428-082031 2026-04-28 08:20:31 +00:00
Flatlogic Bot
388bd415b3 Autosave: 20260428-045349 2026-04-28 04:53:51 +00:00
Flatlogic Bot
f09833681a Autosave: 20260428-041812 2026-04-28 04:18:12 +00:00
Flatlogic Bot
09acd5dd5b Autosave: 20260428-040702 2026-04-28 04:07:03 +00:00
Flatlogic Bot
fc63421a68 Autosave: 20260428-035649 2026-04-28 03:56:49 +00:00
Flatlogic Bot
c214262958 Autosave: 20260428-033211 2026-04-28 03:32:11 +00:00
Flatlogic Bot
870bef9157 Autosave: 20260428-011435 2026-04-28 01:14:38 +00:00
Flatlogic Bot
f8fadfdc8a Autosave: 20260422-225218 2026-04-22 22:52:20 +00:00
Flatlogic Bot
7eb4c17bef Autosave: 20260421-192611 2026-04-21 19:26:12 +00:00
Flatlogic Bot
5de02535d2 Autosave: 20260421-184252 2026-04-21 18:42:52 +00:00
Flatlogic Bot
0a1f6ad1d2 Autosave: 20260421-175524 2026-04-21 17:55:48 +00:00
Flatlogic Bot
bb70c82e6c Autosave: 20260415-175623 2026-04-15 17:56:23 +00:00
Flatlogic Bot
67e276f3a4 Autosave: 20260415-040714 2026-04-15 04:07:24 +00:00
Flatlogic Bot
a88941468e Autosave: 20260326-055859 2026-03-26 05:59:00 +00:00
Flatlogic Bot
58d6986d95 Autosave: 20260326-051748 2026-03-26 05:17:52 +00:00
Flatlogic Bot
88c5d32ce4 Autosave: 20260311-164631 2026-03-11 16:46:33 +00:00
Flatlogic Bot
75e09a3c47 feat: Implement monthly navigation for expenses 2026-03-09 17:39:45 +00:00
Flatlogic Bot
becf639f3b Autosave: 20260309-172536 2026-03-09 17:25:43 +00:00
Flatlogic Bot
fdce191a3f Autosave: 20260220-172714 2026-02-20 17:27:14 +00:00
Flatlogic Bot
b9b815f6ba Autosave: 20260220-170918 2026-02-20 17:09:19 +00:00
Flatlogic Bot
8914f6e2df Autosave: 20260220-162206 2026-02-20 16:22:07 +00:00
Flatlogic Bot
9e7939eebb Autosave: 20260219-181840 2026-02-19 18:18:43 +00:00
Flatlogic Bot
24ba5cd24a Autosave: 20260217-062114 2026-02-17 06:21:14 +00:00
Flatlogic Bot
61ccd5cda6 Autosave: 20260217-053817 2026-02-17 05:38:18 +00:00
Flatlogic Bot
47257c628c Autosave: 20260217-050822 2026-02-17 05:08:23 +00:00
Flatlogic Bot
11c42a7000 Autosave: 20260217-033956 2026-02-17 03:39:56 +00:00
Flatlogic Bot
1bf602b5b0 Autosave: 20260217-031338 2026-02-17 03:13:39 +00:00
Flatlogic Bot
520d1acb1b Autosave: 20260217-024246 2026-02-17 02:42:46 +00:00
Flatlogic Bot
05af420873 Autosave: 20260217-015414 2026-02-17 01:54:14 +00:00
Flatlogic Bot
05c83857b0 Autosave: 20260217-013527 2026-02-17 01:35:29 +00:00
Flatlogic Bot
54f691879e Autosave: 20260212-193734 2026-02-12 19:37:34 +00:00
Flatlogic Bot
304de737e6 Autosave: 20260212-175210 2026-02-12 17:52:11 +00:00
Flatlogic Bot
914c98e457 Autosave: 20260212-171727 2026-02-12 17:17:28 +00:00
Flatlogic Bot
f59550e0b4 Autosave: 20260212-165014 2026-02-12 16:50:14 +00:00
Flatlogic Bot
d37c7e72bd Autosave: 20260212-161706 2026-02-12 16:17:06 +00:00
Flatlogic Bot
7ba3e3fac5 Autosave: 20260212-154809 2026-02-12 15:48:10 +00:00
Flatlogic Bot
60510752fa Autosave: 20260212-121318 2026-02-12 12:13:18 +00:00
Flatlogic Bot
98f0c58027 Autosave: 20260212-051355 2026-02-12 05:13:55 +00:00
Flatlogic Bot
b390ef6318 Autosave: 20260212-044617 2026-02-12 04:46:17 +00:00
Flatlogic Bot
f88375f643 Autosave: 20260212-042541 2026-02-12 04:25:41 +00:00
Flatlogic Bot
697dbbf92b Autosave: 20260212-035552 2026-02-12 03:55:52 +00:00
Flatlogic Bot
184bb26fca Autosave: 20260212-033753 2026-02-12 03:37:54 +00:00
Flatlogic Bot
4cf44f13b0 Autosave: 20260209-174509 2026-02-09 17:45:10 +00:00
Flatlogic Bot
8430b2c637 Autosave: 20260206-051613 2026-02-06 05:16:13 +00:00
Flatlogic Bot
7ccbc77b67 Autosave: 20260206-040234 2026-02-06 04:02:34 +00:00
Flatlogic Bot
149d498ccf Autosave: 20260205-075530 2026-02-05 07:55:31 +00:00
Flatlogic Bot
d0a0fa9577 Autosave: 20260205-071845 2026-02-05 07:18:45 +00:00
Flatlogic Bot
eb777eb9a8 Autosave: 20260205-063404 2026-02-05 06:34:05 +00:00
Flatlogic Bot
306385f2ae Autosave: 20260205-060056 2026-02-05 06:00:56 +00:00
Flatlogic Bot
7917ea5f98 Autosave: 20260205-053735 2026-02-05 05:37:35 +00:00
Flatlogic Bot
2e2d50688f Autosave: 20260203-220138 2026-02-03 22:01:38 +00:00
Flatlogic Bot
5ebd11482d Autosave: 20260203-162317 2026-02-03 16:23:17 +00:00
Flatlogic Bot
762a54dd3e Autosave: 20260203-074408 2026-02-03 07:44:08 +00:00
Flatlogic Bot
e2dbd8b86f Autosave: 20260203-072219 2026-02-03 07:22:19 +00:00
Flatlogic Bot
9a3241c3ec Autosave: 20260203-070225 2026-02-03 07:02:26 +00:00
Flatlogic Bot
a059de67e8 Autosave: 20260203-064311 2026-02-03 06:43:11 +00:00
Flatlogic Bot
db4413f46e Autosave: 20260203-063144 2026-02-03 06:31:44 +00:00
Flatlogic Bot
96d2546dba Autosave: 20260203-060838 2026-02-03 06:08:39 +00:00
Flatlogic Bot
2caed5f3df Autosave: 20260203-052927 2026-02-03 05:29:28 +00:00
Flatlogic Bot
ea1b1360ff Autosave: 20260203-035333 2026-02-03 03:53:33 +00:00
Flatlogic Bot
eeb9b89ef6 Autosave: 20260203-032004 2026-02-03 03:20:04 +00:00
Flatlogic Bot
ef8b2b7b59 Autosave: 20260203-025035 2026-02-03 02:50:35 +00:00
Flatlogic Bot
05535adcfa Autosave: 20260203-022954 2026-02-03 02:29:54 +00:00
Flatlogic Bot
6995544f62 Autosave: 20260203-020543 2026-02-03 02:05:44 +00:00
Flatlogic Bot
bdc6e40a34 Autosave: 20260203-014257 2026-02-03 01:43:03 +00:00
34469 changed files with 4224375 additions and 172 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
node_modules/
*/node_modules/
*/build/
assets/uploads/

View File

@ -1,18 +1,3 @@
DirectoryIndex index.php index.html
Options -Indexes
Options -MultiViews
RewriteEngine On
# 0) Serve existing files/directories as-is
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# 1) Internal map: /page or /page/ -> /page.php (if such PHP file exists)
RewriteCond %{REQUEST_FILENAME}.php -f
RewriteRule ^(.+?)/?$ $1.php [L]
# 2) Optional: strip trailing slash for non-directories (keeps .php links working)
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)/$ $1 [R=301,L]
DirectoryIndex index.php
php_value auto_prepend_file "db/config.php"

0
.perm_test_apache Normal file
View File

0
.perm_test_exec Normal file
View File

31
add_cobertura.php Normal file
View File

@ -0,0 +1,31 @@
<?php
session_start();
header('Content-Type: application/json');
if (!isset($_SESSION['user_id'])) {
echo json_encode(['success' => false, 'error' => 'No autorizado']);
exit;
}
require_once 'db/config.php';
$titulo = isset($_POST['titulo']) ? trim($_POST['titulo']) : '';
$texto = isset($_POST['texto']) ? trim($_POST['texto']) : '';
if (empty($titulo)) {
echo json_encode(['success' => false, 'error' => 'El título no puede estar vacío.']);
exit;
}
try {
$db = db();
$stmt = $db->prepare("INSERT INTO cobertura (titulo, texto) VALUES (?, ?)");
if ($stmt->execute([$titulo, $texto])) {
echo json_encode(['success' => true, 'id' => $db->lastInsertId()]);
} else {
echo json_encode(['success' => false, 'error' => 'No se pudo guardar en la base de datos.']);
}
} catch (PDOException $e) {
echo json_encode(['success' => false, 'error' => 'Error de base de datos: ' . $e->getMessage()]);
}

32
add_cobertura_xpress.php Normal file
View File

@ -0,0 +1,32 @@
<?php
session_start();
header('Content-Type: application/json');
if (!isset($_SESSION['user_id'])) {
echo json_encode(['success' => false, 'error' => 'No autorizado']);
exit;
}
require_once 'db/config.php';
$titulo = isset($_POST['titulo']) ? trim($_POST['titulo']) : '';
$texto = isset($_POST['texto']) ? trim($_POST['texto']) : '';
if (empty($titulo) && empty($texto)) {
echo json_encode(['success' => false, 'error' => 'Los campos no pueden estar vacíos.']);
exit;
}
try {
$db = db();
$stmt = $db->prepare("INSERT INTO cobertura_xpress (titulo, texto) VALUES (?, ?)");
if ($stmt->execute([$titulo, $texto])) {
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'No se pudo guardar en la base de datos.']);
}
} catch (PDOException $e) {
echo json_encode(['success' => false, 'error' => 'Error de base de datos: ' . $e->getMessage()]);
}
?>

65
agregar_producto.php Normal file
View File

@ -0,0 +1,65 @@
<?php
$pageTitle = "Agregar Nuevo Producto";
require_once 'layout_header.php';
require_once 'db/config.php';
$message = '';
$error = '';
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$nombre_producto = filter_input(INPUT_POST, 'nombre_producto', FILTER_SANITIZE_STRING);
if ($nombre_producto) {
try {
$pdo = new PDO("mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8", DB_USER, DB_PASS);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Insertar el nuevo producto
$stmt = $pdo->prepare("INSERT INTO products (nombre) VALUES (:nombre)");
$stmt->execute(['nombre' => $nombre_producto]);
$message = "¡Producto '" . htmlspecialchars($nombre_producto) . "' agregado correctamente!";
} catch (PDOException $e) {
$error = "Error al agregar el producto: " . $e->getMessage();
}
} else {
$error = "Por favor, ingrese el nombre del producto.";
}
}
?>
<div class="container mt-4">
<div class="row">
<div class="col-lg-6 mx-auto">
<?php if ($message): ?>
<div class="alert alert-success" role="alert">
<?php echo $message; ?>
</div>
<?php endif; ?>
<?php if ($error): ?>
<div class="alert alert-danger" role="alert">
<?php echo htmlspecialchars($error); ?>
</div>
<?php endif; ?>
<div class="card">
<div class="card-header">
<i class="fa fa-plus"></i> Agregar Nuevo Producto al Catálogo
</div>
<div class="card-body">
<form action="agregar_producto.php" method="post">
<div class="mb-3">
<label for="nombre_producto" class="form-label">Nombre del Producto</label>
<input type="text" class="form-control" id="nombre_producto" name="nombre_producto" required>
</div>
<button type="submit" class="btn btn-primary w-100"> <i class="fa fa-plus-circle"></i> Guardar Producto</button>
</form>
</div>
</div>
</div>
</div>
</div>
<?php require_once 'layout_footer.php'; ?>

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

@ -0,0 +1,414 @@
/* General Body Styles */
body {
background-color: #f4f7f6;
font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
display: flex;
color: #333;
transition: margin-left 0.3s;
}
/* Sidebar Styles */
.sidebar {
width: 260px;
height: 100vh;
position: fixed;
top: 0;
left: 0;
background: linear-gradient(145deg, #2c3e50, #34495e);
color: #ecf0f1;
box-shadow: 2px 0 15px rgba(0,0,0,0.1);
transition: transform 0.3s ease;
z-index: 1000;
display: flex;
flex-direction: column;
}
.sidebar .navbar-brand {
padding: 1rem 1.5rem;
color: #fff;
font-weight: bold;
text-align: center;
font-size: 1.5rem;
flex-shrink: 0; /* Prevent brand from shrinking */
}
.menu-wrapper {
flex-grow: 1; /* Takes up all available space */
overflow-y: auto; /* Allows scrolling for menu items */
}
.sidebar .nav-link {
padding: 12px 25px;
text-decoration: none;
font-size: 1rem;
color: #bdc3c7;
display: block;
transition: all 0.3s ease;
}
.sidebar .nav-link i {
margin-right: 15px;
width: 20px; /* Align icons */
text-align: center;
}
.sidebar .nav-link:hover, .sidebar .nav-link.active {
background-color: #4a627a;
color: #fff;
border-left: 4px solid #3498db;
padding-left: 21px;
}
.logout-wrapper {
flex-shrink: 0; /* Prevents the logout area from shrinking */
border-top: 1px solid #4a627a; /* Separator line */
}
.logout-wrapper .nav-link {
font-weight: bold;
}
/* Content Area Styles */
.content {
margin-left: 260px;
padding: 30px;
width: calc(100% - 260px);
overflow-y: auto;
transition: margin-left 0.3s ease;
}
/* Hamburger Menu Button */
.sidebar-toggle {
display: none;
position: fixed;
top: 15px;
left: 15px;
z-index: 1001;
background: #34495e;
color: #fff;
border: none;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
}
/* Responsive Styles */
@media (max-width: 992px) {
.sidebar {
transform: translateX(-100%);
z-index: 1020; /* Ensure sidebar is on top */
}
.sidebar.active {
transform: translateX(0);
}
.content {
margin-left: 0;
width: 100%;
transition: filter 0.3s ease; /* Smooth transition for the filter */
}
.sidebar-toggle {
display: block;
}
body.sidebar-active .content {
/* Don't push content, but apply a visual effect */
filter: blur(3px) brightness(0.6);
/* pointer-events: none; REMOVED to allow interaction even if sidebar is active */
}
/* The body itself gets an overlay to darken it */
body.sidebar-active::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.4);
z-index: 1010; /* Below sidebar, above content */
}
}
h1, .h1 {
color: #333 !important; /* Color oscuro para los títulos */
}
/* Card Styles */
.card {
border: none;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
margin-bottom: 20px;
}
.card-header {
background-color: #fff;
border-bottom: 1px solid #e0e0e0;
padding: 1rem 1.5rem;
font-weight: 600;
}
.card-title {
margin: 0;
font-size: 1.2rem;
}
/* Button Styles */
.btn-primary {
background-color: #3498db;
border-color: #3498db;
transition: background-color 0.3s;
}
.btn-primary:hover {
background-color: #2980b9;
border-color: #2980b9;
}
.btn-info {
background-color: #1abc9c;
border-color: #1abc9c;
}
.btn-info:hover {
background-color: #16a085;
border-color: #16a085;
}
/* Table Styles */
.table-responsive {
border-radius: 8px;
overflow: hidden; /* This is important to make border-radius work on tables */
border: 1px solid #dee2e6;
}
.table {
margin-bottom: 0;
}
.table thead th {
background-color: #f8f9fa;
border-bottom-width: 1px;
font-weight: 600;
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: rgba(0,0,0,0.02);
}
.badge {
font-size: 0.85rem;
padding: 0.5em 0.9em;
}
/* Form Styles */
.form-label {
font-weight: 500;
}
.form-control, .form-select {
border-radius: 5px;
}
.form-control:focus, .form-select:focus {
border-color: #3498db;
box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25);
}
/* Payment Verification Status Styles */
.badge.estado-pago-pendiente {
background-color: #fcf8e3 !important;
color: #8a6d3b !important;
padding: 8px 12px;
border-radius: 4px;
font-weight: 500;
}
.badge.estado-pago-verificado {
background-color: #2E8B57 !important;
color: #fff !important;
padding: 8px 12px;
border-radius: 4px;
font-weight: 500;
}
.td-pago-verificado {
background-color: #d4edda !important;
}
/* Style for the select element inside the badge */
.form-select-pago {
background-color: transparent;
border: none;
color: inherit; /* Inherit text color from parent badge */
font-weight: inherit; /* Inherit font weight */
padding: 0;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
.form-select-pago:focus {
box-shadow: none;
}
/* Make the select options have a standard background */
.form-select-pago option {
background-color: #fff;
color: #333;
}
/* Excel-like table for specific pages */
.excel-container {
overflow: auto !important;
max-height: 75vh;
border: 1px solid #dee2e6;
border-radius: 8px;
position: relative;
}
.excel-container .table {
min-width: 1200px; /* Ancho más moderado para que se vea más compacto */
margin-bottom: 0;
border-collapse: separate;
border-spacing: 0;
table-layout: fixed; /* Ayuda a controlar mejor los anchos de columna */
}
.excel-container th {
position: sticky;
top: 0;
z-index: 20;
background-color: #f8f9fa !important;
white-space: nowrap;
box-shadow: inset 0 -1px 0 #dee2e6;
font-size: 0.85rem;
padding: 8px 10px !important;
}
/* Fijar primera columna (ID) */
.excel-container th:first-child,
.excel-container td:first-child {
position: sticky;
left: 0;
z-index: 15;
background-color: #f8f9fa !important;
box-shadow: inset -1px 0 0 #dee2e6;
width: 35px; /* Ancho fijo para el ID */
}
.excel-container th:first-child {
z-index: 30; /* Esquina superior izquierda */
}
.excel-container td {
white-space: normal; /* Permitir que el texto baje a la siguiente línea */
word-wrap: break-word;
padding: 6px 10px !important; /* Padding más reducido */
vertical-align: middle;
background-color: #fff;
font-size: 0.85rem; /* Fuente más pequeña como en Pedidos Rotulados */
line-height: 1.2;
}
.excel-container tr:nth-of-type(odd) td {
background-color: #f9f9f9;
}
.excel-container tr:nth-of-type(odd) td:first-child {
background-color: #f1f1f1 !important;
}
/* Barras de desplazamiento siempre visibles y más gruesas */
.excel-container::-webkit-scrollbar {
width: 14px;
height: 14px;
}
.excel-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.excel-container::-webkit-scrollbar-thumb {
background: #888;
border-radius: 10px;
border: 3px solid #f1f1f1;
}
.excel-container::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Custom Search Table Styles */
.custom-search-table {
border-collapse: separate;
border-spacing: 0;
width: 100%;
}
.custom-search-table thead th {
background-color: #e9ecef; /* A light grey for the header */
color: #495057;
font-weight: 600;
text-align: left;
padding: 12px 15px;
border-bottom: 2px solid #dee2e6;
}
.custom-search-table tbody td {
padding: 12px 15px;
border-bottom: 1px solid #ecf0f1; /* Light border for rows */
color: #555;
}
.custom-search-table tbody tr:last-child td {
border-bottom: none; /* No border for the last row */
}
.custom-search-table tbody tr:hover {
background-color: #f8f9fa; /* Subtle hover effect */
}
/* Submenu Styles */
.sidebar .submenu {
display: none;
list-style: none;
padding-left: 20px;
background-color: #2c3e50;
}
/* Show submenu when it has .show class or its parent .has-submenu is active */
.sidebar .submenu.show,
.sidebar .nav-item.has-submenu.active > .submenu {
display: block;
}
/* Keep the submenu open when the parent has the 'open' class (for JS toggle) */
.sidebar .nav-item.open > .submenu {
display: block;
}
.sidebar .submenu .nav-link {
padding-left: 35px;
color: #bdc3c7; /* Match main link color */
}
/* Style for the active link within the submenu */
.sidebar .submenu .nav-link.active {
color: #fff; /* White color for active sub-item */
font-weight: bold;
}
@media (max-width: 767px) {
.content {
padding: 15px;
}
.card-header, .card-body {
padding: 1rem;
}
h1, .h1 {
font-size: 1.75rem;
}
.table {
font-size: 0.9rem;
}
.table td, .table th {
padding: 0.5rem;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

225
buscador_general.php Normal file
View File

@ -0,0 +1,225 @@
<?php
require_once 'layout_header.php';
require_once 'db/config.php';
// Function from pedidos.php to style the status
function getStatusStyle($status) {
$style = 'color: white;'; // Default text color
$bgColor = '#0dcaf0'; // Default info blue
switch (strtoupper(trim($status))) {
case 'ROTULADO':
$bgColor = '#ffc107'; // yellow
$style = 'color: black;';
break;
case 'EN TRANSITO':
$bgColor = '#90EE90'; // light green
$style = 'color: black;';
break;
case 'EN DESTINO':
$bgColor = '#800080'; // purple
break;
case 'COMPLETADO':
case 'COMPLETADO ✅':
$bgColor = '#198754'; // dark green
break;
}
return "background-color: {$bgColor} !important; {$style}";
}
$search_term = '';
$fecha_creacion = '';
$asesor_id = '';
$pedidos = [];
$is_search_performed = false;
$error_message = null;
$user_role = $_SESSION['user_role'] ?? 'Asesor';
$asesores = [];
try {
$pdo = db();
// Get asesores for the dropdown if user is superadmin
if ($user_role === 'superadmin') {
$stmt_asesores = $pdo->query("SELECT id, nombre_asesor FROM users WHERE role = 'Asesor' ORDER BY nombre_asesor");
$asesores = $stmt_asesores->fetchAll(PDO::FETCH_ASSOC);
}
if (isset($_GET['q']) || isset($_GET['fecha_creacion']) || isset($_GET['asesor_id'])) {
$is_search_performed = true;
$search_term = trim($_GET['q'] ?? '');
// Only process these filters if the user is a superadmin
if ($user_role === 'superadmin') {
$fecha_creacion = trim($_GET['fecha_creacion'] ?? '');
$asesor_id = trim($_GET['asesor_id'] ?? '');
} else {
$fecha_creacion = '';
$asesor_id = '';
}
$sql_conditions = [];
$params = [];
if ($search_term !== '') {
$sql_conditions[] = "(p.nombre_completo LIKE :term OR p.celular LIKE :term OR p.dni_cliente LIKE :term OR p.id = :id_term)";
$params['term'] = '%' . $search_term . '%';
$params['id_term'] = is_numeric($search_term) ? $search_term : 0;
}
if ($fecha_creacion !== '' && $user_role === 'superadmin') {
$sql_conditions[] = "DATE(p.created_at) = :fecha_creacion";
$params['fecha_creacion'] = $fecha_creacion;
}
if ($asesor_id !== '' && $user_role === 'superadmin') {
$sql_conditions[] = "p.asesor_id = :asesor_id";
$params['asesor_id'] = $asesor_id;
}
if (!empty($sql_conditions)) {
$sql = "
SELECT p.*, u.nombre_asesor as asesor_nombre
FROM pedidos p
LEFT JOIN users u ON p.asesor_id = u.id
WHERE " . implode(' AND ', $sql_conditions) . "
ORDER BY p.created_at DESC
";
$stmt = $pdo->prepare($sql);
if (!$stmt->execute($params)) {
$error_info = $stmt->errorInfo();
throw new PDOException("Error en la consulta SQL: " . $error_info[2]);
}
$pedidos = $stmt->fetchAll(PDO::FETCH_ASSOC);
} elseif (isset($_GET['q'])) { // Handle case where only empty q is passed
$pedidos = [];
}
}
} catch (PDOException $e) {
$error_message = "Error de base de datos: " . $e->getMessage();
} catch (Exception $e) {
$error_message = "Ha ocurrido un error inesperado.";
}
?>
<div class="container-fluid mt-4">
<h1>Buscador General</h1>
<p>Busca pedidos por nombre, teléfono, DNI o ID del pedido.</p>
<div class="card">
<div class="card-body">
<form action="buscador_general.php" method="GET" class="mb-4">
<div class="row g-3 align-items-end">
<div class="col">
<label for="q" class="form-label">Búsqueda</label>
<input type="text" class="form-control" id="q" name="q" placeholder="Introduce tu búsqueda..." value="<?php echo htmlspecialchars($search_term); ?>">
</div>
<?php if ($user_role === 'superadmin'): ?>
<div class="col">
<label for="fecha_creacion" class="form-label">Fecha de Creación</label>
<input type="date" class="form-control" id="fecha_creacion" name="fecha_creacion" value="<?php echo htmlspecialchars($fecha_creacion); ?>">
</div>
<div class="col">
<label for="asesor_id" class="form-label">Asesor</label>
<select class="form-select" id="asesor_id" name="asesor_id">
<option value="">Todos los asesores</option>
<?php foreach ($asesores as $asesor): ?>
<option value="<?php echo $asesor['id']; ?>" <?php echo ($asesor_id == $asesor['id']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($asesor['nombre_asesor']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<div class="col-auto">
<button class="btn btn-primary" type="submit">Buscar</button>
</div>
</div>
</form>
<?php if ($error_message): ?>
<div class="alert alert-danger">
<?php echo htmlspecialchars($error_message); ?>
</div>
<?php elseif ($is_search_performed): ?>
<hr>
<h3>Resultados de la Búsqueda</h3>
<?php if (count($pedidos) > 0): ?>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Cliente</th>
<th>DNI</th>
<th>Celular</th>
<th>Producto</th>
<th>Sede de Envío</th>
<?php if ($user_role !== 'Logistica'): ?>
<th>Monto Total</th>
<th>Monto Debe</th>
<?php endif; ?>
<th> De Orden</th>
<th>Codigo De Orden</th>
<th>CLAVE</th>
<th>Estado</th>
<?php if ($user_role !== 'Asesor'): ?><th>Asesor</th><?php endif; ?>
<th>Fecha Creación</th>
</tr>
</thead>
<tbody>
<?php foreach ($pedidos as $pedido): ?>
<tr>
<td><?php echo htmlspecialchars($pedido['id']); ?></td>
<td><?php echo htmlspecialchars($pedido['nombre_completo']); ?></td>
<td><?php echo htmlspecialchars($pedido['dni_cliente'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($pedido['celular']); ?></td>
<td><?php echo htmlspecialchars($pedido['producto']); ?></td>
<td><?php echo htmlspecialchars($pedido['sede_envio'] ?? 'N/A'); ?></td>
<?php if ($user_role !== 'Logistica'): ?>
<td><?php echo htmlspecialchars($pedido['monto_total']); ?></td>
<td><?php echo htmlspecialchars($pedido['monto_debe']); ?></td>
<?php endif; ?>
<td><?php echo htmlspecialchars($pedido['codigo_rastreo'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($pedido['codigo_tracking'] ?? 'N/A'); ?></td>
<td>
<?php
$show_clave = true;
if ($user_role === 'Asesor') {
$estado_actual = strtoupper(trim($pedido['estado']));
if ($estado_actual !== 'COMPLETADO' && $estado_actual !== 'COMPLETADO ✅') {
$show_clave = false;
}
}
if ($show_clave) {
echo htmlspecialchars($pedido['clave'] ?? 'N/A');
} else {
echo '<i class="fas fa-eye-slash text-muted" title="Suba el número de operación y seleccione el banco para ver la clave"></i> <span class="text-muted" style="font-size: 0.8rem;">Oculto</span>';
}
?>
</td>
<td><span class="badge" style="<?php echo getStatusStyle($pedido['estado']); ?>"><?php echo htmlspecialchars($pedido['estado']); ?></span></td>
<?php if ($user_role !== 'Asesor'): ?><td><?php echo htmlspecialchars($pedido['asesor_nombre'] ?? 'N/A'); ?></td><?php endif; ?>
<td><?php echo htmlspecialchars($pedido['created_at']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="alert alert-info">
No se encontraron pedidos que coincidan con los criterios de búsqueda.
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
</div>
<?php require_once 'layout_footer.php'; ?>

145
buscador_inventario.php Normal file
View File

@ -0,0 +1,145 @@
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
require_once 'db/config.php';
require_once 'layout_header.php';
$codigo_unico_buscado = isset($_GET['codigo_unico']) ? trim($_GET['codigo_unico']) : '';
$unidad_info = null;
$error_message = '';
if (!empty($codigo_unico_buscado)) {
try {
$db = db();
$stmt = $db->prepare(
"\n SELECT
ui.codigo_unico,
ui.estado,
ui.fecha_creacion,
ui.fecha_ingreso,
ui.fecha_salida,
ui.pedido_id,
p.nombre AS producto_nombre,
p.sku AS producto_sku
FROM unidades_inventario ui
JOIN products p ON ui.producto_id = p.id
WHERE ui.codigo_unico = :codigo_unico
");
$stmt->bindParam(':codigo_unico', $codigo_unico_buscado, PDO::PARAM_STR);
$stmt->execute();
$unidad_info = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$unidad_info) {
$error_message = "No se encontró ninguna unidad de inventario con el código \"" . htmlspecialchars($codigo_unico_buscado) . "\".";
}
} catch (PDOException $e) {
$error_message = "Error en la base de datos: " . $e->getMessage();
}
}
?>
<div class="container mt-4">
<h1 class="mb-4">Buscador de Unidades de Inventario</h1>
<p>Ingresa o escanea el código de barras de una unidad individual para ver su historial y estado actual.</p>
<div class="card shadow-sm mb-4">
<div class="card-body">
<form action="buscador_inventario.php" method="GET" class="mb-3">
<div class="input-group">
<input type="text" class="form-control form-control-lg" id="codigo_unico" name="codigo_unico" placeholder="Ej: AFS-0035" value="<?php echo htmlspecialchars($codigo_unico_buscado); ?>" required autofocus>
<button class="btn btn-primary" type="submit">
<i class="fas fa-search"></i> Buscar
</button>
</div>
</form>
</div>
</div>
<?php if ($error_message): ?>
<div class="alert alert-warning">
<?php echo $error_message; ?>
</div>
<?php endif; ?>
<?php if ($unidad_info): ?>
<div class="card shadow-sm">
<div class="card-header">
<h5 class="mb-0">Resultados para: <?php echo htmlspecialchars($unidad_info['codigo_unico']); ?></h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h4>Información del Producto</h4>
<table class="table table-bordered">
<tr>
<th style="width: 30%;">Nombre</th>
<td><?php echo htmlspecialchars($unidad_info['producto_nombre']); ?></td>
</tr>
<tr>
<th>SKU</th>
<td><?php echo htmlspecialchars($unidad_info['producto_sku']); ?></td>
</tr>
</table>
</div>
<div class="col-md-6">
<h4>Trazabilidad de la Unidad</h4>
<table class="table table-bordered">
<tr>
<th style="width: 30%;">Estado Actual</th>
<td>
<?php
$estado = htmlspecialchars($unidad_info['estado']);
$badge_class = 'secondary';
if ($estado == 'En Almacén') $badge_class = 'success';
if ($estado == 'Vendido') $badge_class = 'danger';
if ($estado == 'Generado') $badge_class = 'info';
echo "<span class=\"badge bg-$badge_class\">$estado</span>";
?>
</td>
</tr>
<tr>
<th>Pedido ID</th>
<td>
<?php if ($unidad_info['pedido_id']): ?>
<a href="info_producto.php?id=<?php echo $unidad_info['pedido_id']; // Asumiendo que esta es la página de detalle de pedido ?>">
<?php echo $unidad_info['pedido_id']; ?>
</a>
<?php else: ?>
N/A
<?php endif; ?>
</td>
</tr>
</table>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h4>Historial de Fechas</h4>
<table class="table table-sm table-bordered">
<thead>
<tr>
<th>Creación (Etiqueta)</th>
<th>Ingreso a Almacén</th>
<th>Salida (Venta)</th>
</tr>
</thead>
<tbody>
<tr>
<td><?php echo $unidad_info['fecha_creacion'] ? date('d/m/Y H:i:s', strtotime($unidad_info['fecha_creacion'])) : 'N/A'; ?></td>
<td><?php echo $unidad_info['fecha_ingreso'] ? date('d/m/Y H:i:s', strtotime($unidad_info['fecha_ingreso'])) : 'N/A'; ?></td>
<td><?php echo $unidad_info['fecha_salida'] ? date('d/m/Y H:i:s', strtotime($unidad_info['fecha_salida'])) : 'N/A'; ?></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php endif; ?>
</div>
<?php require_once 'layout_footer.php'; ?>

476
calculo_costos.php Normal file
View File

@ -0,0 +1,476 @@
<?php
$pageTitle = "Cálculo de Costos";
include 'db/config.php';
include 'layout_header.php';
$db = db();
// Obtener productos para el select del modal
$stmt_products = $db->query("SELECT id, nombre FROM products ORDER BY nombre ASC");
$productos_select = $stmt_products->fetchAll(PDO::FETCH_ASSOC);
// Obtener videos y sus costos asociados
$stmt = $db->query("SELECT mv.id, mv.orden, mv.foto_producto, p.nombre as nombre_producto,
mc.costo_producto, mc.costo_fijo_film, mc.comision_asesora,
mc.delivery, mc.costo_publicitario, mc.inversion_total,
mc.promo_1, mc.promo_2, mc.promo_3
FROM marketing_videos mv
LEFT JOIN products p ON mv.producto_id = p.id
LEFT JOIN marketing_costos mc ON mv.id = mc.video_id
ORDER BY mv.orden ASC, mv.fecha_creacion DESC");
$costos = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<style>
.table-excel {
font-size: 0.85rem;
}
.table-excel th {
background-color: #f8f9fa;
border-bottom: 2px solid #dee2e6;
white-space: nowrap;
text-transform: uppercase;
font-weight: 600;
color: #495057;
}
.table-excel td {
vertical-align: middle;
border-bottom: 1px solid #eee;
}
.img-preview {
width: 50px;
height: 50px;
object-fit: cover;
border-radius: 4px;
}
.editable {
background-color: #ffffff;
transition: all 0.2s;
position: relative;
min-width: 80px;
pointer-events: auto !important;
}
.editable:hover {
background-color: #f0f7ff !important;
cursor: pointer;
box-shadow: inset 0 0 0 2px #0d6efd;
z-index: 1;
}
.edit-icon {
position: absolute;
right: 4px;
top: 4px;
font-size: 0.7rem;
color: #0d6efd;
background: rgba(255,255,255,0.9);
padding: 2px 4px;
border-radius: 3px;
border: 1px solid #0d6efd;
display: none;
z-index: 10;
}
.editable:hover .edit-icon {
display: block;
}
.recaudo-cell {
background-color: #fdfdfd;
cursor: default;
color: #6c757d;
}
.inline-edit-input {
width: 100%;
padding: 4px 8px;
font-size: 0.85rem;
border: 2px solid #0d6efd;
border-radius: 4px;
text-align: center;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
pointer-events: auto !important;
}
.bg-total {
background-color: #f1f3f5;
font-weight: bold;
}
.btn-delete-row {
padding: 0.2rem 0.4rem;
font-size: 0.75rem;
opacity: 0.3;
transition: opacity 0.2s;
pointer-events: auto !important;
}
tr:hover .btn-delete-row {
opacity: 1;
}
/* Fix for modal selection and backdrop issues */
.modal {
z-index: 2000 !important;
}
.modal-backdrop {
z-index: 1900 !important;
}
.modal-content {
box-shadow: 0 5px 15px rgba(0,0,0,0.5);
pointer-events: auto !important;
}
</style>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="mb-0">Cálculo de Costos</h2>
<p class="text-muted small">Gestión de costos por producto de marketing. <span class="badge bg-info text-dark">Haz clic en las celdas blancas para editar</span></p>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="alert('JavaScript está funcionando correctamente en este navegador.')">
<i class="fas fa-bug"></i> Probar JS
</button>
<button type="button" class="btn btn-primary shadow-sm" onclick="abrirModalNuevo()">
<i class="fas fa-plus me-2"></i> Nuevo Producto
</button>
</div>
</div>
<?php if (isset($_GET['success'])): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
¡Operación realizada con éxito!
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<div class="card shadow-sm border-0">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover table-excel mb-0" id="costosTable">
<thead>
<tr>
<th class="text-center">Orden</th>
<th>Producto</th>
<th class="text-center">Imagen</th>
<th class="text-center">Costo Producto</th>
<th class="text-center">Costo Fijo Film</th>
<th class="text-center">Comisión Asesora</th>
<th class="text-center">Delivery</th>
<th class="text-center">Costo Publicitario</th>
<th class="text-center bg-total">Inversión Total</th>
<th class="text-center">Promo 1</th>
<th class="text-center recaudo-cell">Recaudo 1 (Auto)</th>
<th class="text-center">Promo 2</th>
<th class="text-center recaudo-cell">Recaudo 2 (Auto)</th>
<th class="text-center">Promo 3</th>
<th class="text-center recaudo-cell">Recaudo 3 (Auto)</th>
<th class="text-center">Acción</th>
</tr>
</thead>
<tbody>
<?php if (empty($costos)): ?>
<tr>
<td colspan="16" class="text-center py-4 text-muted">No hay videos registrados en producción.</td>
</tr>
<?php else: ?>
<?php foreach ($costos as $c): ?>
<?php
$inversion_total = ($c['costo_producto'] ?? 0) +
($c['costo_fijo_film'] ?? 0) +
($c['comision_asesora'] ?? 0) +
($c['delivery'] ?? 0) +
($c['costo_publicitario'] ?? 0);
$costos_operativos = ($c['costo_fijo_film'] ?? 0) +
($c['comision_asesora'] ?? 0) +
($c['delivery'] ?? 0) +
($c['costo_publicitario'] ?? 0);
function calculateRecaudo($promo, $costos) {
if (!$promo || $promo == '-') return null;
preg_match('/[\d.]+/', $promo, $matches);
$val = isset($matches[0]) ? floatval($matches[0]) : 0;
return $val > 0 ? $val - $costos : null;
}
$recaudo1 = calculateRecaudo($c['promo_1'], $costos_operativos);
$recaudo2 = calculateRecaudo($c['promo_2'], $costos_operativos);
$recaudo3 = calculateRecaudo($c['promo_3'], $costos_operativos);
?>
<tr data-row-id="<?php echo $c['id']; ?>">
<td class="text-center fw-bold text-primary"><?php echo $c['orden']; ?></td>
<td class="fw-bold"><?php echo htmlspecialchars($c['nombre_producto'] ?: 'General'); ?></td>
<td class="text-center">
<?php if ($c['foto_producto']): ?>
<img src="<?php echo $c['foto_producto']; ?>" class="img-preview" alt="Ref">
<?php else: ?>
<span class="text-muted small">Sin foto</span>
<?php endif; ?>
</td>
<td class="text-center editable" data-id="<?php echo $c['id']; ?>" data-field="costo_producto" onclick="startEdit(this)">
S/ <?php echo number_format($c['costo_producto'] ?? 0, 2); ?>
<span class="edit-icon"><i class="fas fa-pencil-alt"></i></span>
</td>
<td class="text-center editable" data-id="<?php echo $c['id']; ?>" data-field="costo_fijo_film" onclick="startEdit(this)">
S/ <?php echo number_format($c['costo_fijo_film'] ?? 0, 2); ?>
<span class="edit-icon"><i class="fas fa-pencil-alt"></i></span>
</td>
<td class="text-center editable" data-id="<?php echo $c['id']; ?>" data-field="comision_asesora" onclick="startEdit(this)">
S/ <?php echo number_format($c['comision_asesora'] ?? 0, 2); ?>
<span class="edit-icon"><i class="fas fa-pencil-alt"></i></span>
</td>
<td class="text-center editable" data-id="<?php echo $c['id']; ?>" data-field="delivery" onclick="startEdit(this)">
S/ <?php echo number_format($c['delivery'] ?? 0, 2); ?>
<span class="edit-icon"><i class="fas fa-pencil-alt"></i></span>
</td>
<td class="text-center editable" data-id="<?php echo $c['id']; ?>" data-field="costo_publicitario" onclick="startEdit(this)">
S/ <?php echo number_format($c['costo_publicitario'] ?? 0, 2); ?>
<span class="edit-icon"><i class="fas fa-pencil-alt"></i></span>
</td>
<td class="text-center bg-total inversion-total" id="total-<?php echo $c['id']; ?>">
S/ <?php echo number_format($inversion_total, 2); ?>
</td>
<td class="text-center editable" data-id="<?php echo $c['id']; ?>" data-field="promo_1" onclick="startEdit(this)">
<?php echo htmlspecialchars($c['promo_1'] ?: '-'); ?>
<span class="edit-icon"><i class="fas fa-pencil-alt"></i></span>
</td>
<td class="text-center recaudo-cell recaudo-1 <?php echo $recaudo1 !== null ? ($recaudo1 >= 0 ? 'text-success fw-bold' : 'text-danger fw-bold') : 'text-muted'; ?>" data-id="<?php echo $c['id']; ?>" title="Calculado automáticamente (Promo - Costos)">
<?php echo $recaudo1 !== null ? 'S/ ' . number_format($recaudo1, 2) : '-'; ?>
</td>
<td class="text-center editable" data-id="<?php echo $c['id']; ?>" data-field="promo_2" onclick="startEdit(this)">
<?php echo htmlspecialchars($c['promo_2'] ?: '-'); ?>
<span class="edit-icon"><i class="fas fa-pencil-alt"></i></span>
</td>
<td class="text-center recaudo-cell recaudo-2 <?php echo $recaudo2 !== null ? ($recaudo2 >= 0 ? 'text-success fw-bold' : 'text-danger fw-bold') : 'text-muted'; ?>" data-id="<?php echo $c['id']; ?>" title="Calculado automáticamente (Promo - Costos)">
<?php echo $recaudo2 !== null ? 'S/ ' . number_format($recaudo2, 2) : '-'; ?>
</td>
<td class="text-center editable" data-id="<?php echo $c['id']; ?>" data-field="promo_3" onclick="startEdit(this)">
<?php echo htmlspecialchars($c['promo_3'] ?: '-'); ?>
<span class="edit-icon"><i class="fas fa-pencil-alt"></i></span>
</td>
<td class="text-center recaudo-cell recaudo-3 <?php echo $recaudo3 !== null ? ($recaudo3 >= 0 ? 'text-success fw-bold' : 'text-danger fw-bold') : 'text-muted'; ?>" data-id="<?php echo $c['id']; ?>" title="Calculado automáticamente (Promo - Costos)">
<?php echo $recaudo3 !== null ? 'S/ ' . number_format($recaudo3, 2) : '-'; ?>
</td>
<td class="text-center">
<button class="btn btn-outline-danger btn-delete-row" onclick="eliminarProducto(<?php echo $c['id']; ?>)" title="Eliminar">
<i class="fas fa-trash-alt"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Modal Nuevo Video -->
<div class="modal fade" id="nuevoVideoModal" tabindex="-1" aria-labelledby="nuevoVideoModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<form action="save_marketing_video.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="redirect" value="calculo_costos.php?success=created">
<div class="modal-header">
<h5 class="modal-title" id="nuevoVideoModalLabel">Nuevo Producto para Marketing</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row g-3">
<div class="col-md-2">
<label class="form-label">Orden</label>
<input type="number" name="orden" class="form-control" value="1">
</div>
<div class="col-md-5">
<label class="form-label">Fecha Entrega</label>
<input type="date" name="fecha_entrega" class="form-control" value="<?php echo date('Y-m-d'); ?>">
</div>
<div class="col-md-5">
<label class="form-label">Producto</label>
<select name="producto_id" class="form-select" required>
<option value="">Seleccionar producto...</option>
<?php foreach ($productos_select as $p): ?>
<option value="<?php echo $p['id']; ?>"><?php echo htmlspecialchars($p['nombre']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Imagen Referencia</label>
<input type="file" name="foto_producto" class="form-control" accept="image/*">
</div>
<div class="col-md-6">
<label class="form-label">Material</label>
<input type="text" name="material" class="form-control" placeholder="Ej: Acero, Plástico, etc.">
</div>
<div class="col-12">
<label class="form-label">Instrucciones Adicionales</label>
<textarea name="instrucciones" class="form-control" rows="3"></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-primary">Agregar Producto</button>
</div>
</form>
</div>
</div>
</div>
<script>
// Funciones globales para asegurar que funcionen incluso si jQuery tarda en cargar
function abrirModalNuevo() {
try {
const modalEl = document.getElementById('nuevoVideoModal');
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
modal.show();
} catch (e) {
console.error("Error al abrir modal:", e);
// Fallback manual si bootstrap falla
$('#nuevoVideoModal').modal('show');
}
}
function startEdit(element) {
const cell = $(element);
if (cell.find('input').length > 0) return;
// Evitar selección de texto
if (window.getSelection) {
window.getSelection().removeAllRanges();
}
const id = cell.data('id');
const field = cell.data('field');
// Obtener el valor limpio (sin S/ ni comas)
let currentValue = cell.text().trim().replace('S/ ', '').replace(/,/g, '');
if (currentValue === '-') currentValue = '';
const isPromo = field.startsWith('promo_');
const input = $('<input>', {
type: isPromo ? 'text' : 'number',
class: 'form-control form-control-sm inline-edit-input',
value: currentValue
});
if (!isPromo) {
input.attr('step', '0.01');
}
const originalContent = cell.html();
cell.empty().append(input);
input.focus().select();
let isSaving = false;
const saveChange = () => {
if (isSaving) return;
const newValue = input.val();
if (newValue === currentValue) {
cell.html(originalContent);
return;
}
isSaving = true;
input.prop('disabled', true);
$.ajax({
url: 'update_marketing_costos_field.php',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ id, field, value: newValue }),
success: function(data) {
if (data.success) {
if (isPromo) {
cell.html((newValue || '-') + '<span class="edit-icon"><i class="fas fa-pencil-alt"></i></span>');
} else {
const formatted = parseFloat(newValue || 0).toLocaleString('es-PE', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
cell.html('S/ ' + formatted + '<span class="edit-icon"><i class="fas fa-pencil-alt"></i></span>');
}
updateTotal(id);
} else {
alert('Error: ' + data.error);
cell.html(originalContent);
}
},
error: function() {
alert('Error de conexión al guardar.');
cell.html(originalContent);
},
complete: function() {
isSaving = false;
}
});
};
input.on('blur', saveChange);
input.on('keydown', function(e) {
if (e.key === 'Enter') {
input.blur();
} else if (e.key === 'Escape') {
input.off('blur');
cell.html(originalContent);
}
});
}
function updateTotal(id) {
const row = $(`tr[data-row-id="${id}"]`);
if (!row.length) return;
const costFields = ['costo_fijo_film', 'comision_asesora', 'delivery', 'costo_publicitario'];
const allFields = ['costo_producto', ...costFields];
let totalInversion = 0;
let totalCostosOperativos = 0;
allFields.forEach(field => {
const cell = row.find(`[data-field="${field}"]`);
if (cell.length) {
const val = parseFloat(cell.text().replace('S/ ', '').replace(/,/g, '') || 0);
totalInversion += val;
if (costFields.includes(field)) {
totalCostosOperativos += val;
}
}
});
$(`#total-${id}`).text('S/ ' + totalInversion.toLocaleString('es-PE', {minimumFractionDigits: 2, maximumFractionDigits: 2}));
// Update Recaudos
[1, 2, 3].forEach(num => {
const promoCell = row.find(`[data-field="promo_${num}"]`);
const recaudoCell = row.find(`.recaudo-${num}`);
if (promoCell.length && recaudoCell.length) {
const promoText = promoCell.text().trim();
const promoValMatch = promoText.match(/[\d.]+/);
const promoVal = promoValMatch ? parseFloat(promoValMatch[0]) : 0;
if (promoVal > 0) {
const recaudo = promoVal - totalCostosOperativos;
recaudoCell.text('S/ ' + recaudo.toLocaleString('es-PE', {minimumFractionDigits: 2, maximumFractionDigits: 2}));
recaudoCell.removeClass('text-muted').addClass('fw-bold');
if (recaudo >= 0) {
recaudoCell.addClass('text-success').removeClass('text-danger');
} else {
recaudoCell.addClass('text-danger').removeClass('text-success');
}
} else {
recaudoCell.text('-').addClass('text-muted').removeClass('fw-bold text-success text-danger');
}
}
});
}
function eliminarProducto(id) {
if (confirm('¿Estás seguro de que deseas eliminar este producto de la lista de costos?')) {
window.location.href = 'delete_marketing_video.php?id=' + id + '&redirect=calculo_costos.php';
}
}
$(document).ready(function() {
// Mover el modal al body para evitar problemas de z-index con el backdrop
$('#nuevoVideoModal').appendTo('body');
});
</script>
<?php include 'layout_footer.php'; ?>

371
calculo_costos_v2.php Normal file
View File

@ -0,0 +1,371 @@
<?php
header("Location: calculo_costos_v3.php");
exit();
$pageTitle = "Cálculo de Costos V2";
include 'db/config.php';
include 'layout_header.php';
$db = db();
// Obtener productos para el select del modal
$stmt_products = $db->query("SELECT id, nombre FROM products ORDER BY nombre ASC");
$productos_select = $stmt_products->fetchAll(PDO::FETCH_ASSOC);
// Obtener videos y sus costos asociados
$stmt = $db->query("SELECT mv.id, mv.orden, mv.foto_producto, p.nombre as nombre_producto,
mc.costo_producto, mc.costo_fijo_film, mc.comision_asesora,
mc.delivery, mc.costo_publicitario, mc.inversion_total,
mc.promo_1, mc.promo_2, mc.promo_3
FROM marketing_videos mv
LEFT JOIN products p ON mv.producto_id = p.id
LEFT JOIN marketing_costos mc ON mv.id = mc.video_id
ORDER BY mv.orden ASC, mv.fecha_creacion DESC");
$costos = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<style>
.table-v2 {
font-size: 0.85rem;
border-collapse: separate;
border-spacing: 0 5px;
}
.table-v2 th {
background-color: #f1f3f5;
border-bottom: 2px solid #dee2e6;
white-space: nowrap;
padding: 12px 8px;
position: sticky;
top: 0;
z-index: 10;
}
.table-v2 td {
vertical-align: middle;
padding: 8px 4px;
background-color: #fff;
}
.input-cell {
width: 100%;
min-width: 85px;
padding: 8px;
border: 2px solid #e9ecef;
border-radius: 6px;
text-align: center;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.2s;
cursor: text !important;
pointer-events: auto !important;
}
.input-cell:focus {
border-color: #0d6efd;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.15);
outline: 0;
background-color: #fff;
}
.input-cell:hover {
border-color: #adb5bd;
}
.input-cell.saving {
background-color: #fff3cd !important;
border-color: #ffc107 !important;
}
.input-cell.saved {
background-color: #d1e7dd !important;
border-color: #198754 !important;
}
.recaudo-v2 {
background-color: #f8f9fa;
font-weight: bold;
text-align: center;
padding: 10px;
border-radius: 6px;
min-width: 100px;
border: 1px solid #dee2e6;
}
.img-v2 {
width: 45px;
height: 45px;
object-fit: cover;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.btn-action {
width: 32px;
height: 32px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 6px;
}
.editable-label {
font-size: 0.7rem;
color: #6c757d;
display: block;
margin-bottom: 2px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
</style>
<div class="d-flex justify-content-between align-items-center mb-4 mt-2">
<div>
<p class="text-muted mb-0">Edición directa: Escribe en los cuadros y los cambios se guardarán al salir del cuadro.</p>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-secondary" onclick="location.reload()">
<i class="fas fa-sync-alt"></i> Actualizar Todo
</button>
<button type="button" class="btn btn-primary shadow-sm" data-bs-toggle="modal" data-bs-target="#modalNuevoV2">
<i class="fas fa-plus me-2"></i> Nuevo Producto
</button>
</div>
</div>
<?php if (isset($_GET['success'])): ?>
<div class="alert alert-success alert-dismissible fade show shadow-sm" role="alert">
<i class="fas fa-check-circle me-2"></i> ¡Operación realizada con éxito!
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<div class="card shadow-sm border-0">
<div class="card-body p-0">
<div class="table-responsive" style="max-height: 70vh;">
<table class="table table-v2 mb-0">
<thead>
<tr>
<th class="text-center">Orden</th>
<th>Producto</th>
<th class="text-center">Imagen</th>
<th class="text-center">Costo Prod.</th>
<th class="text-center">Film</th>
<th class="text-center">Asesora</th>
<th class="text-center">Delivery</th>
<th class="text-center">Publicidad</th>
<th class="text-center bg-light">Inversión Total</th>
<th class="text-center">Promo 1</th>
<th class="text-center">Recaudo 1</th>
<th class="text-center">Promo 2</th>
<th class="text-center">Recaudo 2</th>
<th class="text-center">Promo 3</th>
<th class="text-center">Recaudo 3</th>
<th class="text-center">Acción</th>
</tr>
</thead>
<tbody>
<?php foreach ($costos as $c): ?>
<?php
$inversion_total = ($c['costo_producto'] ?? 0) +
($c['costo_fijo_film'] ?? 0) +
($c['comision_asesora'] ?? 0) +
($c['delivery'] ?? 0) +
($c['costo_publicitario'] ?? 0);
function getVal($promo) {
if (empty($promo)) return 0;
preg_match('/[\d.]+/', $promo, $matches);
return isset($matches[0]) ? floatval($matches[0]) : 0;
}
$p1 = getVal($c['promo_1']);
$p2 = getVal($c['promo_2']);
$p3 = getVal($c['promo_3']);
$r1 = $p1 > 0 ? $p1 - $inversion_total : null;
$r2 = $p2 > 0 ? $p2 - $inversion_total : null;
$r3 = $p3 > 0 ? $p3 - $inversion_total : null;
?>
<tr id="row-<?php echo $c['id']; ?>" data-id="<?php echo $c['id']; ?>">
<td class="text-center fw-bold text-primary"><?php echo $c['orden']; ?></td>
<td style="max-width: 140px;">
<div class="text-truncate fw-bold" title="<?php echo htmlspecialchars($c['nombre_producto']); ?>">
<?php echo htmlspecialchars($c['nombre_producto'] ?: 'General'); ?>
</div>
</td>
<td class="text-center">
<?php if ($c['foto_producto']): ?>
<img src="<?php echo $c['foto_producto']; ?>" class="img-v2">
<?php else: ?>
<div class="bg-light rounded d-flex align-items-center justify-content-center" style="width:45px; height:45px;">
<i class="fas fa-image text-muted"></i>
</div>
<?php endif; ?>
</td>
<td><input type="number" step="0.01" class="input-cell" data-field="costo_producto" value="<?php echo $c['costo_producto']; ?>" onchange="updateRow(<?php echo $c['id']; ?>, 'costo_producto', this.value)"></td>
<td><input type="number" step="0.01" class="input-cell" data-field="costo_fijo_film" value="<?php echo $c['costo_fijo_film']; ?>" onchange="updateRow(<?php echo $c['id']; ?>, 'costo_fijo_film', this.value)"></td>
<td><input type="number" step="0.01" class="input-cell" data-field="comision_asesora" value="<?php echo $c['comision_asesora']; ?>" onchange="updateRow(<?php echo $c['id']; ?>, 'comision_asesora', this.value)"></td>
<td><input type="number" step="0.01" class="input-cell" data-field="delivery" value="<?php echo $c['delivery']; ?>" onchange="updateRow(<?php echo $c['id']; ?>, 'delivery', this.value)"></td>
<td><input type="number" step="0.01" class="input-cell" data-field="costo_publicitario" value="<?php echo $c['costo_publicitario']; ?>" onchange="updateRow(<?php echo $c['id']; ?>, 'costo_publicitario', this.value)"></td>
<td class="text-center">
<div class="recaudo-v2 bg-light fw-bold text-dark" id="total-<?php echo $c['id']; ?>">
S/ <?php echo number_format($inversion_total, 2); ?>
</div>
</td>
<td><input type="text" class="input-cell" data-field="promo_1" value="<?php echo htmlspecialchars($c['promo_1']); ?>" onchange="updateRow(<?php echo $c['id']; ?>, 'promo_1', this.value)"></td>
<td><div class="recaudo-v2 recaudo-1 <?php echo $r1 >= 0 ? 'text-success' : 'text-danger'; ?>" id="r1-<?php echo $c['id']; ?>"><?php echo $r1 !== null ? 'S/ '.number_format($r1, 2) : '-'; ?></div></td>
<td><input type="text" class="input-cell" data-field="promo_2" value="<?php echo htmlspecialchars($c['promo_2']); ?>" onchange="updateRow(<?php echo $c['id']; ?>, 'promo_2', this.value)"></td>
<td><div class="recaudo-v2 recaudo-2 <?php echo $r2 >= 0 ? 'text-success' : 'text-danger'; ?>" id="r2-<?php echo $c['id']; ?>"><?php echo $r2 !== null ? 'S/ '.number_format($r2, 2) : '-'; ?></div></td>
<td><input type="text" class="input-cell" data-field="promo_3" value="<?php echo htmlspecialchars($c['promo_3']); ?>" onchange="updateRow(<?php echo $c['id']; ?>, 'promo_3', this.value)"></td>
<td><div class="recaudo-v2 recaudo-3 <?php echo $r3 >= 0 ? 'text-success' : 'text-danger'; ?>" id="r3-<?php echo $c['id']; ?>"><?php echo $r3 !== null ? 'S/ '.number_format($r3, 2) : '-'; ?></div></td>
<td class="text-center">
<button class="btn btn-danger btn-action" onclick="eliminarRow(<?php echo $c['id']; ?>)" title="Eliminar">
<i class="fas fa-trash-alt"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Modal Nuevo -->
<div class="modal fade" id="modalNuevoV2" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<form action="save_marketing_video.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="redirect" value="calculo_costos_v2.php?success=1">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title"><i class="fas fa-plus-circle me-2"></i> Nuevo Producto para Costos</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<div class="mb-3">
<label class="form-label fw-bold">Seleccionar Producto</label>
<select name="producto_id" id="select_producto_nuevo" class="form-select form-select-lg" required onchange="fetchProductCost(this.value)">
<option value="">Seleccionar...</option>
<?php foreach ($productos_select as $p): ?>
<option value="<?php echo $p['id']; ?>"><?php echo htmlspecialchars($p['nombre']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">Costo Base (S/)</label>
<input type="number" name="costo_producto" id="costo_producto_nuevo" class="form-control" step="0.01" placeholder="0.00">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">Orden de Lista</label>
<input type="number" name="orden" class="form-control" value="1">
</div>
</div>
<div class="mb-0">
<label class="form-label fw-bold">Imagen del Producto</label>
<input type="file" name="foto_producto" class="form-control">
<small class="text-muted">Opcional. Si no se sube, se usará la del producto.</small>
</div>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-primary px-4">Crear Registro</button>
</div>
</form>
</div>
</div>
</div>
<script>
console.log("Script de Cálculo de Costos V2 cargado");
function fetchProductCost(productId) {
if (!productId) return;
fetch(`get_product_details.php?id=${productId}`)
.then(response => response.json())
.then(data => {
if (data.success && data.product.costo) {
document.getElementById('costo_producto_nuevo').value = data.product.costo;
}
})
.catch(error => console.error('Error fetching product cost:', error));
}
function updateRow(id, field, value) {
const row = document.getElementById(`row-${id}`);
const input = row.querySelector(`[data-field="${field}"]`);
if (!input) return;
input.classList.add('saving');
console.log(`Actualizando ${field} para ID ${id} con valor ${value}`);
fetch('update_marketing_costos_field.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, field, value })
})
.then(response => response.json())
.then(data => {
input.classList.remove('saving');
if (data.success) {
input.classList.add('saved');
setTimeout(() => input.classList.remove('saved'), 1500);
// Recalcular todo en la fila
recalculateRow(id);
} else {
alert('Error al guardar: ' + (data.error || 'Error desconocido'));
}
})
.catch(error => {
input.classList.remove('saving');
console.error('Error:', error);
alert('Error de conexión al servidor');
});
}
function recalculateRow(id) {
const row = document.getElementById(`row-${id}`);
if (!row) return;
const fields = ['costo_producto', 'costo_fijo_film', 'comision_asesora', 'delivery', 'costo_publicitario'];
let total = 0;
fields.forEach(f => {
const el = row.querySelector(`[data-field="${f}"]`);
if (el) {
total += parseFloat(el.value) || 0;
}
});
const totalEl = document.getElementById(`total-${id}`);
if (totalEl) {
totalEl.innerText = 'S/ ' + total.toFixed(2);
}
// Recalcular recaudos
for (let i = 1; i <= 3; i++) {
const promoEl = row.querySelector(`[data-field="promo_${i}"]`);
const recaudoEl = document.getElementById(`r${i}-${id}`);
if (promoEl && recaudoEl) {
const promoVal = promoEl.value;
const match = promoVal.match(/[\d.]+/);
const pVal = match ? parseFloat(match[0]) : 0;
if (pVal > 0) {
const recaudo = pVal - total;
recaudoEl.innerText = 'S/ ' + recaudo.toFixed(2);
recaudoEl.className = 'recaudo-v2 ' + (recaudo >= 0 ? 'text-success' : 'text-danger');
} else {
recaudoEl.innerText = '-';
recaudoEl.className = 'recaudo-v2 text-muted';
}
}
}
}
function eliminarRow(id) {
if (confirm('¿Estás seguro de que deseas eliminar este registro de costos?')) {
window.location.href = 'delete_marketing_video.php?id=' + id + '&redirect=calculo_costos_v2.php';
}
}
</script>
<?php include 'layout_footer.php'; ?>

500
calculo_costos_v3.php Normal file
View File

@ -0,0 +1,500 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user_role']) || !in_array($_SESSION['user_role'], ['Administrador', 'admin'])) {
header('Location: dashboard.php');
exit();
}
$pageTitle = "Cálculo de Costos V3";
include 'db/config.php';
include 'layout_header.php';
$db = db();
// Obtener productos para el select del modal
$stmt_products = $db->query("SELECT id, nombre FROM products ORDER BY nombre ASC");
$productos_select = $stmt_products->fetchAll(PDO::FETCH_ASSOC);
// Obtener videos y sus costos asociados de las tablas V3
$stmt = $db->query("SELECT mv.id, mv.orden, mv.foto_producto, p.nombre as nombre_producto,
mc.costo_producto, mc.costo_fijo_film, mc.comision_asesora,
mc.delivery, mc.costo_publicitario, mc.inversion_total,
mc.promo_1, mc.promo_2, mc.promo_3,
mc.comision_asesora_provincia, mc.delivery_provincia
FROM marketing_videos_v3 mv
LEFT JOIN products p ON mv.producto_id = p.id
LEFT JOIN marketing_costos_v3 mc ON mv.id = mc.video_id
ORDER BY mv.orden ASC, mv.id DESC");
$costos = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<style>
.table-v3 {
font-size: 0.8rem;
border-collapse: separate;
border-spacing: 0 5px;
}
.table-v3 th {
background-color: #f8f9fa;
border-bottom: 2px solid #dee2e6;
white-space: nowrap;
padding: 10px 5px;
position: sticky;
top: 0;
z-index: 10;
}
.table-v3 td {
vertical-align: middle;
padding: 6px 3px;
background-color: #fff;
}
.input-cell {
width: 100%;
min-width: 70px;
padding: 6px;
border: 2px solid #e9ecef;
border-radius: 6px;
text-align: center;
font-size: 0.85rem;
font-weight: 500;
transition: all 0.2s;
}
.input-cell:focus {
border-color: #0d6efd;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.15);
outline: 0;
}
.input-cell.saving { background-color: #fff3cd !important; border-color: #ffc107 !important; }
.input-cell.saved { background-color: #d1e7dd !important; border-color: #198754 !important; }
.recaudo-v3 {
background-color: #f8f9fa;
font-weight: bold;
text-align: center;
padding: 6px 4px;
border-radius: 6px;
min-width: 85px;
border: 1px solid #dee2e6;
font-size: 0.8rem;
}
.recaudo-label {
font-size: 0.65rem;
display: block;
color: #6c757d;
margin-bottom: 2px;
}
.img-v3 {
width: 120px;
height: 120px;
object-fit: cover;
border-radius: 12px;
cursor: pointer;
transition: all 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
}
.img-v3:hover {
transform: scale(2.2);
z-index: 100;
position: relative;
box-shadow: 0 15px 30px rgba(0,0,0,0.2);
}
.img-placeholder {
width: 120px;
height: 120px;
cursor: pointer;
transition: background-color 0.2s;
border-radius: 12px;
}
.img-placeholder:hover {
background-color: #e2e6ea !important;
}
.btn-action {
width: 28px;
height: 28px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 6px;
}
.bg-provincia {
background-color: #fff4e6 !important;
}
.header-provincia {
background-color: #ffe8cc !important;
color: #d9480f !important;
}
</style>
<div class="row align-items-center mb-4 mt-2">
<div class="col-md-6">
<p class="text-muted mb-0">Total de registros encontrados: <span class="fw-bold text-dark"><?php echo count($costos); ?></span></p>
</div>
<div class="col-md-6 text-end">
<button type="button" class="btn btn-outline-secondary me-2" onclick="location.reload()">
<i class="fas fa-sync-alt"></i> Actualizar
</button>
<button type="button" id="btnAbrirModalV3" class="btn btn-success btn-lg shadow-sm px-4">
<i class="fas fa-plus-circle me-2"></i> CREAR NUEVO PRODUCTO
</button>
</div>
</div>
<?php if (isset($_GET['success'])): ?>
<div class="alert alert-success alert-dismissible fade show shadow-sm" role="alert">
<i class="fas fa-check-circle me-2"></i> ¡Operación realizada con éxito!
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if (isset($_GET['error'])): ?>
<div class="alert alert-danger alert-dismissible fade show shadow-sm" role="alert">
<i class="fas fa-exclamation-triangle me-2"></i> <?php echo htmlspecialchars($_GET['error']); ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<div class="card shadow-sm border-0">
<div class="card-body p-0">
<div class="table-responsive" style="max-height: 70vh;">
<table class="table table-v3 mb-0">
<thead>
<tr>
<th class="text-center">Orden</th>
<th>Producto</th>
<th class="text-center">Imagen</th>
<th class="text-center">Costo Prod.</th>
<th class="text-center">Film</th>
<th class="text-center">Asesora (L)</th>
<th class="text-center header-provincia">Asesora (P)</th>
<th class="text-center">Delivery (L)</th>
<th class="text-center header-provincia">Delivery (P)</th>
<th class="text-center">Publicidad</th>
<th class="text-center">Promo 1</th>
<th class="text-center">Recaudo 1</th>
<th class="text-center">Promo 2</th>
<th class="text-center">Recaudo 2</th>
<th class="text-center">Promo 3</th>
<th class="text-center">Recaudo 3</th>
<th class="text-center">Acción</th>
</tr>
</thead>
<tbody>
<?php if (empty($costos)): ?>
<tr>
<td colspan="17" class="text-center py-5 text-muted">
<i class="fas fa-info-circle me-2"></i> No hay productos en esta lista. Haz clic en "Nuevo Producto" para empezar.
</td>
</tr>
<?php endif; ?>
<?php
if (!function_exists('getValV3')) {
function getValV3($promo) {
if (empty($promo)) return 0;
preg_match('/[\d.]+/', $promo, $matches);
return isset($matches[0]) ? floatval($matches[0]) : 0;
}
}
?>
<?php foreach ($costos as $c): ?>
<?php
$costo_prod = ($c['costo_producto'] ?? 0);
$costo_film = ($c['costo_fijo_film'] ?? 0);
$costo_asesora = ($c['comision_asesora'] ?? 0);
$costo_asesora_p = ($c['comision_asesora_provincia'] ?? 0);
$costo_delivery = ($c['delivery'] ?? 0);
$costo_delivery_p = ($c['delivery_provincia'] ?? 0);
$costo_publicidad = ($c['costo_publicitario'] ?? 0);
$p1 = getValV3($c['promo_1']);
$p2 = getValV3($c['promo_2']);
$p3 = getValV3($c['promo_3']);
// Cálculos Local
$r1_l = $p1 > 0 ? $p1 - ($costo_prod + $costo_film + $costo_asesora + $costo_delivery + $costo_publicidad) : null;
$r2_l = $p2 > 0 ? $p2 - (($costo_prod + $costo_film) * 2 + $costo_asesora + $costo_delivery + $costo_publicidad) : null;
$r3_l = $p3 > 0 ? $p3 - (($costo_prod + $costo_film) * 3 + $costo_asesora + $costo_delivery + $costo_publicidad) : null;
// Cálculos Provincia
$r1_p = $p1 > 0 ? $p1 - ($costo_prod + $costo_film + $costo_asesora_p + $costo_delivery_p + $costo_publicidad) : null;
$r2_p = $p2 > 0 ? $p2 - (($costo_prod + $costo_film) * 2 + $costo_asesora_p + $costo_delivery_p + $costo_publicidad) : null;
$r3_p = $p3 > 0 ? $p3 - (($costo_prod + $costo_film) * 3 + $costo_asesora_p + $costo_delivery_p + $costo_publicidad) : null;
?>
<tr id="row-<?php echo $c['id']; ?>" data-id="<?php echo $c['id']; ?>">
<td class="text-center fw-bold text-primary"><?php echo $c['orden']; ?></td>
<td style="max-width: 120px;">
<div class="text-truncate fw-bold" title="<?php echo htmlspecialchars($c['nombre_producto']); ?>">
<?php echo htmlspecialchars($c['nombre_producto'] ?: 'General'); ?>
</div>
</td>
<td class="text-center">
<div class="position-relative d-inline-block">
<?php if ($c['foto_producto']): ?>
<img src="<?php echo $c['foto_producto']; ?>" class="img-v3" onclick="triggerImageUpload(<?php echo $c['id']; ?>)">
<?php else: ?>
<div class="bg-light rounded d-flex align-items-center justify-content-center img-placeholder" onclick="triggerImageUpload(<?php echo $c['id']; ?>)">
<i class="fas fa-camera text-muted"></i>
</div>
<?php endif; ?>
<input type="file" id="file-input-<?php echo $c['id']; ?>" class="d-none" accept="image/*" onchange="uploadImage(<?php echo $c['id']; ?>)">
</div>
</td>
<td><input type="number" step="0.01" class="input-cell cost-input" data-field="costo_producto" value="<?php echo $c['costo_producto']; ?>"></td>
<td><input type="number" step="0.01" class="input-cell cost-input" data-field="costo_fijo_film" value="<?php echo $c['costo_fijo_film']; ?>"></td>
<td><input type="number" step="0.01" class="input-cell cost-input" data-field="comision_asesora" value="<?php echo $c['comision_asesora']; ?>"></td>
<td><input type="number" step="0.01" class="input-cell cost-input bg-provincia" data-field="comision_asesora_provincia" value="<?php echo $c['comision_asesora_provincia']; ?>"></td>
<td><input type="number" step="0.01" class="input-cell cost-input" data-field="delivery" value="<?php echo $c['delivery']; ?>"></td>
<td><input type="number" step="0.01" class="input-cell cost-input bg-provincia" data-field="delivery_provincia" value="<?php echo $c['delivery_provincia']; ?>"></td>
<td><input type="number" step="0.01" class="input-cell cost-input" data-field="costo_publicitario" value="<?php echo $c['costo_publicitario']; ?>"></td>
<td><input type="text" class="input-cell promo-input" data-field="promo_1" value="<?php echo htmlspecialchars($c['promo_1']); ?>"></td>
<td>
<div class="recaudo-v3 mb-1 <?php echo $r1_l >= 0 ? 'text-success' : 'text-danger'; ?>" id="r1l-<?php echo $c['id']; ?>">
<span class="recaudo-label">LOCAL</span>
<?php echo $r1_l !== null ? 'S/ '.number_format($r1_l, 2) : '-'; ?>
</div>
<div class="recaudo-v3 bg-provincia <?php echo $r1_p >= 0 ? 'text-success' : 'text-danger'; ?>" id="r1p-<?php echo $c['id']; ?>">
<span class="recaudo-label">PROVINCIA</span>
<?php echo $r1_p !== null ? 'S/ '.number_format($r1_p, 2) : '-'; ?>
</div>
</td>
<td><input type="text" class="input-cell promo-input" data-field="promo_2" value="<?php echo htmlspecialchars($c['promo_2']); ?>"></td>
<td>
<div class="recaudo-v3 mb-1 <?php echo $r2_l >= 0 ? 'text-success' : 'text-danger'; ?>" id="r2l-<?php echo $c['id']; ?>">
<span class="recaudo-label">LOCAL</span>
<?php echo $r2_l !== null ? 'S/ '.number_format($r2_l, 2) : '-'; ?>
</div>
<div class="recaudo-v3 bg-provincia <?php echo $r2_p >= 0 ? 'text-success' : 'text-danger'; ?>" id="r2p-<?php echo $c['id']; ?>">
<span class="recaudo-label">PROVINCIA</span>
<?php echo $r2_p !== null ? 'S/ '.number_format($r2_p, 2) : '-'; ?>
</div>
</td>
<td><input type="text" class="input-cell promo-input" data-field="promo_3" value="<?php echo htmlspecialchars($c['promo_3']); ?>"></td>
<td>
<div class="recaudo-v3 mb-1 <?php echo $r3_l >= 0 ? 'text-success' : 'text-danger'; ?>" id="r3l-<?php echo $c['id']; ?>">
<span class="recaudo-label">LOCAL</span>
<?php echo $r3_l !== null ? 'S/ '.number_format($r3_l, 2) : '-'; ?>
</div>
<div class="recaudo-v3 bg-provincia <?php echo $r3_p >= 0 ? 'text-success' : 'text-danger'; ?>" id="r3p-<?php echo $c['id']; ?>">
<span class="recaudo-label">PROVINCIA</span>
<?php echo $r3_p !== null ? 'S/ '.number_format($r3_p, 2) : '-'; ?>
</div>
</td>
<td class="text-center">
<button class="btn btn-danger btn-action" onclick="eliminarRow(<?php echo $c['id']; ?>)" title="Eliminar">
<i class="fas fa-trash-alt"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Modal Nuevo -->
<div class="modal fade" id="modalNuevoV3" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<form action="save_marketing_video_v3.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="redirect" value="calculo_costos_v3.php?success=1">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title"><i class="fas fa-plus-circle me-2"></i> Nuevo Producto (V3)</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<div class="mb-3">
<label class="form-label fw-bold">Seleccionar Producto</label>
<select name="producto_id" class="form-select form-select-lg" onchange="fetchProductCost(this.value)">
<option value="">General / Sin producto específico</option>
<?php foreach ($productos_select as $p): ?>
<option value="<?php echo $p['id']; ?>"><?php echo htmlspecialchars($p['nombre']); ?></option>
<?php endforeach; ?>
</select>
<small class="text-muted">Si no seleccionas uno, se marcará como "General".</small>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">Costo Base (S/)</label>
<input type="number" name="costo_producto" id="costo_producto_nuevo_v3" class="form-control" step="0.01" placeholder="0.00">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">Orden de Lista</label>
<input type="number" name="orden" class="form-control" value="1">
</div>
</div>
<div class="mb-0">
<label class="form-label fw-bold">Imagen del Producto</label>
<input type="file" name="foto_producto" class="form-control">
</div>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-primary px-4">Crear Registro</button>
</div>
</form>
</div>
</div>
</div>
<script>
function fetchProductCost(productId) {
if (!productId) return;
fetch(`get_product_details.php?id=${productId}`)
.then(response => response.json())
.then(data => {
if (data.success && data.product.costo) {
document.getElementById('costo_producto_nuevo_v3').value = data.product.costo;
}
});
}
document.querySelectorAll('.input-cell').forEach(input => {
// Actualización instantánea visual
input.addEventListener('input', function() {
const row = this.closest('tr');
recalculateRowVisual(row);
});
// Guardado real al salir del cuadro
input.addEventListener('change', function() {
const row = this.closest('tr');
const id = row.dataset.id;
const field = this.dataset.field;
const value = this.value;
this.classList.add('saving');
fetch('update_marketing_costos_field_v3.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, field, value })
})
.then(response => response.json())
.then(data => {
this.classList.remove('saving');
if (data.success) {
this.classList.add('saved');
setTimeout(() => this.classList.remove('saved'), 1000);
}
});
});
});
function recalculateRowVisual(row) {
const id = row.dataset.id;
const costProd = parseFloat(row.querySelector('[data-field="costo_producto"]').value) || 0;
const costFilm = parseFloat(row.querySelector('[data-field="costo_fijo_film"]').value) || 0;
const costAsesora = parseFloat(row.querySelector('[data-field="comision_asesora"]').value) || 0;
const costAsesoraP = parseFloat(row.querySelector('[data-field="comision_asesora_provincia"]').value) || 0;
const costDelivery = parseFloat(row.querySelector('[data-field="delivery"]').value) || 0;
const costDeliveryP = parseFloat(row.querySelector('[data-field="delivery_provincia"]').value) || 0;
const costPublicidad = parseFloat(row.querySelector('[data-field="costo_publicitario"]').value) || 0;
for (let i = 1; i <= 3; i++) {
const promoVal = row.querySelector(`[data-field="promo_${i}"]`).value;
const match = promoVal.match(/[\d.]+/);
const pVal = match ? parseFloat(match[0]) : 0;
const recaudoElL = row.querySelector(`#r${i}l-${id}`);
const recaudoElP = row.querySelector(`#r${i}p-${id}`);
if (pVal > 0) {
// Local
const recaudoL = pVal - ((costProd + costFilm) * i + costAsesora + costDelivery + costPublicidad);
recaudoElL.innerHTML = `<span class="recaudo-label">LOCAL</span>S/ ${recaudoL.toFixed(2)}`;
recaudoElL.className = 'recaudo-v3 mb-1 ' + (recaudoL >= 0 ? 'text-success' : 'text-danger');
// Provincia
const recaudoP = pVal - ((costProd + costFilm) * i + costAsesoraP + costDeliveryP + costPublicidad);
recaudoElP.innerHTML = `<span class="recaudo-label">PROVINCIA</span>S/ ${recaudoP.toFixed(2)}`;
recaudoElP.className = 'recaudo-v3 bg-provincia ' + (recaudoP >= 0 ? 'text-success' : 'text-danger');
} else {
recaudoElL.innerHTML = `<span class="recaudo-label">LOCAL</span>-`;
recaudoElP.innerHTML = `<span class="recaudo-label">PROVINCIA</span>-`;
}
}
}
function eliminarRow(id) {
if (confirm("¿Eliminar este registro de la V3?")) {
window.location.href = "delete_marketing_video_v3.php?id=" + id;
}
}
function triggerImageUpload(id) {
document.getElementById(`file-input-${id}`).click();
}
function uploadImage(id) {
const fileInput = document.getElementById(`file-input-${id}`);
if (!fileInput.files || !fileInput.files[0]) return;
const formData = new FormData();
formData.append('id', id);
formData.append('foto_producto', fileInput.files[0]);
const cell = fileInput.closest('td');
const originalContent = cell.innerHTML;
cell.innerHTML = '<div class="spinner-border spinner-border-sm text-primary" role="status"></div>';
fetch('update_marketing_video_image_v3.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload(); // Recargamos para ver la nueva imagen
} else {
alert('Error al subir imagen: ' + data.error);
cell.innerHTML = originalContent;
}
})
.catch(err => {
console.error(err);
alert('Error técnico al subir imagen');
cell.innerHTML = originalContent;
});
}
// Script de apertura forzada y diagnóstico
document.addEventListener('DOMContentLoaded', function() {
const btn = document.getElementById('btnAbrirModalV3');
const modalEl = document.getElementById('modalNuevoV3');
if (btn && modalEl) {
btn.addEventListener('click', function() {
console.log('Intento de apertura manual del modal...');
try {
// Intento 1: Usando la API de Bootstrap 5
if (typeof bootstrap !== 'undefined') {
const modalInstance = new bootstrap.Modal(modalEl);
modalInstance.show();
console.log('Modal abierto con bootstrap.Modal');
} else {
// Intento 2: Si bootstrap no está definido, intentar vía jQuery si existe
if (typeof $ !== 'undefined' && typeof $.fn.modal !== 'undefined') {
$(modalEl).modal('show');
console.log('Modal abierto con jQuery');
} else {
alert('Error: No se encontró la librería de Bootstrap. Por favor, recarga la página.');
}
}
} catch (err) {
console.error('Error al abrir modal:', err);
alert('Hubo un problema técnico al abrir la ventana. Error: ' + err.message);
}
});
} else {
console.error('No se encontró el botón o el modal en el DOM');
}
});
</script>
<?php include 'layout_footer.php'; ?>

1349
call_center_pro.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
<?php
header('Content-Type: application/json');
session_start();
if (!isset($_SESSION['user_id'])) {
echo json_encode(['error' => 'No autorizado']);
exit;
}
require_once 'db/config.php';
$pdo = db();
$numero_operacion = trim($_GET['numero_operacion'] ?? '');
$pedido_id = $_GET['pedido_id'] ?? null;
if (empty($numero_operacion)) {
echo json_encode(['duplicate' => false]);
exit;
}
$sql = "SELECT id FROM pedidos WHERE numero_operacion = :numero_operacion";
$params = [':numero_operacion' => $numero_operacion];
if ($pedido_id) {
$sql .= " AND id != :pedido_id";
$params[':pedido_id'] = $pedido_id;
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$result = $stmt->fetch();
if ($result) {
echo json_encode([
'duplicate' => true,
'pedido_id' => $result['id']
]);
} else {
echo json_encode(['duplicate' => false]);
}

87
cobertura.php Normal file
View File

@ -0,0 +1,87 @@
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
require_once 'layout_header.php';
require_once 'db/config.php';
try {
$pdo = db();
$stmt = $pdo->query("SELECT * FROM cobertura ORDER BY id DESC");
$coberturas = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
echo "<div class='alert alert-danger'>Error al conectar con la base de datos: " . $e->getMessage() . "</div>";
die();
}
$cobertura_banner = 'assets/uploads/cobertura_banner.jpg';
?>
<div class="container-fluid mt-4">
<h1>Gestión de Cobertura</h1>
<div class="card">
<div class="card-header">
Banner de la Página de Cobertura
</div>
<div class="card-body">
<?php if (file_exists($cobertura_banner)): ?>
<p><strong>Banner Actual:</strong></p>
<img src="<?php echo $cobertura_banner; ?>?v=<?php echo time(); ?>" alt="Banner Cobertura" class="img-fluid mb-3">
<?php else: ?>
<p class="text-muted">No hay un banner de cobertura actualmente.</p>
<?php endif; ?>
<form action="save_cobertura_banner.php" method="post" enctype="multipart/form-data" class="mt-3">
<div class="form-group">
<label for="cobertura_banner_input">Cambiar/Subir Banner (se recomienda formato JPG, 1200x400px)</label>
<input type="file" name="cobertura_banner" id="cobertura_banner_input" class="form-control" accept=".jpg,.jpeg">
</div>
<button type="submit" class="btn btn-primary mt-2">Guardar Banner</button>
</form>
</div>
</div>
<div class="card mt-4">
<div class="card-header d-flex justify-content-between align-items-center">
Zonas de Cobertura
<a href="add_cobertura.php" class="btn btn-success">Agregar Nueva Zona</a>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-striped">
<thead class="thead-dark">
<tr>
<th>Título</th>
<th>Descripción</th>
<th style="width: 100px;">Acciones</th>
</tr>
</thead>
<tbody>
<?php if (empty($coberturas)): ?>
<tr>
<td colspan="3" class="text-center">No hay zonas de cobertura definidas.</td>
</tr>
<?php else: ?>
<?php foreach ($coberturas as $row): ?>
<tr>
<td><?php echo htmlspecialchars($row['titulo']); ?></td>
<td><?php echo nl2br(htmlspecialchars($row['descripcion'])); ?></td>
<td>
<a href="delete_cobertura.php?id=<?php echo $row['id']; ?>" class="btn btn-danger btn-sm" onclick="return confirm('¿Estás seguro de que quieres eliminar esta zona de cobertura?');">
<i class="fas fa-trash"></i> Eliminar
</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php require_once 'layout_footer.php'; ?>

52
cobertura_xpress.php Normal file
View File

@ -0,0 +1,52 @@
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
require_once 'layout_header.php';
require_once 'db/config.php';
$pdo = db();
$cobertura_xpress_banner = 'assets/uploads/cobertura_xpress_banner.jpg';
?>
<div class="container-fluid mt-4">
<h1>Gestión de Cobertura Xpress</h1>
<div class="card">
<div class="card-header">
Banner de la Página de Cobertura Xpress
</div>
<div class="card-body">
<?php if (file_exists($cobertura_xpress_banner)): ?>
<p><strong>Banner Actual:</strong></p>
<img src="<?php echo $cobertura_xpress_banner; ?>?v=<?php echo time(); ?>" alt="Banner Cobertura Xpress" class="img-fluid mb-3">
<?php else: ?>
<p class="text-muted">No hay un banner de Cobertura Xpress actualmente.</p>
<?php endif; ?>
<form action="save_cobertura_xpress_banner.php" method="post" enctype="multipart/form-data" class="mt-3">
<div class="form-group">
<label for="cobertura_xpress_banner_input">Cambiar/Subir Banner (se recomienda formato JPG, 1200x400px)</label>
<input type="file" name="cobertura_xpress_banner" id="cobertura_xpress_banner_input" class="form-control" accept=".jpg,.jpeg">
</div>
<button type="submit" class="btn btn-primary mt-2">Guardar Banner</button>
</form>
</div>
</div>
<div class="card mt-4">
<div class="card-header d-flex justify-content-between align-items-center">
Zonas de Cobertura Xpress
<a href="add_cobertura_xpress.php" class="btn btn-success">Agregar Nueva Zona Xpress</a>
</div>
<div class="card-body">
<p class="text-muted">La configuración para las zonas de cobertura Xpress estará disponible aquí.</p>
<!-- Aquí iría la tabla y lógica para mostrar las zonas de Cobertura Xpress -->
</div>
</div>
</div>
<?php require_once 'layout_footer.php'; ?>

497
completados.php Normal file
View File

@ -0,0 +1,497 @@
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
require_once 'db/config.php';
function getStatusStyle($status) {
$style = 'color: white;'; // Default text color
$bgColor = '#0dcaf0'; // Default info blue
switch (strtoupper(trim($status))) {
case 'ROTULADO':
$bgColor = '#ffc107'; // yellow
$style = 'color: black;';
break;
case 'EN TRANSITO':
$bgColor = '#90EE90'; // light green
$style = 'color: black;';
break;
case 'EN DESTINO':
$bgColor = '#800080'; // purple
break;
case 'COMPLETADO':
case 'COMPLETADO ✅':
$bgColor = '#198754'; // dark green
break;
case 'GESTION':
$bgColor = '#6c757d'; // secondary grey
break;
}
return "background-color: {$bgColor} !important; {$style}";
}
$pdo = db();
$user_id = $_SESSION['user_id'];
$user_role = $_SESSION['user_role'] ?? 'Asesor';
// Fetch years for the filter
$years_query = "SELECT DISTINCT YEAR(created_at) as year FROM pedidos WHERE estado = 'COMPLETADO ✅'";
if ($user_role === 'Asesor') {
$years_query .= " AND asesor_id = ?";
$years_stmt = $pdo->prepare($years_query);
$years_stmt->execute([$user_id]);
} else {
$years_stmt = $pdo->query($years_query);
}
$years = $years_stmt->fetchAll(PDO::FETCH_COLUMN);
// Filter logic
$selected_month = $_GET['mes'] ?? '';
$selected_year = $_GET['año'] ?? '';
$search_query = $_GET['q'] ?? '';
$sql = "SELECT p.*, u.nombre_asesor as asesor_nombre FROM pedidos p LEFT JOIN users u ON p.asesor_id = u.id WHERE p.estado = 'COMPLETADO ✅'";
$params = [];
if ($user_role === 'Asesor') {
$sql .= " AND p.asesor_id = ?";
$params[] = $user_id;
}
if (!empty($search_query)) {
$sql .= " AND (p.nombre_completo LIKE ? OR p.dni_cliente LIKE ? OR p.celular LIKE ?)";
$params[] = "%$search_query%";
$params[] = "%$search_query%";
$params[] = "%$search_query%";
}
if (!empty($selected_month)) {
$sql .= " AND MONTH(p.created_at) = ?";
$params[] = $selected_month;
}
if (!empty($selected_year)) {
$sql .= " AND YEAR(p.created_at) = ?";
$params[] = $selected_year;
}
$sql .= " ORDER BY p.fecha_completado DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$pedidos = $stmt->fetchAll();
$months = [
1 => 'Enero', 2 => 'Febrero', 3 => 'Marzo', 4 => 'Abril', 5 => 'Mayo', 6 => 'Junio',
7 => 'Julio', 8 => 'Agosto', 9 => 'Septiembre', 10 => 'Octubre', 11 => 'Noviembre', 12 => 'Diciembre'
];
?>
<?php
$pageTitle = "Pedidos Completados";
include 'layout_header.php';
?>
<div class="card mb-4">
<div class="card-body">
<form method="GET" action="completados.php" class="row g-3 align-items-center">
<div class="col-auto">
<label for="q" class="form-label">Buscar</label>
<input type="text" name="q" id="q" class="form-control" value="<?php echo htmlspecialchars($_GET['q'] ?? ''); ?>" placeholder="Nombre, DNI o Celular">
</div>
<div class="col-auto">
<label for="mes" class="form-label">Mes</label>
<select name="mes" id="mes" class="form-select">
<option value="">Todos</option>
<?php foreach ($months as $num => $name): ?>
<option value="<?php echo $num; ?>" <?php echo $selected_month == $num ? 'selected' : ''; ?>><?php echo $name; ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-auto">
<label for="año" class="form-label">Año</label>
<select name="año" id="año" class="form-select">
<option value="">Todos</option>
<?php foreach ($years as $year): ?>
<option value="<?php echo $year; ?>" <?php echo $selected_year == $year ? 'selected' : ''; ?>><?php echo $year; ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-auto mt-4">
<button type="submit" class="btn btn-info">Filtrar</button>
<a href="completados.php" class="btn btn-secondary">Limpiar</a>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Cliente</th>
<th>Celular</th>
<th>Agencia</th>
<th>Producto</th>
<th>Monto Total</th>
<th>Monto Debe</th>
<th>Nro. Operación</th>
<th>Clave</th>
<th>Banco</th>
<th>Recojo Cliente (Día y Hora)</th>
<th>Estado</th>
<th>Asesor</th>
<th>Fecha Creación</th>
<th>Fecha Completado</th>
<th>Voucher Restante</th>
<th>Verificación de Pago</th>
<th>OBSERVACION</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php if (empty($pedidos)): ?>
<tr>
<td colspan="18" class="text-center">No hay pedidos completados que coincidan con el filtro.</td>
</tr>
<?php else: ?>
<?php foreach ($pedidos as $pedido): ?>
<tr>
<td><?php echo htmlspecialchars($pedido['id']); ?></td>
<td><?php echo htmlspecialchars($pedido['nombre_completo']); ?></td>
<td><?php echo htmlspecialchars($pedido['celular']); ?></td>
<td><?php echo htmlspecialchars($pedido['agencia'] ?? 'SHALOM'); ?></td>
<td><?php echo htmlspecialchars($pedido['producto']); ?></td>
<td><?php echo htmlspecialchars($pedido['monto_total']); ?></td>
<td><?php echo htmlspecialchars($pedido['monto_debe']); ?></td>
<td class="editable" data-id="<?php echo $pedido['id']; ?>" data-field="numero_operacion">
<?php echo htmlspecialchars($pedido['numero_operacion'] ?? 'N/A'); ?>
</td>
<?php
$canSeeClave = ($user_role !== 'Asesor' || (!empty($pedido['numero_operacion']) && !empty($pedido['banco'])));
$isEditableClave = ($user_role !== 'Asesor') ? 'editable' : '';
?>
<td class="<?php echo $isEditableClave; ?> clave-cell" data-id="<?php echo $pedido['id']; ?>" data-field="clave" id="clave-<?php echo $pedido['id']; ?>">
<?php echo $canSeeClave ? htmlspecialchars($pedido['clave'] ?? 'N/A') : '<i class="fas fa-eye-slash text-muted" title="Suba el número de operación y seleccione el banco para ver la clave"></i> <span class="text-muted" style="font-size: 0.8rem;">Oculto</span>'; ?>
</td>
<td class="editable-select" data-id="<?php echo $pedido['id']; ?>" data-field="banco">
<?php echo !empty($pedido['banco']) ? htmlspecialchars($pedido['banco']) : 'N/A'; ?>
</td>
<td <?php if (in_array($user_role, ['Administrador', 'personal', 'Verificador de Pagos', 'Control Logistico', 'Logistica']) || strpos($user_role, 'Asesor') !== false) { echo 'class="editable-recojo" data-id="'.$pedido['id'].'" title="Doble clic para editar"'; } ?>>
<?php echo !empty($pedido['fecha_recojo']) ? htmlspecialchars($pedido['fecha_recojo']) : 'N/A'; ?>
</td>
<td><span class="badge" style="<?php echo getStatusStyle($pedido['estado']); ?>"><?php echo ($pedido['estado'] == 'Gestion') ? 'GESTIONES ⚙️' : htmlspecialchars($pedido['estado']); ?></span></td>
<td><?php echo htmlspecialchars($pedido['asesor_nombre'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($pedido['created_at']); ?></td>
<td><?php
if (!empty($pedido['fecha_completado'])) {
try {
// Create DateTime object directly, assuming server timezone is correct
$date = new DateTime($pedido['fecha_completado']);
// Format to d/m/Y H:i:s
echo htmlspecialchars($date->format('d/m/Y H:i:s'));
} catch (Exception $e) {
// Fallback for invalid date formats, which is the likely issue
echo htmlspecialchars($pedido['fecha_completado']);
}
} else {
echo 'N/A';
}
?></td>
<td>
<?php if (!empty($pedido['voucher_restante_path'])): ?>
<a href="<?php echo htmlspecialchars($pedido['voucher_restante_path']); ?>" target="_blank">Ver</a>
<?php else: ?>
N/A
<?php endif; ?>
</td>
<td class="<?php echo (isset($pedido['estado_pago']) && $pedido['estado_pago'] == 'Verificado') ? 'td-pago-verificado' : ''; ?>">
<?php
$estado_pago_class = '';
if (isset($pedido['estado_pago'])) {
if ($pedido['estado_pago'] == 'Pendiente a verificación') {
$estado_pago_class = 'estado-pago-pendiente';
} elseif ($pedido['estado_pago'] == 'Verificado') {
$estado_pago_class = 'estado-pago-verificado';
}
}
?>
<span class="badge <?php echo $estado_pago_class; ?>">
<?php if (in_array($user_role, ['Administrador', 'Verificador de Pagos'])): ?>
<select class="form-select-pago" onchange="updateEstadoPago(<?php echo $pedido['id']; ?>, this)">
<option value="Pendiente a verificación" <?php echo ($pedido['estado_pago'] == 'Pendiente a verificación') ? 'selected' : ''; ?>>Pendiente a verificación</option>
<option value="Verificado" <?php echo ($pedido['estado_pago'] == 'Verificado') ? 'selected' : ''; ?>>Verificado</option>
</select>
<?php else: ?>
<?php echo htmlspecialchars($pedido['estado_pago']); ?>
<?php endif; ?>
</span>
</td>
<td class="editable" data-id="<?php echo $pedido['id']; ?>" data-field="observacion">
<?php echo htmlspecialchars($pedido['observacion'] ?? 'N/A'); ?>
</td>
<td>
<a href="pedido_form.php?id=<?php echo $pedido['id']; ?>" class="btn btn-sm btn-warning">Editar</a>
<?php if ($user_role === 'Administrador'): ?>
<a href="delete_pedido.php?id=<?php echo $pedido['id']; ?>" class="btn btn-sm btn-danger" onclick="return confirm('¿Estás seguro de que quieres eliminar este pedido?');">Eliminar</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<script>
function updatePedidoField(pedidoId, field, value) {
fetch('update_pedido_field.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ id: pedidoId, field: field, value: value })
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log(`${field} actualizado con éxito.`);
// Si el usuario es Asesor, verificar si ahora puede ver la clave
const userRole = "<?php echo $user_role; ?>";
if (userRole === 'Asesor') {
const pedido = data.pedido;
const claveCell = document.getElementById(`clave-${pedidoId}`);
if (pedido.numero_operacion && pedido.banco) {
claveCell.innerHTML = pedido.clave || 'N/A';
} else {
claveCell.innerHTML = '<i class="fas fa-eye-slash text-muted" title="Suba el número de operación y seleccione el banco para ver la clave"></i> <span class="text-muted" style="font-size: 0.8rem;">Oculto</span>';
}
}
} else if (data.error) {
alert('Error: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('Hubo un error de conexión.');
});
}
function updateEstadoPago(pedidoId, selectElement) {
const estado = selectElement.value;
const badge = selectElement.closest('.badge');
const td = selectElement.closest('td');
// Guardar el estado original en caso de error
const originalOption = Array.from(selectElement.options).find(opt => opt.defaultSelected);
const originalState = originalOption ? originalOption.value : selectElement.value;
console.log(`Actualizando pedido ${pedidoId} a estado: ${estado}`);
const formData = new URLSearchParams();
formData.append('pedido_id', pedidoId);
formData.append('estado_pago', estado);
fetch('update_pago.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error('Respuesta de red no OK');
}
return response.json();
})
.then(data => {
if (data.success) {
console.log('Estado de pago actualizado con éxito en el servidor.');
// Limpiar clases previas
badge.classList.remove('estado-pago-pendiente', 'estado-pago-verificado');
td.classList.remove('td-pago-verificado');
// Aplicar nuevas clases según el estado
if (estado === 'Pendiente a verificación') {
badge.classList.add('estado-pago-pendiente');
} else if (estado === 'Verificado') {
badge.classList.add('estado-pago-verificado');
td.classList.add('td-pago-verificado');
}
// Actualizar el estado 'defaultSelected' para futuras reversiones
Array.from(selectElement.options).forEach(opt => {
opt.defaultSelected = (opt.value === estado);
});
} else {
console.error('Error del servidor:', data.message);
alert('Error: ' + (data.message || 'No se pudo actualizar el estado.'));
selectElement.value = originalState;
}
})
.catch(error => {
console.error('Error de conexión:', error);
alert('Hubo un error de conexión al intentar actualizar el estado.');
selectElement.value = originalState;
});
}
document.addEventListener('DOMContentLoaded', function() {
const userRole = "<?php echo $user_role; ?>";
const authorizedRoles = ['Administrador', 'personal', 'Verificador de Pagos', 'Control Logistico', 'Logistica'];
if (authorizedRoles.includes(userRole) || userRole.includes('Asesor')) {
const table = document.querySelector('.table');
table.addEventListener('dblclick', function(e) {
const cell = e.target.closest('.editable-recojo, .editable, .editable-select');
if (!cell) return;
// Evitar doble edición si ya hay un input o select
if (cell.querySelector('input, select')) {
return;
}
const originalText = cell.textContent.trim() === 'N/A' || cell.textContent.trim() === 'Seleccionar' ? '' : cell.textContent.trim();
const pedidoId = cell.dataset.id;
const field = cell.dataset.field;
if (cell.classList.contains('editable-select')) {
const bancos = ['YAPE', 'PLIN', 'BCP', 'INTERBANK', 'BANCO DE LA NACION', 'BBVA'];
let options = '<option value="">Seleccionar</option>';
bancos.forEach(b => {
options += `<option value="${b}" ${originalText === b ? 'selected' : ''}>${b}</option>`;
});
cell.innerHTML = `<select class="form-select form-select-sm">${options}</select>`;
const select = cell.querySelector('select');
select.focus();
const saveSelectChanges = function() {
const newValue = select.value;
cell.innerHTML = newValue === '' ? 'N/A' : newValue;
if (newValue === originalText) return;
updatePedidoField(pedidoId, field, newValue);
};
select.addEventListener('blur', saveSelectChanges);
select.addEventListener('change', function() {
select.blur();
});
select.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
select.removeEventListener('blur', saveSelectChanges);
cell.innerHTML = originalText === '' ? 'N/A' : originalText;
}
});
} else {
cell.innerHTML = `<input type="text" class="form-control form-control-sm" value="${originalText}">`;
const input = cell.querySelector('input');
input.focus();
const saveChanges = function() {
const newValue = input.value.trim();
// Reemplazar el input con el texto
cell.innerHTML = newValue === '' ? 'N/A' : newValue;
// Si el valor no ha cambiado, no hacer nada
if (newValue === originalText) {
return;
}
if (cell.classList.contains('editable-recojo')) {
const formData = new URLSearchParams();
formData.append('id', pedidoId);
formData.append('fecha_recojo', newValue);
fetch('update_recojo.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData
})
.then(response => response.json())
.then(data => {
if (!data.success) {
console.error('Error al guardar:', data.message);
cell.innerHTML = originalText === '' ? 'N/A' : originalText; // Revertir en caso de error
alert('No se pudo guardar el cambio. Por favor, inténtalo de nuevo.');
}
})
.catch(error => {
console.error('Error de red:', error);
cell.innerHTML = originalText === '' ? 'N/A' : originalText; // Revertir en caso de error
alert('Error de conexión. No se pudo guardar el cambio.');
});
} // Cierre de if (cell.classList.contains('editable-recojo'))
if (field === 'clave' || field === 'numero_operacion' || field === 'observacion') {
if (field === 'numero_operacion' && newValue !== '' && newValue.length < 6) {
alert('El número de operación debe tener al menos 6 dígitos.');
cell.innerHTML = originalText === '' ? 'N/A' : originalText;
return;
}
fetch('update_pedido_field.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ id: pedidoId, field: field, value: newValue })
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Si es numero_operacion y es Asesor, verificar visibilidad de clave
if (field === 'numero_operacion' && userRole === 'Asesor') {
const pedido = data.pedido;
const claveCell = document.getElementById(`clave-${pedidoId}`);
if (pedido.numero_operacion && pedido.banco) {
claveCell.innerHTML = pedido.clave || 'N/A';
} else {
claveCell.innerHTML = '<i class="fas fa-eye-slash text-muted" title="Suba el número de operación y seleccione el banco para ver la clave"></i> <span class="text-muted" style="font-size: 0.8rem;">Oculto</span>';
}
}
} else if (data.error) {
console.error('Error al guardar:', data.error);
cell.innerHTML = originalText === '' ? 'N/A' : originalText;
alert('Error: ' + data.error);
}
})
.catch(error => {
console.error('Error de red:', error);
cell.innerHTML = originalText === '' ? 'N/A' : originalText;
alert('Error de conexión.');
});
}
};
input.addEventListener('blur', saveChanges);
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
input.blur(); // Dispara el evento blur para guardar
} else if (e.key === 'Escape') {
input.removeEventListener('blur', saveChanges); // Evitar que se guarde
cell.innerHTML = originalText === '' ? 'N/A' : originalText;
}
});
}
});
}
});
</script>
<?php include 'layout_footer.php'; ?>

6
composer.json Normal file
View File

@ -0,0 +1,6 @@
{
"require": {
"shuchkin/simplexlsxgen": "^1.5",
"google/apiclient": "^2.15"
}
}

1117
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

407
configuracion.php Normal file
View File

@ -0,0 +1,407 @@
<?php
$pageTitle = "Configuración General";
require_once 'layout_header.php';
require_once 'db/config.php';
// Asegurarse de que el usuario sea administrador
if ($_SESSION['user_role'] !== 'Administrador' && $_SESSION['user_role'] !== 'admin') {
echo "<div class='alert alert-danger'>Acceso denegado.</div>";
require_once 'layout_footer.php';
exit();
}
$conn = db();
// --- Procesamiento de Formularios ---
// Guardar visibilidad de columnas del Kanban
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_visibility'])) {
$visible_columns = $_POST['visible_columns'] ?? [];
$json_visible_columns = json_encode($visible_columns);
$stmt = $conn->prepare("INSERT INTO configuracion (clave, valor) VALUES ('kanban_visible_columns', :valor) ON DUPLICATE KEY UPDATE valor = :valor");
$stmt->bindParam(':valor', $json_visible_columns);
if ($stmt->execute()) {
$_SESSION['success_message'] = 'Visibilidad de columnas actualizada.';
} else {
$_SESSION['error_message'] = 'Error al actualizar la visibilidad.';
}
header("Location: configuracion.php");
exit();
}
// Sincronizar sedes de Shalom
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['sync_shalom'])) {
$apiKey = 'sk_mlq1j3na_4a676ewvaop';
$url = "https://shalom-api.lat/api/listar";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Accept: application/json',
"x-api-key: {$apiKey}"
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$data = json_decode($response, true);
if ($data && isset($data['success']) && $data['success'] && isset($data['data'])) {
$sedes = $data['data'];
$count = 0;
foreach ($sedes as $sede) {
$id = $sede['ter_id'];
// Usamos el campo 'nombre' que es más completo (Departamento / Provincia / Distrito / Lugar)
$nombre = $sede['nombre'];
$stmt = $conn->prepare("INSERT INTO sedes_shalom (id_terminal, nombre_sede, activo, permite_origen, permite_destino)
VALUES (?, ?, 1, 1, 1)
ON DUPLICATE KEY UPDATE nombre_sede = VALUES(nombre_sede)");
$stmt->execute([$id, $nombre]);
$count++;
}
$_SESSION['success_message'] = "Sincronización completada. Se procesaron $count sedes.";
} else {
$_SESSION['error_message'] = "Error al procesar los datos de la API.";
}
} else {
$_SESSION['error_message'] = "Error al conectar con la API de Shalom (Código: $httpCode).";
}
header("Location: configuracion.php");
exit();
}
// --- Carga de Datos para la Vista ---
// Cargar configuraciones generales
$stmt_config = $conn->query("SELECT clave, valor FROM configuracion");
$configs = $stmt_config->fetchAll(PDO::FETCH_KEY_PAIR);
$banner_text = $configs['banner_text'] ?? '';
$whatsapp_template_notificacion = $configs['whatsapp_template_notificacion'] ?? ($configs['whatsapp_template_contraentrega'] ?? '');
// Datos para la gestión de columnas
$stmt_manage_columns = $conn->query("SELECT * FROM kanban_columns ORDER BY orden ASC");
$management_columns = $stmt_manage_columns->fetchAll(PDO::FETCH_ASSOC);
// Datos para la visibilidad de columnas
$stmt_visible = $conn->prepare("SELECT valor FROM configuracion WHERE clave = 'kanban_visible_columns'");
$stmt_visible->execute();
$config_row = $stmt_visible->fetch(PDO::FETCH_ASSOC);
$visible_columns = $config_row ? json_decode($config_row['valor'], true) : [];
$stmt_all_db_columns = $conn->query("SELECT nombre FROM kanban_columns ORDER BY orden ASC");
$available_columns_for_visibility = $stmt_all_db_columns->fetchAll(PDO::FETCH_COLUMN);
// --- Datos para la GESTIÓN DE CONTENIDO DEL KANBAN ---
$products = $conn->query("SELECT id, nombre FROM products ORDER BY nombre ASC")->fetchAll(PDO::FETCH_ASSOC);
$kanban_columns_for_cards = $conn->query("SELECT id, nombre FROM kanban_columns ORDER BY orden, id")->fetchAll(PDO::FETCH_ASSOC);
$info_cards = $conn->query("
SELECT ip.*, p.nombre as producto_nombre, kc.nombre as column_nombre
FROM info_productos ip
LEFT JOIN products p ON ip.producto_id = p.id
LEFT JOIN kanban_columns kc ON ip.column_id = kc.id
ORDER BY ip.orden, ip.id DESC
")->fetchAll(PDO::FETCH_ASSOC);
// Mostrar mensajes de éxito o error
if (isset($_SESSION['success_message'])) {
echo "<div class='alert alert-success'>".$_SESSION['success_message']."</div>";
unset($_SESSION['success_message']);
}
if (isset($_SESSION['error_message'])) {
echo "<div class='alert alert-danger'>".$_SESSION['error_message']."</div>";
unset($_SESSION['error_message']);
}
?>
<div class="container mt-5">
<!-- SECCIÓN PARA SINCRONIZAR SEDES DE SHALOM -->
<div class="card mb-5">
<div class="card-header">
<h3>Sincronización de Sedes Shalom</h3>
</div>
<div class="card-body">
<p>Conéctate directamente con la API de Shalom para actualizar el listado de agencias disponibles en el sistema.</p>
<form action="configuracion.php" method="post">
<input type="hidden" name="sync_shalom" value="1">
<button type="submit" class="btn btn-info">
<i class="fas fa-sync"></i> Sincronizar Sedes Ahora
</button>
</form>
<?php
// Mostrar resultado de la sincronización si existe
if (isset($_SESSION['sync_result'])) {
echo "<div class='alert alert-info mt-3'>" . $_SESSION['sync_result'] . "</div>";
unset($_SESSION['sync_result']);
}
?>
</div>
</div>
<!-- SECCIÓN PARA GESTIONAR CONTENIDO DEL KANBAN (TARJETAS) -->
<div class="card mb-5">
<div class="card-header">
<h3>Gestionar Contenido del Kanban</h3>
</div>
<div class="card-body">
<p>Aquí puedes crear, editar y eliminar las tarjetas de información que aparecen en el tablero Kanban.</p>
<!-- Formulario para añadir/editar tarjeta -->
<div class="card mb-4">
<div class="card-header">
<h5 id="form-title">Añadir Nueva Tarjeta</h5>
</div>
<div class="card-body">
<form action="save_info_producto.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="id" id="info-id">
<div class="form-group">
<label for="producto_id">Producto Asociado (Opcional)</label>
<select name="producto_id" id="producto_id" class="form-control">
<option value="">Selecciona un producto</option>
<?php foreach ($products as $product): ?>
<option value="<?php echo $product['id']; ?>"><?php echo htmlspecialchars($product['nombre']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group mt-3">
<label for="texto_informativo">Texto de la Tarjeta</label>
<textarea name="texto_informativo" id="texto_informativo" class="form-control" rows="3" required></textarea>
</div>
<div class="form-group mt-3">
<label for="imagen">Imagen de la Tarjeta</label>
<div id="image-preview" class="mt-2 mb-2"></div>
<input type="file" name="imagen" id="imagen" class="form-control-file">
<input type="hidden" name="current_imagen" id="current_imagen">
<small class="form-text text-muted">Sube una imagen para añadir o reemplazar la actual.</small>
</div>
<div class="form-group mt-3">
<label for="column_id">Columna del Kanban</label>
<select name="column_id" id="column_id" class="form-control" required>
<option value="">Selecciona una columna</option>
<?php foreach ($kanban_columns_for_cards as $column): ?>
<option value="<?php echo $column['id']; ?>"><?php echo htmlspecialchars($column['nombre']); ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary mt-3">Guardar Tarjeta</button>
<button type="button" class="btn btn-secondary mt-3" onclick="resetForm()">Limpiar</button>
</form>
</div>
</div>
<!-- Lista de tarjetas existentes -->
<div class="card">
<div class="card-header">
<h5>Tarjetas Existentes</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Producto</th>
<th>Imagen</th>
<th>Texto</th>
<th>Columna</th>
<th style="width: 120px;">Acciones</th>
</tr>
</thead>
<tbody>
<?php if (empty($info_cards)): ?>
<tr><td colspan="5" class="text-center">No hay tarjetas creadas.</td></tr>
<?php else: ?>
<?php foreach ($info_cards as $card): ?>
<tr>
<td><?php echo htmlspecialchars($card['producto_nombre'] ?: 'N/A'); ?></td>
<td>
<?php if (!empty($card['imagen_url'])): ?>
<img src="<?php echo htmlspecialchars($card['imagen_url']); ?>?t=<?php echo time(); ?>" alt="Imagen" style="width: 100px; height: auto; border-radius: 5px;">
<?php endif; ?>
</td>
<td><?php echo nl2br(htmlspecialchars($card['texto_informativo'])); ?></td>
<td><?php echo htmlspecialchars($card['column_nombre'] ?: 'Sin asignar'); ?></td>
<td>
<button class="btn btn-sm btn-info mb-1 w-100" onclick='editCard(<?php echo json_encode($card); ?>)'>Editar</button>
<a href="delete_info_producto.php?id=<?php echo $card['id']; ?>" class="btn btn-sm btn-danger w-100" onclick="return confirm('¿Seguro?')">Eliminar</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Sección para Configuración de Banners y Textos -->
<div class="card">
<div class="card-body">
<h5 class="card-title">Texto del Banner Principal</h5>
<p class="card-text">Edita el texto que aparece en el banner de la página principal.</p>
<form action="save_banner_text.php" method="post">
<div class="form-group">
<textarea class="form-control" id="banner_text" name="banner_text" rows="3"><?php echo htmlspecialchars($banner_text); ?></textarea>
</div>
<button type="submit" class="btn btn-primary mt-2">Guardar Texto</button>
</form>
</div>
</div>
<!-- Plantilla de Notificación de WhatsApp -->
<div class="card mt-4 border-primary">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0"><i class="fab fa-whatsapp"></i> PLANTILLA DE NOTIFICACIÓN</h5>
</div>
<div class="card-body">
<p class="card-text text-muted">
Este es el mensaje que se enviará a los clientes para notificarles sobre su pedido (disponible en <strong>Pedidos en Tránsito</strong> y <strong>Ruta Contraentrega</strong>).
Puedes personalizarlo usando las siguientes etiquetas:
</p>
<div class="alert alert-light border">
<div class="row">
<div class="col-md-6">
<ul class="mb-0">
<li><code>{NOMBRE_CLIENTE}</code>: Nombre del cliente.</li>
<li><code>{PRODUCTO}</code>: Nombre del producto.</li>
<li><code>{SEDE_ENVIO}</code>: Agencia de destino.</li>
</ul>
</div>
<div class="col-md-6">
<ul class="mb-0">
<li><code>{MONTO_TOTAL}</code>: Precio total.</li>
<li><code>{ADELANTO}</code>: Pago realizado.</li>
<li><code>{SALDO_PENDIENTE}</code>: Lo que falta pagar.</li>
</ul>
</div>
</div>
</div>
<form action="save_whatsapp_template.php" method="post">
<input type="hidden" name="template_name" value="whatsapp_template_notificacion">
<div class="form-group">
<label for="whatsapp_template_notificacion" class="form-label font-weight-bold">Contenido del Mensaje:</label>
<textarea class="form-control" id="whatsapp_template_notificacion" name="template_content" rows="10" placeholder="Escribe aquí tu mensaje..."><?php echo htmlspecialchars($whatsapp_template_notificacion); ?></textarea>
</div>
<div class="mt-3">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Guardar Plantilla
</button>
</div>
</form>
</div>
</div>
<!-- Sección para Gestionar Columnas (Añadir, Editar, Eliminar) -->
<div class="card mb-5">
<div class="card-header">
<h3>Gestionar Columnas del Kanban</h3>
</div>
<div class="card-body">
<!-- Formulario para añadir nueva columna -->
<div class="card mb-4">
<div class="card-header">
<h6>Añadir Nueva Columna</h6>
</div>
<div class="card-body">
<form action="add_column.php" method="post">
<div class="form-group">
<label for="nombre">Nombre de la Columna</label>
<input type="text" name="nombre" id="nombre" class="form-control" required>
</div>
<button type="submit" class="btn btn-success mt-3">Añadir Columna</button>
</form>
</div>
</div>
<!-- Tabla de columnas existentes -->
<h6>Columnas Actuales</h6>
<div class="table-responsive">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Orden</th>
<th>Nombre</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php if (empty($management_columns)): ?>
<tr><td colspan="3" class="text-center">No hay columnas definidas.</td></tr>
<?php else: ?>
<?php foreach ($management_columns as $col): ?>
<tr>
<td><?php echo htmlspecialchars($col['orden']); ?></td>
<td><?php echo htmlspecialchars($col['nombre']); ?></td>
<td>
<a href="edit_column.php?id=<?php echo $col['id']; ?>" class="btn btn-sm btn-info">Editar</a>
<a href="delete_column.php?id=<?php echo $col['id']; ?>" class="btn btn-sm btn-danger" onclick="return confirm('¿Estás seguro de que quieres eliminar esta columna?');">Eliminar</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Sección para Configurar Visibilidad de Columnas -->
<div class="card">
<div class="card-header">
<h3>Configurar Visibilidad de Columnas en Kanban</h3>
</div>
<div class="card-body">
<p>Selecciona las columnas que quieres que sean visibles en el tablero Kanban principal.</p>
<form action="configuracion.php" method="post">
<input type="hidden" name="save_visibility" value="1">
<div class="form-group">
<?php foreach ($available_columns_for_visibility as $column_name): ?>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="visible_columns[]" value="<?php echo htmlspecialchars($column_name); ?>" id="check_<?php echo htmlspecialchars($column_name); ?>"
<?php echo (in_array($column_name, $visible_columns)) ? 'checked' : ''; ?>>
<label class="form-check-label" for="check_<?php echo htmlspecialchars($column_name); ?>">
<?php echo htmlspecialchars($column_name); ?>
</label>
</div>
<?php endforeach; ?>
</div>
<button type="submit" class="btn btn-primary mt-3">Guardar Visibilidad</button>
</form>
</div>
</div>
</div>
<script>
function editCard(card) {
document.getElementById('form-title').innerText = 'Editar Tarjeta';
document.getElementById('info-id').value = card.id;
document.getElementById('producto_id').value = card.producto_id;
document.getElementById('texto_informativo').value = card.texto_informativo;
document.getElementById('column_id').value = card.column_id;
document.getElementById('current_imagen').value = card.imagen_url;
const imagePreview = document.getElementById('image-preview');
imagePreview.innerHTML = '';
if (card.imagen_url) {
imagePreview.innerHTML = `<p class="mb-1">Imagen actual:</p><img src="${card.imagen_url}?t=${new Date().getTime()}" style="width: 100px; height: auto; border-radius: 5px;"/>`;
}
const formCard = document.querySelector('#form-title').closest('.card');
formCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function resetForm() {
document.getElementById('form-title').innerText = 'Añadir Nueva Tarjeta';
document.getElementById('info-id').value = '';
document.querySelector('form[action="save_info_producto.php"]').reset();
document.getElementById('image-preview').innerHTML = '';
}
</script>
<?php require_once 'layout_footer.php'; ?>

View File

@ -0,0 +1,89 @@
<?php
session_start();
require_once 'db/config.php';
// Verificar si el usuario es Administrador
if (!isset($_SESSION['user_id']) || ($_SESSION['user_role'] !== 'admin' && $_SESSION['user_role'] !== 'Administrador')) {
header("Location: login.php");
exit;
}
$db = db();
$message = '';
// Crear tabla de configuración si no existe
$db->exec("CREATE TABLE IF NOT EXISTS configuracion_costos (
id INT AUTO_INCREMENT PRIMARY KEY,
clave VARCHAR(50) UNIQUE,
valor DECIMAL(10,2)
)");
// Valores por defecto
$defaults = [
'envio_contraentrega' => 10.00,
'envio_agencia' => 12.00,
'comision_venta' => 5.00,
'publicidad_diaria' => 50.00
];
foreach ($defaults as $clave => $valor) {
$stmt = $db->prepare("INSERT IGNORE INTO configuracion_costos (clave, valor) VALUES (?, ?)");
$stmt->execute([$clave, $valor]);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
foreach ($_POST['costos'] as $clave => $valor) {
$stmt = $db->prepare("UPDATE configuracion_costos SET valor = ? WHERE clave = ?");
$stmt->execute([$valor, $clave]);
}
$message = '<div class="alert alert-success">Configuración actualizada correctamente.</div>';
}
$stmt = $db->query("SELECT clave, valor FROM configuracion_costos");
$config = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
$pageTitle = 'Configuración de Rentabilidad';
include 'layout_header.php';
?>
<div class="container-fluid mt-4">
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="fas fa-calculator me-2"></i>Configuración de Costos y Rentabilidad</h4>
</div>
<div class="card-body">
<?php echo $message; ?>
<form method="POST">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Costo Envío Contraentrega Estándar (S/)</label>
<input type="number" step="0.01" name="costos[envio_contraentrega]" class="form-control" value="<?php echo $config['envio_contraentrega']; ?>">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Costo Envío Agencia Estándar (S/)</label>
<input type="number" step="0.01" name="costos[envio_agencia]" class="form-control" value="<?php echo $config['envio_agencia']; ?>">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Comisión por Venta Estándar (S/)</label>
<input type="number" step="0.01" name="costos[comision_venta]" class="form-control" value="<?php echo $config['comision_venta']; ?>">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Presupuesto Publicidad Diario Estimado (S/)</label>
<input type="number" step="0.01" name="costos[publicidad_diaria]" class="form-control" value="<?php echo $config['publicidad_diaria']; ?>">
</div>
</div>
<hr>
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i> Estos valores se utilizarán para calcular la rentabilidad automática en los reportes, sin necesidad de llenar datos en cada pedido.
</div>
<button type="submit" class="btn btn-primary w-100">Guardar Configuración</button>
</form>
</div>
</div>
</div>
</div>
</div>
<?php include 'layout_footer.php'; ?>

685
dashboard.php Normal file
View File

@ -0,0 +1,685 @@
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
require_once 'db/config.php';
$user_role = $_SESSION['user_role'];
$user_id = $_SESSION['user_id'];
$user_name = $_SESSION['user_name'] ?? 'Usuario';
$pdo = db();
$page_title = "Dashboard";
// Lógica para manejar el filtro de fecha
$selected_month = isset($_GET['month']) ? (int)$_GET['month'] : date('m');
$selected_year = isset($_GET['year']) ? (int)$_GET['year'] : date('Y');
$meses = [
1 => 'Enero', 2 => 'Febrero', 3 => 'Marzo', 4 => 'Abril', 5 => 'Mayo', 6 => 'Junio',
7 => 'Julio', 8 => 'Agosto', 9 => 'Septiembre', 10 => 'Octubre', 11 => 'Noviembre', 12 => 'Diciembre'
];
$current_year = date('Y');
$years = range($current_year, $current_year - 5); // Últimos 5 años
include 'layout_header.php';
// Contar pedidos del día (esto no cambia con el filtro mensual)
$stmt_pedidos_hoy = $pdo->prepare("SELECT COUNT(*) FROM pedidos WHERE DATE(created_at) = CURDATE()");
$stmt_pedidos_hoy->execute();
$pedidos_hoy = $stmt_pedidos_hoy->fetchColumn();
// Contar pedidos del mes seleccionado
$stmt_pedidos_mes = $pdo->prepare("SELECT COUNT(*) FROM pedidos WHERE MONTH(created_at) = ? AND YEAR(created_at) = ?");
$stmt_pedidos_mes->execute([$selected_month, $selected_year]);
$pedidos_mes = $stmt_pedidos_mes->fetchColumn();
// Contar pedidos enviados (esto es un total general, no cambia con el filtro)
$stmt_pedidos_enviados = $pdo->prepare("SELECT COUNT(*) FROM pedidos WHERE estado = 'EN TRANSITO 🚛'");
$stmt_pedidos_enviados->execute();
$pedidos_enviados = $stmt_pedidos_enviados->fetchColumn();
// Contadores específicos para el rol de Asesor
$pedidos_hoy_asesor = 0;
$pedidos_mes_asesor = 0;
if ($user_role === 'Asesor') {
// Contar pedidos del día para el asesor actual
$stmt_hoy_asesor = $pdo->prepare("SELECT COUNT(*) FROM pedidos WHERE DATE(created_at) = CURDATE() AND asesor_id = ?");
$stmt_hoy_asesor->execute([$user_id]);
$pedidos_hoy_asesor = $stmt_hoy_asesor->fetchColumn();
// Contar pedidos del mes seleccionado para el asesor actual
$stmt_mes_asesor = $pdo->prepare("SELECT COUNT(*) FROM pedidos WHERE MONTH(created_at) = ? AND YEAR(created_at) = ? AND asesor_id = ?");
$stmt_mes_asesor->execute([$selected_month, $selected_year, $user_id]);
$pedidos_mes_asesor = $stmt_mes_asesor->fetchColumn();
}
// Define los estados de los pedidos
$estados = ['RUTA_CONTRAENTREGA', 'RETORNADO', 'ENTREGA EXITOSA', 'ROTULADO 📦', 'EN TRANSITO 🚛', 'EN DESTINO 🏬', 'COMPLETADO ✅'];
$asesor_data = [];
$chart_data = [];
if ($user_role === 'Administrador' || $user_role === 'Control Logistico' || $user_role === 'Logistica' || $user_role === 'Asesor') {
// 1. Obtener todos los asesores
$stmt_asesores = $pdo->query("SELECT id, username, nombre_asesor FROM users WHERE role = 'Asesor'");
$asesores = $stmt_asesores->fetchAll(PDO::FETCH_ASSOC);
// 2. Preparar la estructura de datos
foreach ($asesores as $asesor) {
$asesor_data[$asesor['id']] = [
'nombre' => !empty($asesor['nombre_asesor']) ? $asesor['nombre_asesor'] : $asesor['username'],
'pedidos' => array_fill_keys($estados, 0),
'total' => 0
];
}
// Add a special entry for unassigned orders
$asesor_data['sin_asignar'] = [
'nombre' => 'Sin Asignar',
'pedidos' => array_fill_keys($estados, 0),
'total' => 0
];
// 3. Construir y ejecutar una consulta única para todos los pedidos del mes seleccionado
$placeholders = rtrim(str_repeat('?,', count($estados)), ',');
$sql = "
SELECT asesor_id, estado, COUNT(*) as total_pedidos
FROM pedidos
WHERE estado IN ($placeholders) AND MONTH(created_at) = ? AND YEAR(created_at) = ?
GROUP BY asesor_id, estado
";
$stmt_pedidos = $pdo->prepare($sql);
$params = array_merge($estados, [$selected_month, $selected_year]);
$stmt_pedidos->execute($params);
// 4. Llenar la estructura con los datos de la consulta
while ($row = $stmt_pedidos->fetch(PDO::FETCH_ASSOC)) {
$asesor_id = $row['asesor_id'];
$estado = $row['estado'];
$total_pedidos = (int)$row['total_pedidos'];
// Si el asesor_id es nulo o no corresponde a un asesor conocido, se agrupa en "Sin Asignar".
if ($asesor_id === null || !isset($asesor_data[$asesor_id])) {
$asesor_data['sin_asignar']['pedidos'][$estado] += $total_pedidos;
} else {
// De lo contrario, se asigna al asesor correspondiente.
$asesor_data[$asesor_id]['pedidos'][$estado] = $total_pedidos;
}
}
// 5. Calcular totales y preparar datos para el gráfico
foreach ($asesor_data as $id => &$data) {
$data['total'] = array_sum($data['pedidos']);
}
unset($data);
// Encontrar a la asesora del mes
$top_advisor_name = 'N/A';
$top_advisor_total = 0;
if (!empty($asesor_data)) {
// Clonar el array para no afectar el orden del gráfico
$temp_asesor_data = $asesor_data;
// Ordenar de forma descendente para encontrar a la mejor
uasort($temp_asesor_data, function ($a, $b) {
return $b['total'] <=> $a['total'];
});
$top_advisor = reset($temp_asesor_data); // Obtener la primera
if ($top_advisor && $top_advisor['total'] > 0) {
$top_advisor_name = $top_advisor['nombre'];
$top_advisor_total = $top_advisor['total'];
}
}
// Encontrar la mejor efectividad en completados
$best_completion_advisor = ['nombre' => 'N/A', 'efectividad' => 0];
if (!empty($asesores)) {
$completion_effectiveness = [];
foreach ($asesores as $asesor) {
// Contar el total de pedidos asignados al asesor en el mes
$stmt_total = $pdo->prepare("
SELECT COUNT(*)
FROM pedidos
WHERE asesor_id = ? AND MONTH(created_at) = ? AND YEAR(created_at) = ?
");
$stmt_total->execute([$asesor['id'], $selected_month, $selected_year]);
$total_asignados = $stmt_total->fetchColumn();
if ($total_asignados > 0) {
// Contar pedidos completados para el asesor en el mes
$stmt_completados = $pdo->prepare("
SELECT COUNT(*)
FROM pedidos
WHERE asesor_id = ? AND estado = 'COMPLETADO ✅' AND MONTH(created_at) = ? AND YEAR(created_at) = ?
");
$stmt_completados->execute([$asesor['id'], $selected_month, $selected_year]);
$total_completados = $stmt_completados->fetchColumn();
$efectividad = ($total_completados / $total_asignados) * 100;
$nombre_asesor = !empty($asesor['nombre_asesor']) ? $asesor['nombre_asesor'] : $asesor['username'];
$completion_effectiveness[] = ['nombre' => $nombre_asesor, 'efectividad' => $efectividad];
}
}
if (!empty($completion_effectiveness)) {
// Ordenar por efectividad descendente
usort($completion_effectiveness, function ($a, $b) {
return $b['efectividad'] <=> $a['efectividad'];
});
$best_completion_advisor = $completion_effectiveness[0];
}
}
// Ordenar asesores por total de pedidos (descendente) para la tabla y el gráfico
uasort($asesor_data, function ($a, $b) {
return $b['total'] <=> $a['total'];
});
// Preparar datos para Chart.js
foreach ($asesor_data as $data) {
$chart_data['labels'][] = $data['nombre'];
$chart_data['data'][] = $data['total'];
}
// Invertir el orden para el gráfico (de menor a mayor)
if (isset($chart_data['labels'])) {
$chart_data['labels'] = array_reverse($chart_data['labels']);
$chart_data['data'] = array_reverse($chart_data['data']);
}
}
// TABLA DE PEDIDOS POR DÍA Y ASESORA (TIPO EXCEL)
$pedidos_mensual_asesora = [];
$asesoras_mensual = [];
if ($user_role === 'Administrador' || $user_role === 'Control Logistico' || $user_role === 'Logistica' || $user_role === 'Asesor') {
// 1. Obtener todas las asesoras
$stmt_asesoras = $pdo->query("SELECT id, nombre_asesor FROM users WHERE role = 'Asesor' AND nombre_asesor IS NOT NULL AND nombre_asesor != ''");
$asesoras_mensual = $stmt_asesoras->fetchAll(PDO::FETCH_ASSOC);
$asesora_ids = array_column($asesoras_mensual, 'id');
// 2. Obtener y procesar los datos de pedidos del mes
if (!empty($asesora_ids)) {
$placeholders = implode(',', array_fill(0, count($asesora_ids), '?'));
$stmt_pedidos_mensual = $pdo->prepare("
SELECT DAY(created_at) as dia, asesor_id, COUNT(id) as total_pedidos
FROM pedidos
WHERE MONTH(created_at) = ? AND YEAR(created_at) = ? AND asesor_id IN (" . $placeholders . ")
GROUP BY dia, asesor_id
");
$params = array_merge([$selected_month, $selected_year], $asesora_ids);
$stmt_pedidos_mensual->execute($params);
$pedidos_data = $stmt_pedidos_mensual->fetchAll(PDO::FETCH_ASSOC);
foreach ($pedidos_data as $row) {
$pedidos_mensual_asesora[$row['dia']][$row['asesor_id']] = $row['total_pedidos'];
}
// 3. Calcular totales por asesora para el mes
$totales_por_asesora_para_orden = array_fill_keys($asesora_ids, 0);
foreach ($pedidos_mensual_asesora as $dia_data) {
foreach ($dia_data as $asesor_id => $cantidad) {
if (isset($totales_por_asesora_para_orden[$asesor_id])) {
$totales_por_asesora_para_orden[$asesor_id] += $cantidad;
}
}
}
// 4. Ordenar el array de asesoras basado en el total de pedidos (de menor a mayor)
usort($asesoras_mensual, function ($a, $b) use ($totales_por_asesora_para_orden) {
$total_a = $totales_por_asesora_para_orden[$a['id']] ?? 0;
$total_b = $totales_por_asesora_para_orden[$b['id']] ?? 0;
return $total_a <=> $total_b;
});
}
}
?>
<div class="container-fluid">
<h1 class="h3 mb-4 text-gray-800">Dashboard</h1>
<p class="mb-4">Bienvenido, <?php echo htmlspecialchars($user_name); ?>!</p>
<!-- Filtro de Fecha -->
<div class="card shadow mb-4">
<div class="card-body">
<form method="get" action="dashboard.php" class="form-inline">
<div class="form-group mr-3">
<label for="month" class="mr-2">Mes:</label>
<select name="month" id="month" class="form-control">
<?php foreach ($meses as $num => $nombre):
echo "<option value=\"" . $num . "\" " . (($num == $selected_month) ? 'selected' : '') . ">" . $nombre . "</option>";
endforeach; ?>
</select>
</div>
<div class="form-group mr-3">
<label for="year" class="mr-2">Año:</label>
<select name="year" id="year" class="form-control">
<?php foreach ($years as $year):
echo "<option value=\"" . $year . "\" " . (($year == $selected_year) ? 'selected' : '') . ">" . $year . "</option>";
endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary">Filtrar</button>
</form>
</div>
</div>
<!-- Fila de tarjetas de resumen para Asesor -->
<?php if ($user_role === 'Asesor'): ?>
<div class="row">
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
Mis Pedidos del Día
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
<?php echo $pedidos_hoy_asesor; ?>
</div>
</div>
<div class="col-auto">
<i class="fas fa-box-open fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-primary shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
Mis Pedidos de <?php echo $meses[$selected_month]; ?>
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
<?php echo $pedidos_mes_asesor; ?>
</div>
</div>
<div class="col-auto">
<i class="fas fa-calendar-alt fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Tarjeta para la Asesora del Mes -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-warning shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
Asesora del Mes (<?php echo $meses[$selected_month]; ?>) - En Curso<br>Bonificacion S/250
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
<?php echo htmlspecialchars($top_advisor_name); ?>
</div>
<div class="text-xs font-weight-bold text-gray-600 mt-1">
<?php echo $top_advisor_total; ?> pedidos
</div>
</div>
<div class="col-auto">
<i class="fas fa-trophy fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Tarjeta para Mejor Efectividad en Completados -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-danger shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-danger text-uppercase mb-1">
Mejor Efectividad en Completados (<?php echo $meses[$selected_month]; ?>) - En Curso
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
<?php echo htmlspecialchars($best_completion_advisor['nombre']); ?>
</div>
<div class="text-xs font-weight-bold text-gray-600 mt-1">
<?php echo round($best_completion_advisor['efectividad'], 2); ?>% de efectividad
</div>
</div>
<div class="col-auto">
<i class="fas fa-check-circle fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<?php endif; ?>
<!-- Card para Pedidos del Día -->
<?php if ($user_role !== 'Asesor'): ?>
<div class="row">
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
Pedidos del Día Cargados
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
<?php echo $pedidos_hoy; ?>
</div>
</div>
<div class="col-auto">
<i class="fas fa-box-open fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-primary shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
Pedidos de <?php echo $meses[$selected_month]; ?>
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
<?php echo $pedidos_mes; ?>
</div>
</div>
<div class="col-auto">
<i class="fas fa-calendar-alt fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
Pedidos en Tránsito (Total)
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
<?php echo $pedidos_enviados; ?>
</div>
</div>
<div class="col-auto">
<i class="fas fa-truck fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Tarjeta para la Asesora del Mes -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-warning shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
Asesora del Mes (<?php echo $meses[$selected_month]; ?>) - En Curso<br>Bonificacion S/250
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
<?php echo htmlspecialchars($top_advisor_name); ?>
</div>
<div class="text-xs font-weight-bold text-gray-600 mt-1">
<?php echo $top_advisor_total; ?> pedidos
</div>
</div>
<div class="col-auto">
<i class="fas fa-trophy fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Tarjeta para Mejor Efectividad en Completados -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-danger shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-danger text-uppercase mb-1">
Mejor Efectividad en Completados (<?php echo $meses[$selected_month]; ?>) - En Curso
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
<?php echo htmlspecialchars($best_completion_advisor['nombre']); ?>
</div>
<div class="text-xs font-weight-bold text-gray-600 mt-1">
<?php echo round($best_completion_advisor['efectividad'], 2); ?>% de efectividad
</div>
</div>
<div class="col-auto">
<i class="fas fa-check-circle fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<?php endif; ?>
<?php if (($user_role === 'Administrador' || $user_role === 'Control Logistico' || $user_role === 'Logistica' || $user_role === 'Asesor')): ?>
<!-- Reporte Mensual de Pedidos por Asesora -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Reporte Mensual de Pedidos por Asesora (<?php echo $meses[$selected_month] . ' ' . $selected_year; ?>)</h6>
</div>
<div class="card-body">
<?php if (empty($asesoras_mensual)): ?>
<div class="alert alert-warning">No hay asesoras configuradas para mostrar en este reporte.</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-bordered table-striped" width="100%" cellspacing="0" style="font-size: 0.8rem;">
<thead class="thead-dark">
<tr class="text-center">
<th style="width: 80px; vertical-align: middle;">Día</th>
<?php foreach ($asesoras_mensual as $asesora): ?>
<th><?php echo htmlspecialchars($asesora['nombre_asesor']); ?></th>
<?php endforeach; ?>
<th style="vertical-align: middle;">Total Día</th>
</tr>
</thead>
<tbody>
<?php
$dias_en_mes = cal_days_in_month(CAL_GREGORIAN, $selected_month, $selected_year);
$totales_por_asesora = array_fill_keys(array_column($asesoras_mensual, 'id'), 0);
$gran_total_mes = 0;
for ($dia = 1; $dia <= $dias_en_mes; $dia++):
$total_dia = 0;
?>
<tr class="text-center">
<td class="font-weight-bold"><?php echo str_pad($dia, 2, '0', STR_PAD_LEFT) . '/' . str_pad($selected_month, 2, '0', STR_PAD_LEFT); ?></td>
<?php foreach ($asesoras_mensual as $asesora):
$asesora_id = $asesora['id'];
$cantidad = $pedidos_mensual_asesora[$dia][$asesora_id] ?? 0;
$total_dia += $cantidad;
$totales_por_asesora[$asesora_id] += $cantidad;
echo '<td>' . ($cantidad > 0 ? $cantidad : '<span class="text-muted">-</span>') . '</td>';
endforeach; ?>
<td class="font-weight-bold"><?php echo $total_dia; ?></td>
</tr>
<?php
$gran_total_mes += $total_dia;
endfor;
?>
</tbody>
<tfoot class="thead-dark">
<tr class="text-center">
<th>Total Mes</th>
<?php foreach ($asesoras_mensual as $asesora): ?>
<th class="text-center"><?php echo $totales_por_asesora[$asesora['id']]; ?></th>
<?php endforeach; ?>
<th class="text-center"><?php echo $gran_total_mes; ?></th>
</tr>
</tfoot>
</table>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php if ($user_role === 'Administrador' || $user_role === 'Control Logistico' || $user_role === 'Logistica' || $user_role === 'Asesor'): ?>
<!-- Gráfico de Rendimiento de Asesores -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Rendimiento de Asesoras (<?php echo $meses[
$selected_month] . ' ' . $selected_year; ?>)</h6>
</div>
<div class="card-body">
<div class="chart-container" style="position: relative; height:40vh; width:100%">
<canvas id="adminChart"></canvas>
</div>
</div>
</div>
<!-- Tabla de Resumen por Asesora -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Resumen Detallado por Asesora (<?php echo $meses[$selected_month] . ' ' . $selected_year; ?>)</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" id="dataTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>Asesora</th>
<?php
$estado_display_map = ['RUTA_CONTRAENTREGA' => 'Ruta Contraentrega', 'RETORNADO' => 'Retornado', 'ENTREGA EXITOSA' => 'Entrega Exitosa'];
foreach ($estados as $estado):
$display_name = $estado_display_map[$estado] ?? $estado;
echo "<th>" . htmlspecialchars($display_name) . "</th>";
endforeach; ?>
<th>Total</th>
</tr>
</thead>
<tbody>
<?php if (empty($asesor_data)):
echo "<tr><td colspan=\"" . (count($estados) + 2) . "\" class=\"text-center\">No hay datos para el período seleccionado.</td></tr>";
else:
// Initialize totals
$totals = array_fill_keys($estados, 0);
$grand_total = 0;
// Definir los grupos de estados para los cálculos de porcentaje
$estados_efectividad = ['RUTA_CONTRAENTREGA', 'RETORNADO', 'ENTREGA EXITOSA'];
foreach ($asesor_data as $data):
echo "<tr>";
echo "<td>" . htmlspecialchars($data['nombre']) . "</td>";
// Calcular los totales para cada grupo
$total_efectividad = 0;
$total_general_grupo = 0;
foreach ($estados_efectividad as $e) {
$total_efectividad += $data['pedidos'][$e];
}
foreach ($estados as $estado) {
if (!in_array($estado, $estados_efectividad)) {
$total_general_grupo += $data['pedidos'][$estado];
}
}
foreach ($estados as $estado):
$cantidad = $data['pedidos'][$estado];
$totals[$estado] += $cantidad;
echo "<td>";
echo $cantidad;
// Calcular porcentaje basado en el grupo
if (in_array($estado, $estados_efectividad)) {
if ($total_efectividad > 0) {
$porcentaje = round(($cantidad / $total_efectividad) * 100, 1);
echo " <span class=\"text-muted\">({" . $porcentaje . "}%)</span>";
}
} else {
if ($total_general_grupo > 0) {
$porcentaje = round(($cantidad / $total_general_grupo) * 100, 1);
echo " <span class=\"text-muted\">({" . $porcentaje . "}%)</span>";
}
}
echo "</td>";
endforeach;
echo "<td><strong>" . $data['total'] . "</strong></td>";
$grand_total += $data['total'];
echo "</tr>";
endforeach;
endif;
?>
</tbody>
<?php if (!empty($asesor_data)):
?>
<tfoot>
<tr style="background-color: #f8f9fc; font-weight: bold;">
<td>Total General</td>
<?php foreach ($totals as $total_estado):
echo "<td>" . $total_estado . "</td>";
endforeach;
echo "<td>" . $grand_total . "</td>";
?>
</tr>
</tfoot>
<?php endif; ?>
</table>
</div>
</div>
</div>
<?php endif; ?>
</div>
<!-- Scripts de Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
<?php if (($user_role === 'Administrador' || $user_role === 'Control Logistico' || $user_role === 'Logistica' || $user_role === 'Asesor') && !empty($chart_data['labels'])):
?>
const adminCtx = document.getElementById('adminChart').getContext('2d');
new Chart(adminCtx, {
type: 'bar',
data: {
labels: <?php echo json_encode($chart_data['labels']); ?>,
datasets: [{
label: 'Total de Pedidos',
data: <?php echo json_encode($chart_data['data']); ?>,
backgroundColor: 'rgba(78, 115, 223, 0.7)',
borderColor: 'rgba(78, 115, 223, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: { stepSize: 1 }
}
},
plugins: {
legend: { display: false }
}
}
});
<?php endif; ?>
});
</script>
<?php include 'layout_footer.php'; ?>

1480
dashboard_principal.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,34 @@
<?php
// Generated by setup_mariadb_project.sh — edit as needed.
define('DB_HOST', '127.0.0.1');
define('DB_NAME', 'app_30953');
define('DB_USER', 'app_30953');
define('DB_PASS', 'e45f2778-db1f-450c-99c6-29efb4601472');
date_default_timezone_set('America/Lima');
if (!function_exists('required_env')) {
function required_env($key) {
$value = getenv($key);
if ($value === false || $value === '') {
throw new RuntimeException('Missing required environment variable: ' . $key);
}
return $value;
}
}
define('DB_HOST', required_env('DB_HOST'));
define('DB_NAME', required_env('DB_NAME'));
define('DB_USER', required_env('DB_USER'));
define('DB_PASS', required_env('DB_PASS'));
if (!function_exists('db')) {
function db() {
static $pdo;
if (!$pdo) {
$pdo = new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4', DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$pdo->exec("SET time_zone = '-05:00'");
}
return $pdo;
}
}

View File

@ -0,0 +1,17 @@
<?php
// Generated by setup_mariadb_project.sh — edit as needed.
define('DB_HOST', '127.0.0.1');
define('DB_NAME', 'app_34849');
define('DB_USER', 'app_34849');
define('DB_PASS', '37a77d1f-97d4-4cf8-8b1e-cfcb83d3b778');
function db() {
static $pdo;
if (!$pdo) {
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
return $pdo;
}

View File

@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS `migrations` (
`id` INT AUTO_INCREMENT PRIMARY_KEY,
`migration` VARCHAR(255) NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

View File

@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(255) NOT NULL,
`role` enum('Administrador','Asesor','Supervisor','Contabilidad') NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -0,0 +1,20 @@
CREATE TABLE IF NOT EXISTS `pedidos` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`dni_cliente` VARCHAR(20) NOT NULL,
`nombre_completo` VARCHAR(255) NOT NULL,
`celular` VARCHAR(20) NOT NULL,
`sede_envio` VARCHAR(100) NOT NULL,
`codigo_rastreo` VARCHAR(100),
`codigo_tracking` VARCHAR(100),
`producto` TEXT NOT NULL,
`cantidad` INT NOT NULL,
`monto_total` DECIMAL(10, 2) NOT NULL,
`monto_adelantado` DECIMAL(10, 2) DEFAULT 0.00,
`monto_debe` DECIMAL(10, 2) NOT NULL,
`estado` ENUM('pendiente_validar', 'contactado', 'pago_en_proceso', 'pagado_validado', 'rechazado', 'cancelado') NOT NULL DEFAULT 'pendiente_validar',
`asesor_id` INT,
`notas` TEXT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (`asesor_id`) REFERENCES `users`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -0,0 +1,3 @@
ALTER TABLE `pedidos`
ADD COLUMN `voucher_adelanto_path` VARCHAR(255) NULL,
ADD COLUMN `voucher_restante_path` VARCHAR(255) NULL;

View File

@ -0,0 +1 @@
ALTER TABLE pedidos ADD COLUMN numero_operacion VARCHAR(255) NULL;

View File

@ -0,0 +1 @@
ALTER TABLE pedidos MODIFY COLUMN estado ENUM('ROTULADO 📦', 'EN TRANSITO 🚛', 'EN DESTINO 🏬', 'COMPLETADO ✅') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'ROTULADO 📦';

View File

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN role VARCHAR(50) NOT NULL DEFAULT 'asesora';

View File

@ -0,0 +1,2 @@
ALTER TABLE pedidos ADD COLUMN asesor_id INT NULL;
ALTER TABLE pedidos ADD CONSTRAINT fk_asesor_id FOREIGN KEY (asesor_id) REFERENCES users(id) ON DELETE SET NULL;

View File

@ -0,0 +1 @@
ALTER TABLE `pedidos` ADD COLUMN `dni_cliente` VARCHAR(20) NULL DEFAULT NULL AFTER `nombre_completo`;

View File

@ -0,0 +1 @@
ALTER TABLE pedidos ADD COLUMN fecha_recojo DATETIME NULL;

View File

@ -0,0 +1 @@
ALTER TABLE pedidos MODIFY fecha_recojo VARCHAR(255) NULL;

View File

@ -0,0 +1,3 @@
-- Actualiza los roles de 'user' a 'Asesor' y de 'admin' a 'Administrador'
UPDATE users SET role = 'Asesor' WHERE role = 'user';
UPDATE users SET role = 'Administrador' WHERE role = 'admin';

View File

@ -0,0 +1 @@
ALTER TABLE pedidos ADD COLUMN estado_pago VARCHAR(50) NOT NULL DEFAULT 'Pendiente a verificación';

View File

@ -0,0 +1,3 @@
-- Add cp_t and cp_f columns to flujo_caja table
ALTER TABLE flujo_caja ADD COLUMN cp_t INT DEFAULT 0;
ALTER TABLE flujo_caja ADD COLUMN cp_f INT DEFAULT 0;

View File

@ -0,0 +1,2 @@
-- Migration: Add nota_adicional column to pedidos table
ALTER TABLE pedidos ADD COLUMN nota_adicional TEXT NULL AFTER clave;

View File

@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

View File

@ -0,0 +1,5 @@
ALTER TABLE products
ADD COLUMN cost DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
ADD COLUMN ads_cost DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
ADD COLUMN commission DECIMAL(10, 2) NOT NULL DEFAULT 0.00;

View File

@ -0,0 +1,4 @@
ALTER TABLE products
ADD COLUMN ingreso_rentabilidad DECIMAL(10, 2) DEFAULT 0.00 NOT NULL AFTER price;
UPDATE products SET ingreso_rentabilidad = price;

View File

@ -0,0 +1 @@
ALTER TABLE products ADD COLUMN unidades_vendidas INT DEFAULT 0;

View File

@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS `rentabilidad_notas` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`titulo` VARCHAR(255) NOT NULL DEFAULT 'Nueva Nota',
`contenido` TEXT,
`fecha_creacion` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS `rentabilidad_reportes` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`titulo` VARCHAR(255) NOT NULL DEFAULT 'Nuevo Reporte',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -0,0 +1,3 @@
-- Add promo_2 and promo_3 to marketing_costos
ALTER TABLE marketing_costos ADD COLUMN promo_2 VARCHAR(255) DEFAULT NULL AFTER promo_1;
ALTER TABLE marketing_costos ADD COLUMN promo_3 VARCHAR(255) DEFAULT NULL AFTER promo_2;

View File

@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS `rentabilidad_productos` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`reporte_id` INT NOT NULL,
`product_id` INT NOT NULL,
`recaudo` DECIMAL(10, 2) DEFAULT 0.00,
`unidades_vendidas` INT DEFAULT 0,
`costo_ads` DECIMAL(10, 2) DEFAULT 0.00,
`comision` DECIMAL(10, 2) DEFAULT 0.00,
FOREIGN KEY (`reporte_id`) REFERENCES `rentabilidad_reportes`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`product_id`) REFERENCES `products`(`id`) ON DELETE CASCADE,
UNIQUE KEY `reporte_product` (`reporte_id`, `product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -0,0 +1,12 @@
-- Add 'Duplicados' to the estado ENUM in the pedidos table
ALTER TABLE pedidos MODIFY COLUMN estado ENUM(
'ROTULADO 📦',
'EN TRANSITO 🚛',
'EN DESTINO 🏬',
'COMPLETADO ✅',
'Gestion',
'RUTA_CONTRAENTREGA',
'ENTREGA EXITOSA',
'RETORNADO',
'Duplicados'
) DEFAULT 'ROTULADO 📦';

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS `rentabilidad_notas`;

View File

@ -0,0 +1,3 @@
ALTER TABLE `products`
DROP COLUMN IF EXISTS `ingreso_rentabilidad`,
DROP COLUMN IF EXISTS `unidades_vendidas`;

View File

@ -0,0 +1 @@
ALTER TABLE rentabilidad_reportes ADD COLUMN fecha_inicio DATE, ADD COLUMN fecha_fin DATE;

View File

@ -0,0 +1,4 @@
-- Add provincia columns to marketing_costos_v3
ALTER TABLE marketing_costos_v3
ADD COLUMN comision_asesora_provincia DECIMAL(10,2) DEFAULT 0.00 AFTER comision_asesora,
ADD COLUMN delivery_provincia DECIMAL(10,2) DEFAULT 0.00 AFTER delivery;

View File

@ -0,0 +1 @@
ALTER TABLE rentabilidad_reportes DROP COLUMN titulo;

View File

@ -0,0 +1 @@
ALTER TABLE rentabilidad_productos ADD COLUMN costo_producto DECIMAL(10, 2) DEFAULT 0.00;

View File

@ -0,0 +1 @@
ALTER TABLE users MODIFY COLUMN role VARCHAR(50) NOT NULL;

View File

@ -0,0 +1 @@
ALTER TABLE `pedidos` ADD `clave` VARCHAR(255) NULL DEFAULT NULL AFTER `codigo_tracking`;

View File

@ -0,0 +1 @@
ALTER TABLE pedidos ADD COLUMN fecha_completado DATETIME DEFAULT NULL;

View File

@ -0,0 +1 @@
ALTER TABLE rentabilidad_productos ADD COLUMN recaudo_ct DECIMAL(10, 2) DEFAULT 0.00;

View File

@ -0,0 +1 @@
ALTER TABLE rentabilidad_productos ADD COLUMN unidades_vendidas_ct INT DEFAULT 0;

View File

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN nombre_asesor VARCHAR(255) NULL;

Some files were not shown because too many files have changed in this diff Show More