Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save caysoasesores/55e97b3a3dd5a6fe35789c55544d806b to your computer and use it in GitHub Desktop.

Select an option

Save caysoasesores/55e97b3a3dd5a6fe35789c55544d806b to your computer and use it in GitHub Desktop.
bakerypos-system plugin
<?php
class BakeryPOS_API {
public function __construct() {
// Constructor, aunque esté vacío, es una buena práctica tenerlo.
}
public function register_routes() {
// --- TUS RUTAS ORIGINALES DEL TPV (INTACTAS) ---
register_rest_route('bakerypos/v1', '/login', [
'methods' => 'POST',
'callback' => [$this, 'handle_login_and_get_data'],
'permission_callback' => '__return_true',
]);
register_rest_route('bakerypos/v1', '/sale', [
'methods' => 'POST',
'callback' => [$this, 'handle_sale'],
'permission_callback' => function () { return is_user_logged_in(); }
]);
register_rest_route('bakerypos/v1', '/tasks', [
'methods' => 'GET',
'callback' => [$this, 'get_tasks'],
'permission_callback' => function () { return is_user_logged_in(); }
]);
register_rest_route('bakerypos/v1', '/update-task', [
'methods' => 'POST',
'callback' => [$this, 'update_task_status'],
'permission_callback' => function () { return is_user_logged_in(); }
]);
register_rest_route('bakerypos/v1', '/expenses', [
'methods' => 'GET',
'callback' => [$this, 'get_expense_categories'],
'permission_callback' => function () { return is_user_logged_in(); }
]);
register_rest_route('bakerypos/v1', '/scrap-products', [
'methods' => 'GET',
'callback' => [$this, 'get_scrap_products'],
'permission_callback' => function () { return is_user_logged_in(); }
]);
// --- NUEVAS RUTAS PARA EL PANEL DE ADMINISTRACIÓN ---
register_rest_route('bakerypos/v1', '/branches', array(
'methods' => 'GET',
'callback' => array($this, 'get_all_branches'),
'permission_callback' => function () { return current_user_can('manage_options'); }
));
register_rest_route('bakerypos/v1', '/products', array(
'methods' => 'GET',
'callback' => array($this, 'get_all_products'),
'permission_callback' => function () { return current_user_can('manage_options'); }
));
register_rest_route('bakerypos/v1', '/reports/sales', array(
'methods' => 'GET',
'callback' => array($this, 'get_sales_report'),
'permission_callback' => function () { return current_user_can('manage_options'); }
));
register_rest_route('bakerypos/v1', '/reports/expenses', array(
'methods' => 'GET',
'callback' => array($this, 'get_expenses_report'),
'permission_callback' => function () { return current_user_can('manage_options'); }
));
}
// --- TUS FUNCIONES ORIGINALES DEL TPV (INTACTAS) ---
public function handle_login_and_get_data(WP_REST_Request $request) {
$username = sanitize_text_field($request->get_param('username'));
$password = $request->get_param('password');
$user = wp_authenticate($username, $password);
if (is_wp_error($user)) {
return new WP_Error('login_failed', 'Usuario o contraseña incorrectos.', ['status' => 401]);
}
wp_set_current_user($user->ID);
$products = $this->get_products_data_privately();
$tasks = $this->get_tasks_privately();
$expense_categories = $this->get_expense_categories_privately();
$scrap_products = $this->get_scrap_products_privately();
return new WP_REST_Response([
'success' => true,
'user' => ['display_name' => $user->display_name],
'products' => $products,
'tasks' => $tasks,
'expense_categories' => $expense_categories,
'scrap_products' => $scrap_products,
], 200);
}
public function handle_sale(WP_REST_Request $request) {
// Tu lógica para manejar la venta
return new WP_REST_Response(['success' => true, 'message' => 'Venta registrada.'], 200);
}
public function get_tasks(WP_REST_Request $request) {
return new WP_REST_Response($this->get_tasks_privately(), 200);
}
public function update_task_status(WP_REST_Request $request) {
// Tu lógica para actualizar tareas
return new WP_REST_Response(['success' => true], 200);
}
public function get_expense_categories(WP_REST_Request $request) {
return new WP_REST_Response($this->get_expense_categories_privately(), 200);
}
public function get_scrap_products(WP_REST_Request $request) {
return new WP_REST_Response($this->get_scrap_products_privately(), 200);
}
private function get_products_data_privately() {
$products = wc_get_products(['status' => 'publish', 'limit' => -1]);
$formatted_products = [];
foreach ($products as $product) {
$formatted_products[] = [
'id' => $product->get_id(),
'name' => $product->get_name(),
'price' => wc_format_decimal($product->get_price(), 2),
'image' => wp_get_attachment_image_url($product->get_image_id(), 'thumbnail') ?: wc_placeholder_img_src(),
];
}
return $formatted_products;
}
private function get_tasks_privately() {
return get_option('bakerypos_tasks', [['id' => 1, 'text' => 'Limpiar mostrador', 'completed' => false]]);
}
private function get_expense_categories_privately() {
return get_option('bakerypos_expense_categories', ['Limpieza', 'Insumos', 'Servicios', 'Otros']);
}
private function get_scrap_products_privately() {
$products = wc_get_products(['status' => 'publish', 'limit' => -1]);
$product_list = [];
foreach($products as $product) {
$product_list[] = ['id' => $product->get_id(), 'name' => $product->get_name()];
}
return $product_list;
}
// --- NUEVAS FUNCIONES DE CALLBACK PARA EL ADMIN ---
public function get_all_branches(WP_REST_Request $request) {
global $wpdb;
$table_name = $wpdb->prefix . 'bakerypos_sucursales';
$results = $wpdb->get_results("SELECT sucursal_id, name FROM $table_name ORDER BY name ASC");
return new WP_REST_Response($results, 200);
}
public function get_all_products(WP_REST_Request $request) {
if (!class_exists('WooCommerce')) {
return new WP_Error('woocommerce_inactive', 'WooCommerce no está activo.', array('status' => 500));
}
$products = wc_get_products(array('status' => 'publish', 'limit' => -1));
$formatted_products = array();
foreach ($products as $product) {
$formatted_products[] = array(
'id' => $product->get_id(),
'name' => $product->get_name(),
'sku' => $product->get_sku(),
'stock' => $product->get_stock_quantity() ?? 0,
);
}
return new WP_REST_Response($formatted_products, 200);
}
public function get_sales_report(WP_REST_Request $request) {
$dummy_sales = [
['session_id' => 1, 'user_name' => 'Admin', 'sucursal' => 'Centro', 'total' => 150.50, 'time' => '2025-07-25 10:00:00'],
['session_id' => 2, 'user_name' => 'Vendedor 1', 'sucursal' => 'Norte', 'total' => 85.00, 'time' => '2025-07-25 11:30:00'],
];
return new WP_REST_Response($dummy_sales, 200);
}
public function get_expenses_report(WP_REST_Request $request) {
$dummy_expenses = [
['expense_id' => 1, 'user_name' => 'Vendedor 1', 'sucursal' => 'Norte', 'category' => 'Limpieza', 'amount' => 25.00, 'time' => '2025-07-25 09:15:00'],
['expense_id' => 2, 'user_name' => 'Admin', 'sucursal' => 'Centro', 'category' => 'Insumos', 'amount' => 50.75, 'time' => '2025-07-25 12:00:00'],
];
return new WP_REST_Response($dummy_expenses, 200);
}
}
/* --- Estilos para el Gestor de Tareas --- */
#task-list-sortable {
list-style: none;
margin: 1em 0;
padding: 0;
}
.task-item {
display: flex;
align-items: center;
background: #f9f9f9;
border: 1px solid #ddd;
padding: 10px;
margin-bottom: 5px;
}
.task-item .handle {
cursor: move;
color: #a0a5aa;
margin-right: 10px;
}
.task-item .task-description {
flex-grow: 1;
}
#add-task-form {
display: flex;
gap: 10px;
margin-top: 1em;
}
#add-task-form input {
flex-grow: 1;
}
/* Estilos para páginas de admin (Reportes, Tareas, Sucursales, Inventario) */
.report-section { margin: 2em 0; background: #fff; padding: 1.5em; border: 1px solid #c3c4c7; box-shadow: 0 1px 1px rgba(0,0,0,.04); }
.report-table-wrapper { margin-top: 1em; }
.report-table-wrapper .error { color: #d63638; }
#task-list-sortable { list-style: none; margin: 1em 0; padding: 0; }
.task-item { display: flex; align-items: center; background: #f9f9f9; border: 1px solid #ddd; padding: 10px; margin-bottom: 5px; }
.task-item .handle { cursor: move; color: #a0a5aa; margin-right: 10px; }
.task-item .task-description { flex-grow: 1; }
#add-task-form, #add-branch-form { display: flex; gap: 10px; margin-top: 1em; }
#add-task-form input, #add-branch-form input { flex-grow: 1; }
#inventory-manager .stock-input { width: 100%; text-align: right; }
/* --- ESTILOS GENERALES Y LOGIN --- */
.pos-container {
max-width: 1200px;
margin: 20px auto;
}
#login-screen {
background: #fff;
padding: 2em;
border: 1px solid #ddd;
box-shadow: 0 1px 2px rgba(0, 0, 0, .07);
max-width: 380px;
margin: 2em auto;
}
#login-screen h3 {
margin-top: 0;
border-bottom: 1px solid #eee;
padding-bottom: 15px;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 1.2em;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
.form-group .regular-text {
width: 100%;
padding: 8px 10px;
}
.button.button-large {
width: 100%;
padding: 10px;
height: auto;
font-size: 1.1em;
}
/* --- CABECERA DEL TPV --- */
#pos-header {
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #ddd;
display: flex;
gap: 10px;
}
#pos-header .dashicons {
vertical-align: middle;
margin-top: -3px;
}
#register-scrap-btn {
background: #f8f9fa;
border-color: #d1d3d5;
color: #495057;
}
#register-scrap-btn:hover {
background: #e2e6ea;
}
/* --- INTERFAZ PRINCIPAL DEL TPV --- */
#pos-main-interface {
display: flex;
gap: 20px;
}
#pos-products-grid {
flex: 3;
display: flex;
flex-wrap: wrap;
gap: 15px;
align-content: flex-start;
}
#pos-ticket {
flex: 1;
background: #fdfdfd;
border: 1px solid #ddd;
padding: 20px;
height: fit-content;
border-radius: 4px;
}
.pos-product-card {
width: 140px;
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
text-align: center;
cursor: pointer;
transition: box-shadow 0.2s, transform 0.2s;
}
.pos-product-card:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.pos-product-card img { max-width: 100%; height: 100px; object-fit: cover; margin-bottom: 10px; }
.pos-product-card .product-name { font-weight: 600; margin-bottom: 5px; height: 40px; overflow: hidden; font-size: 0.9em; }
.pos-product-card .product-price { color: #2271b1; font-weight: bold; font-size: 1.1em; }
/* --- ESTILOS DEL TICKET --- */
#pos-ticket h2 { margin-top: 0; border-bottom: 1px solid #eee; padding-bottom: 10px; margin-bottom: 15px; }
#pos-ticket-items { min-height: 200px; margin-bottom: 15px; }
.ticket-empty-message { color: #888; text-align: center; padding-top: 50px; }
.ticket-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 4px; border-bottom: 1px dashed #ddd; gap: 10px; }
.ticket-item .item-name { flex-grow: 1; font-size: 0.95em; }
.ticket-item .item-subtotal { font-weight: 600; min-width: 65px; text-align: right; }
.ticket-item .remove-btn { background: transparent; border: none; color: #a02020; font-size: 22px; font-weight: bold; cursor: pointer; border-radius: 4px; padding: 0 5px; }
.ticket-item .remove-btn:hover { background: #f8d7da; }
#pos-ticket-total { border-top: 2px solid #333; padding-top: 15px; font-size: 1.5em; display: flex; justify-content: space-between; }
#pos-ticket-actions { margin-top: 20px; display: flex; gap: 10px; }
.button-clear-ticket { flex-grow: 1; background: #787c82 !important; border-color: #787c82 !important; color: #fff !important; text-shadow: none !important; box-shadow: none !important; }
.button-clear-ticket:hover { background: #5c6166 !important; border-color: #5c6166 !important; }
.button-pay { flex-grow: 2; }
/* --- ESTILOS PARA TODOS LOS MODALES --- */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
#payment-modal, #expense-modal, #scrap-modal, #task-modal {
background: #fff;
padding: 2em;
border-radius: 8px;
width: 90%;
max-width: 450px;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
}
#payment-modal h2, #expense-modal h2, #scrap-modal h2, #task-modal h2 { margin-top: 0; }
.modal-total { font-size: 1.8em; font-weight: bold; color: #333; text-align: center; margin: 1em 0; border-top: 1px solid #eee; border-bottom: 1px solid #eee; padding: 0.5em 0; }
.payment-methods { display: grid; grid-template-columns: 1fr 1fr; gap: 1em; margin-bottom: 1.5em; }
.payment-method-btn { padding: 1em; font-size: 1.1em; cursor: pointer; border: 2px solid #ddd; background: #f9f9f9; border-radius: 5px; }
.payment-method-btn.active { border-color: #007cba; background: #f0f6fc; font-weight: bold; }
#cash-details { margin-bottom: 1.5em; }
#cash-details input, #expense-modal input, #scrap-modal input, #scrap-modal select { width: 100%; padding: 0.8em; font-size: 1.1em; box-sizing: border-box; }
#cash-details input { text-align: right; }
.cash-change { font-size: 1.4em; text-align: right; margin-top: 0.5em; }
.cash-change span { font-weight: bold; }
.modal-actions { display: flex; justify-content: space-between; gap: 1em; margin-top: 1.5em; }
.modal-actions button { flex-grow: 1; }
/* --- ESTILOS ESPECÍFICOS PARA MODAL DE TAREAS --- */
#task-modal-overlay { z-index: 1001; }
#task-modal { max-width: 500px; }
#task-checklist { list-style: none; margin: 1.5em 0; padding: 0; max-height: 50vh; overflow-y: auto; }
.task-checklist-item { display: flex; align-items: center; font-size: 1.2em; padding: 0.8em 0.5em; border-bottom: 1px solid #eee; }
.task-checklist-item input[type="checkbox"] { width: 20px; height: 20px; margin-right: 15px; }
.task-checklist-item label { flex-grow: 1; }
/* --- RESPONSIVIDAD --- */
@media (max-width: 960px) {
#pos-main-interface { flex-direction: column; }
#pos-products-grid, #pos-ticket { flex: 1 1 100%; }
}
@media (max-width: 480px) {
.pos-product-card { width: calc(50% - 10px); }
#pos-ticket, #payment-modal, #expense-modal, #scrap-modal, #task-modal { padding: 15px; }
}
(function ($) {
'use strict';
$(function () {
const appContainer = $('#branches-manager');
let branches = [];
function loadBranches() {
appContainer.html('<p>Cargando sucursales...</p>');
$.ajax({
url: bakerypos_data.api_url + 'branches',
method: 'GET',
beforeSend: function(xhr) { xhr.setRequestHeader('X-WP-Nonce', bakerypos_data.nonce); }
}).done(function(response) {
if (response.success) {
branches = response.branches;
renderManager();
}
}).fail(function() {
appContainer.html('<p>Error al cargar las sucursales.</p>');
});
}
function renderManager() {
const managerHTML = `
<div class="report-section">
<h2>Sucursales Activas</h2>
<table class="wp-list-table widefat fixed striped">
<thead><tr><th>ID</th><th>Nombre de la Sucursal</th></tr></thead>
<tbody id="branches-list"></tbody>
</table>
</div>
<div class="report-section">
<h2>Añadir Nueva Sucursal</h2>
<form id="add-branch-form">
<input type="text" id="new-branch-name" placeholder="Nombre de la nueva sucursal" required>
<button type="submit" class="button button-primary">Añadir Sucursal</button>
</form>
</div>
`;
appContainer.html(managerHTML);
renderList();
}
function renderList() {
const listContainer = $('#branches-list');
listContainer.empty();
if (branches.length > 0) {
branches.forEach(function(branch) {
listContainer.append(`<tr><td>${branch.branch_id}</td><td>${branch.branch_name}</td></tr>`);
});
} else {
listContainer.html('<tr><td colspan="2">No hay sucursales registradas.</td></tr>');
}
}
appContainer.on('submit', '#add-branch-form', function(e) {
e.preventDefault();
const newName = $('#new-branch-name').val().trim();
if (newName) {
$(this).find('button').text('Añadiendo...').prop('disabled', true);
$.ajax({
url: bakerypos_data.api_url + 'branches',
method: 'POST',
beforeSend: function(xhr) { xhr.setRequestHeader('X-WP-Nonce', bakerypos_data.nonce); },
contentType: 'application/json',
data: JSON.stringify({ branch_name: newName })
}).done(function() {
loadBranches(); // Recargamos la lista
}).fail(function() {
alert('Error al añadir la sucursal.');
});
}
});
loadBranches();
});
})(jQuery);
(function ($) {
'use strict';
$(function () {
const appContainer = $('#inventory-manager');
if (!appContainer.length) return;
// --- Almacenaremos las promesas de las llamadas AJAX ---
let productsRequest;
let branchesRequest;
function renderLayout() {
const layoutHTML = `
<div class="inventory-controls">
<label for="branch-select">Selecciona una Sucursal:</label>
<select id="branch-select" disabled>
<option>Cargando sucursales...</option>
</select>
</div>
<div id="product-inventory-list"><p>Por favor, selecciona una sucursal para ver la lista de productos.</p></div>
`;
appContainer.html(layoutHTML);
}
function loadInitialData() {
// Hacemos las llamadas a la API y guardamos las promesas
branchesRequest = $.ajax({
url: bakerypos_data.api_url + 'branches',
method: 'GET',
beforeSend: function (xhr) { xhr.setRequestHeader('X-WP-Nonce', bakerypos_data.nonce); }
});
productsRequest = $.ajax({
url: bakerypos_data.api_url + 'products',
method: 'GET',
beforeSend: function (xhr) { xhr.setRequestHeader('X-WP-Nonce', bakerypos_data.nonce); }
});
}
function populateBranchesSelect() {
const select = $('#branch-select');
branchesRequest.done(function (branches) {
select.empty().prop('disabled', false);
if (branches && branches.length > 0) {
select.append('<option value="">-- Elige una sucursal --</option>');
branches.forEach(branch => {
select.append(`<option value="${branch.sucursal_id}">${branch.name}</option>`);
});
} else {
select.append('<option>No hay sucursales creadas.</option>');
}
}).fail(function () {
select.empty().append('<option>Error al cargar sucursales.</option>');
});
}
function renderProductTable() {
const productListContainer = $('#product-inventory-list');
productListContainer.html('<p>Cargando inventario...</p>');
// --- ESTA ES LA MAGIA ---
// Le decimos a jQuery que espere a que la llamada de 'products' termine
productsRequest.done(function (allProducts) {
if (!allProducts || allProducts.length === 0) {
productListContainer.html('<p>No se encontraron productos en WooCommerce.</p>');
return;
}
const tableRows = allProducts.map(product => `
<tr>
<td>${product.name} (${product.sku || 'N/A'})</td>
<td>${product.stock}</td>
<td><input type="number" class="small-text" data-product-id="${product.id}" placeholder="0" min="0"></td>
</tr>
`).join('');
const tableHTML = `
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Producto (SKU)</th>
<th>Stock General (WooCommerce)</th>
<th>Cantidad a Asignar</th>
</tr>
</thead>
<tbody>${tableRows}</tbody>
</table>
<p><button class="button button-primary">Asignar Inventario a Sucursal</button></p>
`;
productListContainer.html(tableHTML);
}).fail(function () {
productListContainer.html('<p style="color:red;">Error al cargar la lista de productos desde la API.</p>');
});
}
// --- Eventos ---
appContainer.on('change', '#branch-select', function() {
if ($(this).val()) {
renderProductTable();
} else {
$('#product-inventory-list').html('<p>Por favor, selecciona una sucursal para ver la lista de productos.</p>');
}
});
// --- Inicialización ---
renderLayout();
loadInitialData();
populateBranchesSelect();
});
})(jQuery);
(function ($) {
'use strict';
$(function () {
const appContainer = $('#bakery-pos-app');
if (!appContainer.length) return;
// --- ESTADO GLOBAL DE LA APP ---
let ticketItems = [];
let currentTotal = 0;
let initialProducts = [];
// --- LÓGICA DE INICIO ---
function init() {
if (bakerypos_data.is_logged_in) {
appContainer.html('<div class="pos-container"><p>Cargando datos iniciales...</p></div>');
fetchInitialData();
} else {
renderLoginScreen();
}
}
function fetchInitialData() {
$.ajax({
url: bakerypos_data.api_url + 'initial-data',
method: 'GET',
beforeSend: function (xhr) { xhr.setRequestHeader('X-WP-Nonce', bakerypos_data.nonce); }
}).done(function (response) {
if (response.success) {
initialProducts = response.products;
if (response.tasks && response.tasks.length > 0 && !sessionStorage.getItem('bakerypos_tasks_completed')) {
renderTaskModal(response.tasks);
} else {
renderMainPosScreen(initialProducts);
}
}
}).fail(function() {
appContainer.html('<p>Error al cargar los datos iniciales.</p>');
});
}
// --- RENDERIZADO DE PANTALLAS ---
function renderLoginScreen() {
const loginHTML = `
<div class="pos-container">
<h2>Bienvenido al Punto de Venta</h2>
<div id="login-screen">
<h3>Iniciar Sesión</h3>
<form id="pos-login-form">
<div class="form-group"><label for="pos-username">Usuario</label><input type="text" id="pos-username" class="regular-text" name="username" required></div>
<div class="form-group"><label for="pos-password">Contraseña</label><input type="password" id="pos-password" class="regular-text" name="password" required></div>
<button type="submit" class="button button-primary button-large">Entrar</button>
<div id="login-error" style="display: none; color: red; margin-top: 10px;"></div>
</form>
</div>
</div>`;
appContainer.html(loginHTML);
}
function renderMainPosScreen(products) {
const mainPosHTML = `
<div id="pos-header">
<button class="button" id="register-expense-btn"><span class="dashicons dashicons-money-alt"></span> Registrar Gasto</button>
<button class="button" id="register-scrap-btn"><span class="dashicons dashicons-trash"></span> Registrar Merma</button>
</div>
<div id="pos-main-interface">
<div id="pos-products-grid"></div>
<div id="pos-ticket">
<h2>Ticket de Venta</h2>
<div id="pos-ticket-items"><p class="ticket-empty-message">Selecciona un producto.</p></div>
<div id="pos-ticket-total"><strong>Total:</strong><span id="ticket-total-amount">$0.00</span></div>
<div id="pos-ticket-actions"><button class="button button-large button-clear-ticket">Limpiar</button><button class="button button-primary button-large button-pay" disabled>Pagar</button></div>
</div>
</div>
<!-- Contenedores de Modales -->
<div id="payment-modal-overlay" class="modal-overlay" style="display: none;"></div>
<div id="expense-modal-overlay" class="modal-overlay" style="display: none;"></div>
<div id="scrap-modal-overlay" class="modal-overlay" style="display: none;"></div>
`;
appContainer.html(mainPosHTML);
// Renderizar productos
const productsContainer = $('#pos-products-grid');
if (!products || products.length === 0) {
productsContainer.html('<p>No se encontraron productos.</p>');
} else {
products.forEach(function (product) {
const productCardHTML = `<div class="pos-product-card" data-product-id="${product.id}" data-name="${product.name}" data-price="${product.price}"><img src="${product.image}" alt="${product.name}"><div class="product-name">${product.name}</div><div class="product-price">$${product.price}</div></div>`;
productsContainer.append(productCardHTML);
});
}
}
function renderTaskModal(tasks) {
let taskItemsHTML = tasks.map((task, index) => `
<li class="task-checklist-item">
<input type="checkbox" id="task-${index}" class="task-checkbox">
<label for="task-${index}">${task.task_description}</label>
</li>`).join('');
const taskModalHTML = `
<div id="task-modal-overlay" class="modal-overlay">
<div id="task-modal">
<h2>Tareas Pendientes del Turno</h2>
<ul id="task-checklist">${taskItemsHTML}</ul>
<div class="modal-actions">
<button id="confirm-tasks-btn" class="button button-primary button-large" disabled>Confirmar y Continuar</button>
</div>
</div>
</div>`;
appContainer.html(taskModalHTML);
}
function renderTicket() {
const ticketItemsContainer = $('#pos-ticket-items');
const totalAmountSpan = $('#ticket-total-amount');
const payButton = $('.button-pay');
currentTotal = 0;
ticketItemsContainer.empty();
if (ticketItems.length === 0) {
ticketItemsContainer.html('<p class="ticket-empty-message">Selecciona un producto.</p>');
payButton.prop('disabled', true);
} else {
payButton.prop('disabled', false);
ticketItems.forEach(function(item) {
const itemSubtotal = item.price * item.quantity;
currentTotal += itemSubtotal;
const ticketItemHTML = `<div class="ticket-item" data-product-id="${item.id}"><span class="item-name">${item.name} (x${item.quantity})</span><span class="item-subtotal">$${itemSubtotal.toFixed(2)}</span><button class="control-btn remove-btn">×</button></div>`;
ticketItemsContainer.append(ticketItemHTML);
});
}
totalAmountSpan.text(`$${currentTotal.toFixed(2)}`);
}
// --- MANEJO DE EVENTOS ---
// Login
appContainer.on('submit', '#pos-login-form', function (e) {
e.preventDefault();
const $button = $(this).find('button[type="submit"]');
$button.text('Verificando...').prop('disabled', true);
$.ajax({ url: bakerypos_data.api_url + 'login', method: 'POST', contentType: 'application/json', data: JSON.stringify({ username: $('#pos-username').val(), password: $('#pos-password').val() }) })
.done(response => { if (response.success) window.location.reload(); })
.fail(jqXHR => { $('#login-error').text(jqXHR.responseJSON?.message || 'Error').show(); $button.text('Entrar').prop('disabled', false); });
});
// Tareas
appContainer.on('change', '.task-checkbox', function() {
const allChecked = $('.task-checkbox').length === $('.task-checkbox:checked').length;
$('#confirm-tasks-btn').prop('disabled', !allChecked);
});
appContainer.on('click', '#confirm-tasks-btn', function() {
sessionStorage.setItem('bakerypos_tasks_completed', 'true');
renderMainPosScreen(initialProducts);
});
// Ticket
appContainer.on('click', '.pos-product-card', function() { const productId = $(this).data('product-id'); const productName = $(this).data('name'); const productPrice = parseFloat($(this).data('price')); const existingItem = ticketItems.find(item => item.id === productId); if (existingItem) { existingItem.quantity++; } else { ticketItems.push({ id: productId, name: productName, price: productPrice, quantity: 1 }); } renderTicket(); });
appContainer.on('click', '.remove-btn', function() { const productId = $(this).closest('.ticket-item').data('product-id'); ticketItems = ticketItems.filter(i => i.id !== productId); renderTicket(); });
appContainer.on('click', '.button-clear-ticket', function() { if (confirm('¿Estás seguro?')) { ticketItems = []; renderTicket(); } });
// Modal de Pago
let selectedPaymentMethod = null;
appContainer.on('click', '.button-pay', function() {
const paymentModalHTML = `<div id="payment-modal"><h2>Finalizar Venta</h2><div class="modal-total">Total: <span id="modal-total-amount">$${currentTotal.toFixed(2)}</span></div><div class="payment-methods"><button class="payment-method-btn" data-method="Efectivo">Efectivo</button><button class="payment-method-btn" data-method="Tarjeta">Tarjeta</button></div><div id="cash-details" style="display: none;"><label for="cash-received">Recibido:</label><input type="number" id="cash-received" step="0.01" min="0"><div class="cash-change">Cambio: <span id="cash-change-amount">$0.00</span></div></div><div class="modal-actions"><button id="cancel-payment-btn" class="button">Cancelar</button><button id="finalize-sale-btn" class="button button-primary" disabled>Finalizar Venta</button></div></div>`;
$('#payment-modal-overlay').html(paymentModalHTML).fadeIn(200);
});
appContainer.on('click', '#cancel-payment-btn, #payment-modal-overlay', function(e) { if (e.target === this || e.target.id === 'cancel-payment-btn') $('#payment-modal-overlay').fadeOut(200); });
appContainer.on('click', '.payment-method-btn', function() { selectedPaymentMethod = $(this).data('method'); $('.payment-method-btn').removeClass('active'); $(this).addClass('active'); if (selectedPaymentMethod === 'Efectivo') { $('#cash-details').slideDown(100); $('#cash-received').focus(); $('#finalize-sale-btn').prop('disabled', (parseFloat($('#cash-received').val()) || 0) < currentTotal); } else { $('#cash-details').slideUp(100); $('#finalize-sale-btn').prop('disabled', false); } });
appContainer.on('input', '#cash-received', function() { const received = parseFloat($(this).val()) || 0; const change = received - currentTotal; $('#cash-change-amount').text(`$${change > 0 ? change.toFixed(2) : '0.00'}`); $('#finalize-sale-btn').prop('disabled', received < currentTotal); });
appContainer.on('click', '#finalize-sale-btn', function() { const $button = $(this); $button.text('Procesando...').prop('disabled', true); $.ajax({ url: bakerypos_data.api_url + 'sale', method: 'POST', beforeSend: function(xhr) { xhr.setRequestHeader('X-WP-Nonce', bakerypos_data.nonce); }, contentType: 'application/json', data: JSON.stringify({ items: ticketItems, total: currentTotal, payment_method: selectedPaymentMethod }) }).done(() => { alert('¡Venta registrada!'); ticketItems = []; renderTicket(); $('#payment-modal-overlay').fadeOut(200); }).fail(jqXHR => { alert("Error: " + (jqXHR.responseJSON?.message || 'desconocido')); $button.text('Finalizar Venta').prop('disabled', false); }); });
// Modal de Gastos
appContainer.on('click', '#register-expense-btn', function() {
const expenseModalHTML = `<div id="expense-modal"><h2>Registrar Gasto</h2><div class="form-group"><label for="expense-amount">Monto ($):</label><input type="number" id="expense-amount" step="0.01" min="0.01" required></div><div class="form-group"><label for="expense-description">Descripción:</label><input type="text" id="expense-description" required maxlength="250"></div><div class="modal-actions"><button id="cancel-expense-btn" class="button">Cancelar</button><button id="finalize-expense-btn" class="button button-primary" disabled>Guardar</button></div></div>`;
$('#expense-modal-overlay').html(expenseModalHTML).fadeIn(200);
});
appContainer.on('click', '#cancel-expense-btn, #expense-modal-overlay', function(e) { if(e.target === this || e.target.id === 'cancel-expense-btn') $('#expense-modal-overlay').fadeOut(200); });
appContainer.on('input', '#expense-amount, #expense-description', function() { const amount = parseFloat($('#expense-amount').val()) || 0; const description = $('#expense-description').val().trim(); $('#finalize-expense-btn').prop('disabled', amount <= 0 || description === ''); });
appContainer.on('click', '#finalize-expense-btn', function() { const $button = $(this); $button.text('Guardando...').prop('disabled', true); $.ajax({ url: bakerypos_data.api_url + 'expense', method: 'POST', beforeSend: function(xhr) { xhr.setRequestHeader('X-WP-Nonce', bakerypos_data.nonce); }, contentType: 'application/json', data: JSON.stringify({ amount: $('#expense-amount').val(), description: $('#expense-description').val() }) }).done(() => { alert('¡Gasto registrado!'); $('#expense-modal-overlay').fadeOut(200); }).fail(jqXHR => { alert("Error: " + (jqXHR.responseJSON?.message || 'desconocido')); $button.text('Guardar').prop('disabled', false); }); });
// Modal de Merma (Scrap)
appContainer.on('click', '#register-scrap-btn', function() {
let productOptions = initialProducts.map(p => `<option value="${p.id}">${p.name}</option>`).join('');
const scrapModalHTML = `<div id="scrap-modal"><h2>Registrar Merma</h2><div class="form-group"><label for="scrap-product">Producto:</label><select id="scrap-product" required><option value="">-- Selecciona --</option>${productOptions}</select></div><div class="form-group"><label for="scrap-quantity">Cantidad:</label><input type="number" id="scrap-quantity" min="1" step="1" required></div><div class="form-group"><label for="scrap-reason">Motivo:</label><select id="scrap-reason" required><option value="">-- Selecciona --</option><option value="Dañado">Dañado</option><option value="Caducado">Caducado</option><option value="Otro">Otro</option></select></div><div class="modal-actions"><button id="cancel-scrap-btn" class="button">Cancelar</button><button id="finalize-scrap-btn" class="button button-primary" disabled>Registrar</button></div></div>`;
$('#scrap-modal-overlay').html(scrapModalHTML).fadeIn(200);
});
appContainer.on('click', '#cancel-scrap-btn, #scrap-modal-overlay', function(e) { if(e.target === this || e.target.id === 'cancel-scrap-btn') $('#scrap-modal-overlay').fadeOut(200); });
appContainer.on('change input', '#scrap-product, #scrap-quantity, #scrap-reason', function() { const product = $('#scrap-product').val(); const quantity = parseInt($('#scrap-quantity').val()) || 0; const reason = $('#scrap-reason').val(); $('#finalize-scrap-btn').prop('disabled', !product || quantity <= 0 || !reason); });
appContainer.on('click', '#finalize-scrap-btn', function() { const $button = $(this); $button.text('Registrando...').prop('disabled', true); $.ajax({ url: bakerypos_data.api_url + 'scrap', method: 'POST', beforeSend: function(xhr) { xhr.setRequestHeader('X-WP-Nonce', bakerypos_data.nonce); }, contentType: 'application/json', data: JSON.stringify({ product_id: $('#scrap-product').val(), quantity: $('#scrap-quantity').val(), reason: $('#scrap-reason').val() }) }).done(() => { alert('¡Merma registrada!'); $('#scrap-modal-overlay').fadeOut(200); }).fail(jqXHR => { alert("Error: " + (jqXHR.responseJSON?.message || 'desconocido')); $button.text('Registrar').prop('disabled', false); }); });
// --- INICIAR LA APLICACIÓN ---
init();
});
})(jQuery);
(function ($) {
'use strict';
$(function () {
const salesContainer = $('#sales-report-table');
const expensesContainer = $('#expenses-report-table');
if (!salesContainer.length || !expensesContainer.length) {
$('#reports-container').html('<p style="color:red;">Error de configuración del plugin: No se encontraron los contenedores de reportes.</p>');
return;
}
function loadSales() {
$.ajax({
url: bakerypos_data.api_url + 'reports/sales',
method: 'GET',
beforeSend: function (xhr) { xhr.setRequestHeader('X-WP-Nonce', bakerypos_data.nonce); }
})
.done(function (sales) {
if (!sales || sales.length === 0) {
salesContainer.html('<p>No hay datos de ventas disponibles.</p>');
return;
}
const tableRows = sales.map(sale => `
<tr>
<td>${sale.session_id}</td>
<td>${sale.user_name}</td>
<td>${sale.sucursal}</td>
<td>$${sale.total.toFixed(2)}</td>
<td>${sale.time}</td>
</tr>
`).join('');
const tableHTML = `
<table class="wp-list-table widefat fixed striped">
<thead><tr><th>Sesión ID</th><th>Vendedor</th><th>Sucursal</th><th>Total</th><th>Fecha</th></tr></thead>
<tbody>${tableRows}</tbody>
</table>`;
salesContainer.html(tableHTML);
})
.fail(function () {
salesContainer.html('<p style="color:red;">Error al cargar el reporte de ventas.</p>');
});
}
function loadExpenses() {
$.ajax({
url: bakerypos_data.api_url + 'reports/expenses',
method: 'GET',
beforeSend: function (xhr) { xhr.setRequestHeader('X-WP-Nonce', bakerypos_data.nonce); }
})
.done(function (expenses) {
if (!expenses || expenses.length === 0) {
expensesContainer.html('<p>No hay datos de gastos disponibles.</p>');
return;
}
const tableRows = expenses.map(exp => `
<tr>
<td>${exp.expense_id}</td>
<td>${exp.user_name}</td>
<td>${exp.sucursal}</td>
<td>${exp.category}</td>
<td>$${exp.amount.toFixed(2)}</td>
<td>${exp.time}</td>
</tr>
`).join('');
const tableHTML = `
<table class="wp-list-table widefat fixed striped">
<thead><tr><th>ID</th><th>Vendedor</th><th>Sucursal</th><th>Categoría</th><th>Monto</th><th>Fecha</th></tr></thead>
<tbody>${tableRows}</tbody>
</table>`;
expensesContainer.html(tableHTML);
})
.fail(function () {
expensesContainer.html('<p style="color:red;">Error al cargar el reporte de gastos.</p>');
});
}
loadSales();
loadExpenses();
});
})(jQuery);
(function ($) {
'use strict';
$(function () {
const appContainer = $('#task-manager');
let taskList = [];
function loadTasks() {
appContainer.html('<p>Cargando tareas...</p>');
$.ajax({
url: bakerypos_tasks_data.api_url + 'tasks',
method: 'GET',
beforeSend: function(xhr) { xhr.setRequestHeader('X-WP-Nonce', bakerypos_tasks_data.nonce); }
}).done(function(response) {
if (response.success) {
taskList = response.tasks;
renderTaskManager();
}
}).fail(function() {
appContainer.html('<p>Error al cargar las tareas.</p>');
});
}
function renderTaskManager() {
const managerHTML = `
<div class="report-section">
<h2>Checklist Actual (Turno Matutino)</h2>
<ul id="task-list-sortable"></ul>
<form id="add-task-form">
<input type="text" id="new-task-description" placeholder="Añadir nueva tarea..." required>
<button type="submit" class="button button-primary">Añadir Tarea</button>
</form>
</div>
`;
appContainer.html(managerHTML);
renderTaskList();
}
function renderTaskList() {
const listContainer = $('#task-list-sortable');
listContainer.empty();
if (taskList.length > 0) {
taskList.forEach(function(task) {
const taskHTML = `
<li class="task-item" data-task-id="${task.task_id}">
<span class="dashicons dashicons-menu handle"></span>
<span class="task-description">${task.task_description}</span>
<button class="button button-small delete-task-btn">Eliminar</button>
</li>
`;
listContainer.append(taskHTML);
});
} else {
listContainer.html('<li>No hay tareas en la lista.</li>');
}
}
function saveTasks() {
$.ajax({
url: bakerypos_tasks_data.api_url + 'tasks',
method: 'POST',
beforeSend: function(xhr) { xhr.setRequestHeader('X-WP-Nonce', bakerypos_tasks_data.nonce); },
contentType: 'application/json',
data: JSON.stringify({ tasks: taskList })
}).done(function() {
// Opcional: mostrar un mensaje de "Guardado"
}).fail(function() {
alert('Error al guardar las tareas.');
});
}
// --- Eventos ---
appContainer.on('submit', '#add-task-form', function(e) {
e.preventDefault();
const description = $('#new-task-description').val().trim();
if (description) {
taskList.push({ task_id: 'new_' + Date.now(), task_description: description });
renderTaskList();
saveTasks();
$('#new-task-description').val('');
}
});
appContainer.on('click', '.delete-task-btn', function() {
if (confirm('¿Seguro que quieres eliminar esta tarea?')) {
const taskId = $(this).closest('.task-item').data('task-id');
taskList = taskList.filter(task => task.task_id != taskId);
renderTaskList();
saveTasks();
}
});
$('#task-list-sortable').sortable({
handle: '.handle',
update: function(event, ui) {
let newOrder = [];
$('#task-list-sortable .task-item').each(function() {
const taskId = $(this).data('task-id');
const task = taskList.find(t => t.task_id == taskId);
if (task) {
newOrder.push(task);
}
});
taskList = newOrder;
saveTasks();
}
}).disableSelection();
loadTasks();
});
})(jQuery);
<?php
/**
* Plugin Name: BakeryPOS System
* Description: Sistema de Punto de Venta para panaderías con multi-inventario.
* Version: 3.1.0
* Author: Tu Nombre
*/
if ( ! defined( 'WPINC' ) ) die;
define( 'BAKERYPOS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'BAKERYPOS_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
require_once BAKERYPOS_PLUGIN_DIR . 'api/class-bakerypos-api.php';
class BakeryPOS_System_Final {
public function __construct() {
add_action( 'admin_menu', array( $this, 'add_admin_pages' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_assets' ) );
add_shortcode( 'bakery_pos', array( $this, 'render_pos_shortcode' ) );
add_action( 'rest_api_init', function () { (new BakeryPOS_API())->register_routes(); });
}
public function add_admin_pages() {
add_menu_page('BakeryPOS', 'BakeryPOS', 'manage_options', 'bakerypos-reports', array($this, 'display_reports_page'), 'dashicons-chart-pie', 25);
add_submenu_page('bakerypos-reports', 'Reportes', 'Reportes', 'manage_options', 'bakerypos-reports', array($this, 'display_reports_page'));
// --- NUEVOS SUBMENÚS ---
add_submenu_page('bakerypos-reports', 'Sucursales', 'Sucursales', 'manage_options', 'bakerypos-branches', array($this, 'display_branches_page'));
add_submenu_page('bakerypos-reports', 'Asignar Inventario', 'Asignar Inventario', 'manage_options', 'bakerypos-inventory', array($this, 'display_inventory_page'));
add_submenu_page('bakerypos-reports', 'Gestor de Tareas', 'Gestor de Tareas', 'manage_options', 'bakerypos-tasks', array($this, 'display_tasks_page'));
}
public function display_reports_page() { echo '<div class="wrap" id="bakerypos-reports-app"><h1>Reportes</h1><div id="reports-container"><div class="report-section"><h2>Ventas</h2><div id="sales-report-table"><p>Cargando...</p></div></div><div class="report-section"><h2>Gastos</h2><div id="expenses-report-table"><p>Cargando...</p></div></div></div></div>'; }
public function display_tasks_page() { echo '<div class="wrap" id="bakerypos-tasks-app"><h1>Gestor de Tareas</h1><div id="task-manager"><p>Cargando...</p></div></div>'; }
public function display_branches_page() { echo '<div class="wrap" id="bakerypos-branches-app"><h1>Gestor de Sucursales</h1><div id="branches-manager">Cargando...</div></div>'; }
public function display_inventory_page() { echo '<div class="wrap" id="bakerypos-inventory-app"><h1>Asignar Inventario por Sucursal</h1><div id="inventory-manager">Cargando...</div></div>'; }
public function enqueue_admin_assets($hook) {
$version = '3.1.0';
if ($hook === 'toplevel_page_bakerypos-reports' || $hook === 'bakerypos_page_bakerypos-reports') {
wp_enqueue_style('bakerypos-admin-style', BAKERYPOS_PLUGIN_URL . 'assets/css/admin-style.css', array(), $version);
wp_enqueue_script('bakerypos-reports-app', BAKERYPOS_PLUGIN_URL . 'assets/js/reports-app.js', array('jquery'), $version, true);
wp_localize_script('bakerypos-reports-app', 'bakerypos_data', array('api_url' => rest_url('bakerypos/v1/'), 'nonce' => wp_create_nonce('wp_rest')));
}
if ($hook === 'bakerypos_page_bakerypos-tasks') {
wp_enqueue_style('bakerypos-admin-style', BAKERYPOS_PLUGIN_URL . 'assets/css/admin-style.css', array(), $version);
wp_enqueue_script('bakerypos-tasks-app', BAKERYPOS_PLUGIN_URL . 'assets/js/tasks-app.js', array('jquery', 'jquery-ui-sortable'), $version, true);
wp_localize_script('bakerypos-tasks-app', 'bakerypos_data', array('api_url' => rest_url('bakerypos/v1/'), 'nonce' => wp_create_nonce('wp_rest')));
}
if ($hook === 'bakerypos_page_bakerypos-branches') {
wp_enqueue_style('bakerypos-admin-style', BAKERYPOS_PLUGIN_URL . 'assets/css/admin-style.css', array(), $version);
wp_enqueue_script('bakerypos-branches-app', BAKERYPOS_PLUGIN_URL . 'assets/js/branches-app.js', array('jquery'), $version, true);
wp_localize_script('bakerypos-branches-app', 'bakerypos_data', array('api_url' => rest_url('bakerypos/v1/'), 'nonce' => wp_create_nonce('wp_rest')));
}
if ($hook === 'bakerypos_page_bakerypos-inventory') {
wp_enqueue_style('bakerypos-admin-style', BAKERYPOS_PLUGIN_URL . 'assets/css/admin-style.css', array(), $version);
wp_enqueue_script('bakerypos-inventory-app', BAKERYPOS_PLUGIN_URL . 'assets/js/inventory-app.js', array('jquery'), $version, true);
wp_localize_script('bakerypos-inventory-app', 'bakerypos_data', array('api_url' => rest_url('bakerypos/v1/'), 'nonce' => wp_create_nonce('wp_rest')));
}
}
public function enqueue_frontend_assets() {
global $post;
if (is_a($post, 'WP_Post') && has_shortcode($post->post_content, 'bakery_pos')) {
$script_handle = 'bakerypos-pos-app';
wp_enqueue_style('bakerypos-pos-style', BAKERYPOS_PLUGIN_URL . 'assets/css/pos-style.css', array(), '3.1.0');
wp_enqueue_script($script_handle, BAKERYPOS_PLUGIN_URL . 'assets/js/pos-app.js', array('jquery'), '3.1.0', true);
wp_localize_script($script_handle, 'bakerypos_data', array('api_url' => rest_url('bakerypos/v1/'),'is_logged_in' => is_user_logged_in(),'nonce' => wp_create_nonce('wp_rest')));
}
}
public function render_pos_shortcode() {
return '<div id="bakery-pos-app"><h1>Cargando...</h1></div>';
}
}
new BakeryPOS_System_Final();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment