Skip to content

Commit 01e6db9

Browse files
committed
Update de layouts, integração com QZ TRAY e impressão manual de pedidos
1 parent 9aa09dc commit 01e6db9

File tree

8 files changed

+240
-29
lines changed

8 files changed

+240
-29
lines changed

app/Controllers/OrderContoller.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace App\Controllers;
4+
5+
use App\Models\Order;
6+
use Psr\Http\Message\ResponseInterface as Response;
7+
use Psr\Http\Message\ServerRequestInterface as Request;
8+
9+
class OrderContoller
10+
{
11+
protected $orderModel;
12+
13+
public function __construct(Order $orderModel)
14+
{
15+
$this->orderModel = $orderModel;
16+
}
17+
18+
/**
19+
* Retorna o último ID de pedido criado
20+
*/
21+
public function getLastOrderId(Request $request, Response $response, $args = [])
22+
{
23+
$pdo = $this->orderModel->getConnection();
24+
$stmt = $pdo->query("SELECT MAX(id) as last_order_id FROM orders");
25+
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
26+
$response->getBody()->write(json_encode(['last_order_id' => (int)($row['last_order_id'] ?? 0)]));
27+
return $response->withHeader('Content-Type', 'application/json');
28+
}
29+
30+
public function getLastOrderByPhone(Request $request, Response $response, $args = [])
31+
{
32+
$phone = $request->getQueryParams()['phone'] ?? '';
33+
$pdo = $this->orderModel->getConnection();
34+
$stmt = $pdo->prepare("SELECT customer_name, customer_address, notes FROM orders WHERE customer_phone = ? ORDER BY id DESC LIMIT 1");
35+
$stmt->execute([$phone]);
36+
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
37+
$response->getBody()->write(json_encode($row ?: []));
38+
return $response->withHeader('Content-Type', 'application/json');
39+
}
40+
}

app/Models/Order.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44

55
class Order extends BaseModel
66
{
7+
/**
8+
* Retorna a conexão DBAL (Doctrine Connection)
9+
*/
10+
public function getConnection()
11+
{
12+
return $this->db;
13+
}
714
public function create($data)
815
{
916
return $this->insert('orders', $data);
@@ -111,4 +118,5 @@ public function generateWhatsAppMessage($order)
111118

112119
return $message;
113120
}
121+
114122
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// admin-print-poll.js
2+
// Script para polling de novos pedidos e impressão automática via QZ Tray
3+
// Requer QZ Tray instalado no PC do admin: https://qz.io/
4+
5+
// CONFIGURAÇÕES
6+
const POLL_INTERVAL = 5000; // ms (5 segundos)
7+
const PRINT_ENDPOINT = '/admin/print-order/'; // endpoint que retorna o JSON de impressão (ajuste se necessário)
8+
const LAST_ORDER_KEY = 'acaiteria_last_printed_order';
9+
10+
// Função para buscar o último pedido do backend
11+
async function fetchLastOrderId() {
12+
try {
13+
const resp = await fetch('/api/last-order-id'); // Crie esse endpoint para retornar o último ID
14+
if (!resp.ok) return null;
15+
const data = await resp.json();
16+
return data.last_order_id;
17+
} catch (e) {
18+
console.error('Erro ao buscar último pedido:', e);
19+
return null;
20+
}
21+
}
22+
23+
// Função para buscar o JSON de impressão de um pedido
24+
async function fetchPrintData(orderId) {
25+
const resp = await fetch(PRINT_ENDPOINT + orderId);
26+
if (!resp.ok) throw new Error('Erro ao buscar dados de impressão');
27+
return await resp.json();
28+
}
29+
30+
// Função para imprimir via QZ Tray
31+
function printWithQZTray(printData) {
32+
if (!window.qz) {
33+
alert('QZ Tray não está disponível. Instale e permita o acesso.');
34+
return;
35+
}
36+
// Exemplo: imprime texto simples (ajuste conforme seu layout)
37+
const config = qz.configs.create(null); // null = impressora padrão
38+
const data = printData.map(line => line.content || '');
39+
qz.print(config, data).catch(err => {
40+
alert('Erro ao imprimir: ' + err);
41+
});
42+
}
43+
44+
// Loop de polling
45+
async function pollForNewOrders() {
46+
let lastPrinted = localStorage.getItem(LAST_ORDER_KEY) || 0;
47+
setInterval(async () => {
48+
const lastOrderId = await fetchLastOrderId();
49+
if (lastOrderId && lastOrderId > lastPrinted) {
50+
try {
51+
const printData = await fetchPrintData(lastOrderId);
52+
printWithQZTray(printData);
53+
localStorage.setItem(LAST_ORDER_KEY, lastOrderId);
54+
} catch (e) {
55+
console.error('Erro ao imprimir pedido:', e);
56+
}
57+
}
58+
}, POLL_INTERVAL);
59+
}
60+
61+
// Inicialização
62+
window.addEventListener('DOMContentLoaded', () => {
63+
if (window.qz) {
64+
qz.websocket.connect().then(() => {
65+
pollForNewOrders();
66+
});
67+
} else {
68+
alert('QZ Tray não detectado. Instale e permita o acesso para impressão automática.');
69+
}
70+
});

routes/web.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use App\Controllers\AdminController;
77
use App\Controllers\AuthController;
88
use App\Controllers\CartController;
9+
use App\Controllers\OrderContoller;
910
use App\Models\Product;
1011
use App\Models\Category;
1112
use App\Models\Ingredient;
@@ -181,6 +182,7 @@
181182
$group->post('/carrinho/adicionar', [CartController::class, 'addItem']);
182183
$group->get('/carrinho/remover/{cart_id}', [CartController::class, 'removeItem']);
183184
$group->post('/carrinho/sync', [CartController::class, 'syncCart']);
185+
$group->get('/api/last-order-by-phone', [OrderContoller::class, 'getLastOrderByPhone']);
184186

185187
// Checkout
186188
$group->get('/checkout', [CartController::class, 'checkout']);

views/templates/admin/dashboard.blade.php

Lines changed: 75 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,38 +12,83 @@
1212
<header class="bg-white shadow">
1313
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
1414
<div class="flex justify-between items-center py-6">
15-
<div>
16-
<h1 class="text-3xl font-bold text-gray-900">{{ $store['store_name'] }}</h1>
17-
<p class="text-gray-600">Painel Administrativo</p>
15+
<!-- Store Info -->
16+
<div class="flex items-center space-x-3">
17+
<div>
18+
<h1 class="text-2xl md:text-3xl font-bold text-gray-900">{{ $store['store_name'] }}</h1>
19+
<p class="text-gray-600 text-sm md:text-base">Painel Administrativo</p>
20+
</div>
1821
</div>
19-
<!---logo upload--->
20-
<div class="flex items-center space-x-4 m-2 mb-3">
21-
<form action="/admin/upload-logo" method="post" enctype="multipart/form-data" class="flex items-center space-x-2">
22-
<label for="logo-upload" class="bg-blue-600 text-white px-4 py-2 rounded cursor-pointer hover:bg-blue-700">
23-
<i class="fas fa-upload mr-1"></i>
24-
Adicionar Logo à loja
25-
</label>
26-
<input type="file" id="logo-upload" name="logo" accept="image/*" class="hidden" onchange="this.form.submit()">
27-
</form>
28-
@if (!empty($store['logo']))
29-
<img src="{{ $store['logo'] }}" alt="Logo da loja" class="h-10 rounded shadow">
30-
@endif
31-
</div>
32-
33-
<div class="flex items-center space-x-4">
34-
<a href="/{{ $store['store_slug'] }}"
35-
target="_blank"
36-
class="text-blue-600 hover:text-blue-800">
22+
<!-- Logo upload & Actions -->
23+
<div class="hidden md:flex items-center space-x-6">
24+
<form action="/admin/upload-logo" method="post" enctype="multipart/form-data" class="flex items-center space-x-2">
25+
<label for="logo-upload" class="bg-blue-600 text-white px-3 py-2 rounded cursor-pointer hover:bg-blue-700 text-sm flex items-center">
26+
<i class="fas fa-upload mr-1"></i>
27+
<span>Atualizar Logo</span>
28+
</label>
29+
<input type="file" id="logo-upload" name="logo" accept="image/*" class="hidden" onchange="this.form.submit()">
30+
</form>
31+
@if (!empty($store['logo']))
32+
<img src="{{ $store['logo'] }}" alt="Logo da loja" class="h-10 w-10 rounded img-fluid shadow">
33+
@endif
34+
<a href="/{{ $store['store_slug'] }}" target="_blank" class="text-blue-600 hover:text-blue-800 text-sm flex items-center">
3735
<i class="fas fa-external-link-alt mr-1"></i>
38-
Ver Cardápio
36+
<span>Ver Cardápio</span>
3937
</a>
40-
<a href="/admin/logout" class="text-red-600 hover:text-red-800">
38+
<a href="/admin/logout" class="text-red-600 hover:text-red-800 text-sm flex items-center">
4139
<i class="fas fa-sign-out-alt mr-1"></i>
42-
Sair
40+
<span>Sair</span>
4341
</a>
4442
</div>
43+
<!-- Botão do menu mobile à direita -->
44+
<button id="menu-toggle" class="md:hidden text-gray-700 focus:outline-none ml-2">
45+
<i class="fas fa-bars text-2xl"></i>
46+
</button>
4547
</div>
4648
</div>
49+
<!-- Offcanvas Mobile Menu (Direita) -->
50+
<div id="offcanvas-menu" class="fixed inset-0 z-50 bg-black bg-opacity-40 hidden">
51+
<div class="fixed right-0 top-0 h-full w-64 bg-white shadow-lg p-6 flex flex-col space-y-6">
52+
<div class="flex items-center justify-between mb-4">
53+
<span class="font-bold text-lg">{{ $store['store_name'] }}</span>
54+
<button id="menu-close" class="text-gray-700 focus:outline-none">
55+
<i class="fas fa-times text-xl"></i>
56+
</button>
57+
</div>
58+
<form action="/admin/upload-logo" method="post" enctype="multipart/form-data" class="flex items-center space-x-2">
59+
<label for="logo-upload-mobile" class="bg-blue-600 text-white px-3 py-2 rounded cursor-pointer hover:bg-blue-700 text-sm flex items-center">
60+
<i class="fas fa-upload mr-1"></i>
61+
<span>Atualizar Logo</span>
62+
</label>
63+
<input type="file" id="logo-upload-mobile" name="logo" accept="image/*" class="hidden" onchange="this.form.submit()">
64+
</form>
65+
@if (!empty($store['logo']))
66+
<img src="{{ $store['logo'] }}" alt="Logo da loja" class="h-10 w-10 rounded img-fluid shadow">
67+
@endif
68+
<a href="/{{ $store['store_slug'] }}" target="_blank" class="text-blue-600 hover:text-blue-800 text-sm flex items-center">
69+
<i class="fas fa-external-link-alt mr-1"></i>
70+
<span>Ver Cardápio</span>
71+
</a>
72+
<a href="/admin/logout" class="text-red-600 hover:text-red-800 text-sm flex items-center">
73+
<i class="fas fa-sign-out-alt mr-1"></i>
74+
<span>Sair</span>
75+
</a>
76+
</div>
77+
</div>
78+
<script>
79+
const menuToggle = document.getElementById('menu-toggle');
80+
const offcanvasMenu = document.getElementById('offcanvas-menu');
81+
const menuClose = document.getElementById('menu-close');
82+
menuToggle?.addEventListener('click', () => {
83+
offcanvasMenu.classList.remove('hidden');
84+
});
85+
menuClose?.addEventListener('click', () => {
86+
offcanvasMenu.classList.add('hidden');
87+
});
88+
offcanvasMenu?.addEventListener('click', (e) => {
89+
if (e.target === offcanvasMenu) offcanvasMenu.classList.add('hidden');
90+
});
91+
</script>
4792
</header>
4893
4994
<main class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
@@ -195,6 +240,12 @@ class="block p-6 bg-purple-50 rounded-lg hover:bg-purple-100 transition-colors">
195240
<div class="mt-1 text-xs text-yellow-700 bg-yellow-50 rounded p-2">Obs: {{ $order['notes'] }}</div>
196241
@endif
197242
</div>
243+
<!-- Botão Imprimir -->
244+
<div class="mt-3 md:mt-0 md:ml-4 flex-shrink-0 flex items-center">
245+
<button type="button" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded flex items-center gap-2 print-btn" data-order-id="{{ $order['id'] }}">
246+
<i class="fas fa-print"></i> Imprimir
247+
</button>
248+
</div>
198249
</div>
199250
@endforeach
200251
</div>

views/templates/cart/checkout.blade.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,22 @@ class="flex-1 bg-green-600 text-white px-6 py-3 rounded-lg hover:bg-green-700 tr
138138
</div>
139139

140140
<script>
141+
// Busca dados do cliente no backend ao digitar telefone
142+
async function autofillCustomerDataFromBackend() {
143+
const phone = document.getElementById('customer_phone').value.replace(/\D/g, '');
144+
if (phone.length >= 8) {
145+
try {
146+
const resp = await fetch(`/api/last-order-by-phone?phone=${encodeURIComponent(phone)}`);
147+
if (!resp.ok) return;
148+
const obj = await resp.json();
149+
if (obj.customer_name) document.getElementById('customer_name').value = obj.customer_name;
150+
if (obj.customer_address) document.getElementById('customer_address').value = obj.customer_address;
151+
if (obj.notes) document.getElementById('notes').value = obj.notes;
152+
} catch (e) {}
153+
}
154+
}
155+
156+
document.getElementById('customer_phone').addEventListener('blur', autofillCustomerDataFromBackend);
141157
// Formatação do telefone
142158
document.getElementById('customer_phone').addEventListener('input', function() {
143159
let value = this.value.replace(/\D/g, '');

views/templates/cart/success.blade.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@
99

1010
<!-- { Ativando print do pedido na impressora } -->
1111
<script>
12+
// Limpa o carrinho do localStorage ao exibir a tela de sucesso
13+
document.addEventListener('DOMContentLoaded', function() {
14+
try {
15+
localStorage.removeItem('cart_{{ $store['id'] }}');
16+
if (typeof updateCartCount === 'function') updateCartCount();
17+
} catch (e) {}
18+
});
1219
window.onload = function() {
1320
window.location.href = "my.bluetoothprint.scheme://https://menu.linksbio.me/imprimir-pedido/{{ $order['id'] }}";
1421
};

views/templates/menu/index.blade.php

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@
1616
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
1717
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" integrity="sha512-Evv84Mr4kqVGRNSgIGL/F/aIDqQb7xQ2vcrdIwxfjThSH8CSR7PBEakCr51Ck+w+/U6swU2Im1vVX0SVk9ABhg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
1818
<script>
19+
// Atualiza o número de itens no carrinho no ícone
20+
function updateCartCount() {
21+
try {
22+
const cart = JSON.parse(localStorage.getItem('cart_{{ $store['id'] }}') || '[]');
23+
document.getElementById('cart-count').textContent = cart.length;
24+
} catch (e) {
25+
document.getElementById('cart-count').textContent = 0;
26+
}
27+
}
28+
29+
// Atualiza o carrinho ao carregar a página
30+
document.addEventListener('DOMContentLoaded', function() {
31+
updateCartCount();
32+
});
1933
tailwind.config = {
2034
theme: {
2135
extend: {
@@ -188,10 +202,10 @@ class="w-full bg-gradient-to-r from-primary to-secondary text-white py-2 px-4 ro
188202
<span class="font-medium">Total:</span>
189203
<span class="text-xl font-bold text-primary" id="totalPrice">R$ 0,00</span>
190204
</div>
191-
<button onclick="addCustomizedToCart()"
192-
class="w-full bg-gradient-to-r from-primary to-secondary text-white py-3 px-4 rounded-lg font-medium hover:from-primary/90 hover:to-secondary/90 transition-all duration-200">
193-
Finalizar <i class="fa-solid fa-arrow-right"></i>
194-
</button>
205+
<button type="button" onclick="addCustomizedToCart()"
206+
class="w-full bg-gradient-to-r from-primary to-secondary text-white py-3 px-4 rounded-lg font-medium hover:from-primary/90 hover:to-secondary/90 transition-all duration-200">
207+
Finalizar <i class="fa-solid fa-arrow-right"></i>
208+
</button>
195209
</div>
196210
</div>
197211
</div>
@@ -442,8 +456,11 @@ function addCustomizedToCart() {
442456
}
443457
localStorage.setItem('cart_{{ $store['id'] }}', JSON.stringify(cart));
444458
updateCartCount();
459+
// Fecha o modal e redireciona após garantir atualização
445460
closeCustomizeModal();
446-
showToast('Produtos adicionados ao carrinho!');
461+
setTimeout(function() {
462+
window.location.href = '/{{ $store_slug }}/carrinho';
463+
}, 100);
447464
}
448465
</script>
449466
</body>

0 commit comments

Comments
 (0)