Merge pull request #6 from welton89/dev_home

Dev home
This commit is contained in:
Welton Moura
2025-08-16 17:42:53 -03:00
committed by GitHub
36 changed files with 801 additions and 260 deletions

View File

@@ -6,7 +6,7 @@ from django.contrib.auth.models import User
from comandas.models import Comanda, ProductComanda
from comandas.models import Comanda, ProductComanda, StockMovementType, StockMovement
from mesas.models import Mesa
from products.models import Product
from payments.models import Payments
@@ -30,6 +30,20 @@ def addProductBalcao(request, product_id, comanda_id, qtd):
for i in range(qtd):
product_comanda = ProductComanda(comanda_id=comanda_id, product_id=product_id)
product_comanda.save()
product = Product.objects.get(id=product_id)
comanda = Comanda.objects.get(id=comanda_id)
user = User.objects.get(id=request.user.id)
typeMovement = StockMovementType.objects.get(name="Venda - Balcao")
StockMovement.subTransactionStock(
product=product,
movement_type=typeMovement,
comanda=comanda,
user=user,
qtd=1,
obs= "Vendido no balcão"
)
consumo = ProductComanda.objects.filter(comanda=comanda_id)
total = 0
for produto in consumo:
@@ -42,6 +56,20 @@ def addProductBalcaoTeclado(request, product_id, comanda_id, qtd):
for i in range(qtd):
product_comanda = ProductComanda(comanda_id=comanda_id, product_id=product_id)
product_comanda.save()
product = Product.objects.get(id=product_id)
comanda = Comanda.objects.get(id=comanda_id)
user = User.objects.get(id=request.user.id)
typeMovement = StockMovementType.objects.get(name="Venda - Balcao")
StockMovement.subTransactionStock(
product=product,
movement_type=typeMovement,
comanda=comanda,
user=user,
qtd=1,
obs= "Vendido no balcão"
)
consumo = ProductComanda.objects.filter(comanda=comanda_id)
total = 0
for produto in consumo:
@@ -51,8 +79,24 @@ def addProductBalcaoTeclado(request, product_id, comanda_id, qtd):
@group_required(groupName='Garçom')
def removeProductBalcao(request, productComanda_id):
product_comanda = ProductComanda.objects.get(id=productComanda_id)
comanda = product_comanda.comanda
product = product_comanda.product
user = User.objects.get(id=request.user.id)
typeMovement = StockMovementType.objects.get(name="Estorno")
consumo = ProductComanda.objects.filter(comanda=product_comanda.comanda)
StockMovement.sumTransactionStock(
product=product,
movement_type=typeMovement,
comanda=comanda,
user=user,
qtd=1,
obs= "Excluido do balcão"
)
product_comanda.delete()
total = 0
for produto in consumo:
total += produto.product.price

View File

@@ -1,7 +1,9 @@
from django.contrib import admin
from comandas.models import Comanda, ProductComanda
from comandas.models import Comanda, ProductComanda, StockMovement, StockMovementType
admin.site.register(Comanda)
admin.site.register(ProductComanda)
admin.site.register(StockMovementType)
admin.site.register(StockMovement)

View File

@@ -2,8 +2,10 @@ from datetime import date
from decimal import Decimal
from django.http import JsonResponse
from django.shortcuts import render, redirect
from django.contrib.auth.models import User
from comandas.models import Comanda, ProductComanda
from comandas.models import Comanda, ProductComanda, StockMovementType, StockMovement
from orders.models import Order
from products.models import Product
from payments.models import Payments, somar
@@ -37,14 +39,28 @@ def removeProductComanda(request, productComanda_id):
'taxa': False
}
product_comanda = ProductComanda.objects.get(id=productComanda_id)
comanda = Comanda.objects.get(id= product_comanda.comanda.id)
comanda = product_comanda.comanda
product = product_comanda.product
user = User.objects.get(id=request.user.id)
typeMovement = StockMovementType.objects.get(name="Estorno")
if comanda.status == 'OPEN':
parcial = Payments.objects.filter(comanda=comanda)
consumo = ProductComanda.objects.filter(comanda=comanda)
valores = somar(consumo,comanda)
if product_comanda.product.cuisine == True:
order = Order.objects.get(productComanda=product_comanda)
StockMovement.sumTransactionStock(
product=product,
movement_type=typeMovement,
comanda=comanda,
user=user,
qtd=1,
obs= "Excluido da comanda"
)
product_comanda.delete()
msg = JsonResponse({
'type': 'broadcast',
'message': 'Atenção! Pedido cancelado',
@@ -58,6 +74,14 @@ def removeProductComanda(request, productComanda_id):
consumo = ProductComanda.objects.filter(comanda=comanda)
valores = somar(consumo,comanda)
else:
StockMovement.sumTransactionStock(
product=product,
movement_type=typeMovement,
comanda=comanda,
user=user,
qtd=1,
obs= "Excluido da comanda"
)
product_comanda.delete()
consumo = ProductComanda.objects.filter(comanda=comanda)
valores = somar(consumo,comanda)

View File

@@ -0,0 +1,42 @@
# Generated by Django 5.1.4 on 2025-07-22 18:36
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('comandas', '0004_comanda_user'),
('products', '0004_unitofmeasure_productcomponent_product_components_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='StockMovementType',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(help_text="Ex: 'Entrada por Compra', 'Saída por Venda', 'Ajuste de Estoque'", max_length=100, unique=True)),
('observation', models.TextField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='StockMovement',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('quantity', models.IntegerField(help_text='Quantidade movimentada. Use valores negativos para saídas.')),
('observation', models.TextField(blank=True, null=True)),
('movement_date', models.DateTimeField(auto_now_add=True)),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stock_movements', to='products.product')),
('related_comanda', models.ForeignKey(blank=True, help_text='Comanda relacionada à movimentação (opcional).', null=True, on_delete=django.db.models.deletion.SET_NULL, to='comandas.comanda')),
('user', models.ForeignKey(blank=True, help_text='Usuário que realizou a movimentação.', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('movement_type', models.ForeignKey(help_text='Tipo de movimentação (entrada, saída, ajuste).', on_delete=django.db.models.deletion.PROTECT, to='comandas.stockmovementtype')),
],
options={
'ordering': ['-movement_date'],
},
),
]

View File

@@ -5,7 +5,7 @@ from django.db.models import Count, F
from clients.models import Client
from products.models import Product
from products.models import Product, ProductComponent
from mesas.models import Mesa
from typePay.models import TypePay
@@ -43,4 +43,109 @@ class ProductComanda(models.Model):
for p in products:
if p.name == produto['nome'] and p.active == True:
products_ordenados.append(p)
return products_ordenados[:15]
return products_ordenados[:15]
class StockMovementType(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=100, unique=True, help_text="Ex: 'Entrada por Compra', 'Saída por Venda', 'Ajuste de Estoque'")
observation = models.TextField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
class StockMovement(models.Model):
id = models.AutoField(primary_key=True)
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='stock_movements')
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, help_text="Usuário que realizou a movimentação.")
related_comanda = models.ForeignKey(
Comanda,
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Comanda relacionada à movimentação (opcional)."
)
movement_type = models.ForeignKey(StockMovementType, on_delete=models.PROTECT, help_text="Tipo de movimentação (entrada, saída, ajuste).")
quantity = models.IntegerField(help_text="Quantidade movimentada. Use valores negativos para saídas.")
observation = models.TextField(null=True, blank=True)
movement_date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return (
f"Movimentação de {self.quantity} de {self.product.name} "
f"({self.movement_type.name}) em {self.movement_date.strftime('%d/%m/%Y %H:%M')}"
)
def subTransactionStock(product:Product,
movement_type:StockMovementType,
comanda:Comanda,
obs:str,
user:User=None,
qtd:int=1):
components = ProductComponent.objects.filter(composite_product=product)
if components.exists():
for component in components:
movi = StockMovement.objects.create(
product=component.component_product ,
related_comanda=comanda,
user=user,
movement_type=movement_type,
quantity=-component.quantity_required,
observation=obs
)
movi.save()
component.component_product.quantity -= component.quantity_required
component.component_product.save()
movi = StockMovement.objects.create(
product=product ,
related_comanda=comanda,
user=user,
movement_type=movement_type,
quantity=-qtd,
observation=obs
)
movi.save()
product.quantity -= qtd
product.save()
def sumTransactionStock(product:Product,
movement_type:StockMovementType,
comanda:Comanda,
obs:str,
user:User=None,
qtd:int=1):
components = ProductComponent.objects.filter(composite_product=product)
if components.exists():
for component in components:
movi = StockMovement.objects.create(
product=component.component_product ,
related_comanda=comanda,
user=user,
movement_type=movement_type,
quantity=component.quantity_required,
observation=obs
)
movi.save()
component.component_product.quantity += component.quantity_required
component.component_product.save()
movi = StockMovement.objects.create(
product=product ,
related_comanda=comanda,
user=user,
movement_type=movement_type,
quantity=qtd,
observation=obs
)
movi.save()
product.quantity += qtd
product.save()
class Meta:
ordering = ['-movement_date']

View File

@@ -50,7 +50,7 @@ Detalhes {{comanda.name}}
<input hidden id="id-temp" type="number">
<div class="grid-container" >
<div style="display: flex;padding: 5px;gap:8px">
<button class="btn-primary" id="openModal" onclick="openModal()"
<button class="btn-primary" id="openModal" onclick="modalAddProduct()"
{% if comanda.status != 'OPEN'%}
disabled
{% endif %}

View File

@@ -3,15 +3,16 @@ from django.urls import reverse
from django.utils import timezone
from django.http import JsonResponse, HttpResponseRedirect
from django.contrib.auth.models import User
from django.shortcuts import render, redirect
from django.db.models import Count, F
from comandas.models import Comanda, ProductComanda
from comandas.models import Comanda, ProductComanda, StockMovement, StockMovementType
from clients.models import Client
from payments.models import Payments, somar
from orders.models import Order
from products.models import Product
from products.models import Product, ProductComponent
from mesas.models import Mesa
from gestaoRaul.decorators import group_required
@@ -141,15 +142,27 @@ def closeComanda(request, comanda_id):
@group_required(groupName='Garçom')
def addProduct(request, product_id, comanda_id):
print('chamouuuuuuuuuuu')
config = {
'taxa': False
}
obs = request.GET.get("obs")
product_comanda = ProductComanda(comanda_id=comanda_id, product_id=product_id)
product_comanda.save()
product = Product.objects.get(id=product_id)
comanda = Comanda.objects.get(id=comanda_id)
user = User.objects.get(id=request.user.id)
typeMovement = StockMovementType.objects.get(name="Venda - Comanda")
StockMovement.subTransactionStock(
product=product,
movement_type=typeMovement,
comanda=comanda,
user=user,
qtd=1,
obs= "Adicionado na comanda"
)
parcial = Payments.objects.filter(comanda=comanda)
if product.cuisine == True:
order = Order(id_comanda=comanda, id_product=product, productComanda=product_comanda, obs='')

Binary file not shown.

View File

@@ -106,12 +106,12 @@ WSGI_APPLICATION = 'gestaoRaul.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
#DATABASES = {
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': BASE_DIR / 'db.sqlite3',
# }
#}
# }
DATABASES = {

View File

@@ -1,6 +1,11 @@
from django.contrib import admin
from products.models import Product
from products.models import Product, ProductComponent, UnitOfMeasure
admin.site.register(Product)
admin.site.register(ProductComponent)
admin.site.register(UnitOfMeasure)

View File

@@ -0,0 +1,44 @@
# Generated by Django 5.1.4 on 2025-07-22 18:36
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0003_product_cuisine'),
]
operations = [
migrations.CreateModel(
name='UnitOfMeasure',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(help_text="Ex: 'Unidade', 'Kg', 'Litro'", max_length=50, unique=True)),
('abbreviation', models.CharField(help_text="Ex: 'un', 'kg', 'L'", max_length=10, unique=True)),
],
),
migrations.CreateModel(
name='ProductComponent',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity_required', models.PositiveIntegerField(default=1, help_text='Quantidade deste componente necessária para o produto composto.')),
('component_product', models.ForeignKey(help_text='Um produto que é componente de outro produto.', on_delete=django.db.models.deletion.CASCADE, related_name='used_as_component_in', to='products.product')),
('composite_product', models.ForeignKey(help_text='O produto que é composto por outros produtos.', on_delete=django.db.models.deletion.CASCADE, related_name='composition_entries', to='products.product')),
],
options={
'unique_together': {('composite_product', 'component_product')},
},
),
migrations.AddField(
model_name='product',
name='components',
field=models.ManyToManyField(related_name='is_component_of', through='products.ProductComponent', to='products.product'),
),
migrations.AddField(
model_name='product',
name='unit_of_measure',
field=models.ForeignKey(blank=True, help_text='Unidade de medida para este produto.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='products.unitofmeasure'),
),
]

View File

@@ -1,8 +1,19 @@
from django.db import models
from django.contrib.auth.models import User
from categories.models import Categories
class UnitOfMeasure(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=50, unique=True, help_text="Ex: 'Unidade', 'Kg', 'Litro'")
abbreviation = models.CharField(max_length=10, unique=True, help_text="Ex: 'un', 'kg', 'L'")
def __str__(self):
return self.abbreviation
# Create your models here.
class Product(models.Model):
id = models.AutoField(primary_key=True)
@@ -14,6 +25,56 @@ class Product(models.Model):
category = models.ForeignKey(Categories, on_delete=models.CASCADE)
cuisine = models.BooleanField(default=False)
active = models.BooleanField(default=True)
unit_of_measure = models.ForeignKey(
UnitOfMeasure,
on_delete=models.SET_NULL, # Define como NULL se a unidade de medida for excluída
null=True,
blank=True,
help_text="Unidade de medida para este produto."
)
# Campo de composição (mantido da resposta anterior)
components = models.ManyToManyField(
'self',
through='ProductComponent',
through_fields=('composite_product', 'component_product'),
symmetrical=False,
related_name='is_component_of'
)
def __str__(self) -> str:
return f"{self.id} - {self.name} - {self.price} - {self.category} - {self.cuisine} - {self.active} "
return f"{self.name}"
class ProductComponent(models.Model):
composite_product = models.ForeignKey(
Product,
on_delete=models.CASCADE,
related_name='composition_entries',
help_text="O produto que é composto por outros produtos."
)
component_product = models.ForeignKey(
Product,
on_delete=models.CASCADE,
related_name='used_as_component_in',
help_text="Um produto que é componente de outro produto."
)
quantity_required = models.PositiveIntegerField(
default=1,
help_text="Quantidade deste componente necessária para o produto composto."
)
class Meta:
unique_together = ('composite_product', 'component_product')
def __str__(self):
return (
f"{self.composite_product.name} requer "
f"{self.quantity_required} de {self.component_product.name}"
)

View File

@@ -18,73 +18,75 @@ Produtos
<div class="grid-top">
<button class="btn-primary"
onclick="openModal()" id="openModal">Novo Produto</button>
<input type="text" id="search-product" name="search-product" placeholder="Buscar Produto" hx-get="{% url 'searchProduct' %}" hx-trigger="keyup" hx-target="#product-list">
<input onclick="listerSortTeable()" type="text" id="search-product" name="search-product" placeholder="Buscar Produto" hx-get="{% url 'searchProduct' %}" hx-trigger="keyup" hx-target="#product-list">
<a href="https://raulrockbar.blogspot.com/p/cardapio.html" target="_blank">Cardápio Digital</a>
</div>
<table id="product-list">
<tr>
<th style="text-align: left;">Produto</th>
<th style="text-align: left;width: 20%;">Preço</th>
<th class="hide-on-mobile" style="text-align: left;">Quantidade</th>
<th class="hide-on-mobile" style="text-align: left;">Categoria</th>
<th style="text-align: left;width: 20%;">Ações</th>
</tr>
{% for product in products %}
<table id="product-list">
<thead> <tr>
<th style="text-align: left;" data-col-type="text">Produto</th>
<th style="text-align: left;width: 20%;" data-col-type="number">Preço</th>
<th class="hide-on-mobile" style="text-align: left;" data-col-type="number">Quantidade</th>
<th class="hide-on-mobile" style="text-align: left;" data-col-type="text">Categoria</th>
<th style="text-align: left;width: 20%;">Ações</th> </tr>
</thead>
<tbody> {% for product in products %}
<tr>
<td id="name-{{product.id}}" >{{product.name}}</td>
<td id="price-{{product.id}}" >R$ {{product.price}}</td>
<tr>
<td id="name-{{product.id}}" >{{product.name}}</td>
<td id="price-{{product.id}}" >R$ {{product.price}}</td>
<td class="hide-on-mobile" id="quantity-{{product.id}}" >{{product.quantity}}</td>
<td hidden class="hide-on-mobile" id="image-{{product.id}}" >{{product.image}}</td>
<td class="hide-on-mobile" id="category-{{product.id}}" >{{product.category.name}}</td>
<td >
<div class="grid-buttons">
<img
src="{% static 'midia/icons/edit.svg' %}"
style=" width: 35px; height: 35px; cursor: pointer;"
onclick="editProduct({{product.id}})" >
</img>
{% if product.quantity > 20 %}
<td class="hide-on-mobile" id="quantity-{{product.id}}" >{{product.quantity}}</td>
{% else %}
<td class="hide-on-mobile" id="quantity-{{product.id}}" style="background-color: brown;" >{{product.quantity}}</td>
{% endif %}
<td hidden class="hide-on-mobile" id="image-{{product.id}}" >{{product.image}}</td>
<td class="hide-on-mobile" id="category-{{product.id}}" >{{product.category.name}}</td>
<td>
<div class="grid-buttons">
<img
src="{% static 'midia/icons/edit.svg' %}"
style="width: 35px; height: 35px; cursor: pointer;"
onclick="editProduct({{product.id}})"
>
</img>
<input type="hidden" id="h-category-{{product.id}}" value="{{ product.category.id }}">
<input type="hidden" id="description-{{product.id}}" value="{{ product.description }}">
<input type="hidden" id="cuisine-{{product.id}}" value="{{ product.cuisine }}">
<form hx-post="{% url 'onOffproduct' %}" hx-trigger="click" hx-target="#product-list">
<form hx-post="{% url 'onOffproduct' %}" hx-trigger="click" hx-target="#product-list">
{% csrf_token %}
<input type="hidden" name="id-product" id="id-{{product.id}}" value="{{ product.id }}">
<input type="hidden" name="id-product" id="id-{{product.id}}" value="{{ product.id }}">
{% if product.active == True %}
<button style="background-color: rgba(255, 0, 0, 0); padding: 0px;border: 0px;">
<span data-tooltip="Ativar ou Desativar Produto" data-flow="top">
<img
src="{% static 'midia/icons/toggle-on.svg' %}"
style=" width: 35px; height: 35px; cursor: pointer;" >
</img>
</span>
</button>
{% else %}
<button style="background-color: rgba(0, 128, 0, 0); padding: 0px;border: 0px;" >
<span data-tooltip="Ativar ou Desativar Produto" data-flow="top">
<img
src="{% static 'midia/icons/toggle-off.svg' %}"
style=" width: 35px; height: 35px; cursor: pointer;" >
</img>
<span>
</button>
{% endif %}
</form>
</div>
</td>
</tr>
{% endfor %}
</table>
{% if product.active == True %}
<button style="background-color: rgba(255, 0, 0, 0); padding: 0px;border: 0px;">
<span data-tooltip="Ativar ou Desativar Produto" data-flow="top">
<img
src="{% static 'midia/icons/toggle-on.svg' %}"
style="width: 35px; height: 35px; cursor: pointer;"
>
</img>
</span>
</button>
{% else %}
<button style="background-color: rgba(0, 128, 0, 0); padding: 0px;border: 0px;" >
<span data-tooltip="Ativar ou Desativar Produto" data-flow="top">
<img
src="{% static 'midia/icons/toggle-off.svg' %}"
style="width: 35px; height: 35px; cursor: pointer;"
>
</img>
<span>
</button>
{% endif %}
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</body>

View File

@@ -1,9 +1,11 @@
from django.shortcuts import render, redirect
from django.http import HttpResponse
import json
from django.db.models import Q
from django.db.models import Q, Count, F
from categories.models import Categories
from comandas.models import ProductComanda
from products.models import Product
from gestaoRaul.decorators import group_required
@@ -65,6 +67,10 @@ def editProduct(request, productId):
return render(request, "htmx_components/products/htmx_search_products.html", {"products": products})
def createJson(request):
produtos_mais_vendidos = list(ProductComanda.objects.values('product').annotate(
quantidade=Count('product'),
nome=F('product__name') ).order_by('-quantidade'))
products = Product.objects.filter(active=True).exclude(
category__name__in=['Insumos',
'Adicional',
@@ -76,8 +82,21 @@ def createJson(request):
'Cigarros',
'Outros']
)
products_ordenados = []
for produto in produtos_mais_vendidos:
for p in products:
if p.name == produto['nome']:
products_ordenados.append(p)
product_list = []
for product in products:
for product in products_ordenados:
product_data = {
"id": product.id,
"name": product.name,

View File

@@ -1,64 +1,160 @@
{% load static %}
<tr>
<th style="text-align: left;">Produto</th>
<th style="text-align: left;width: 20%;">Preço</th>
<th class="hide-on-mobile" style="text-align: left;">Quantidade</th>
<th class="hide-on-mobile" style="text-align: left;">Categoria</th>
<th style="text-align: left;width: 20%;">Ações</th>
</tr>
<thead> <tr>
<th style="text-align: left;" data-col-type="text">Produto</th>
<th style="text-align: left;width: 20%;" data-col-type="number">Preço</th>
<th class="hide-on-mobile" style="text-align: left;" data-col-type="number">Quantidade</th>
<th class="hide-on-mobile" style="text-align: left;" data-col-type="text">Categoria</th>
<th style="text-align: left;width: 20%;">Ações</th> </tr>
</thead>
<tbody> {% for product in products %}
<tr>
<td id="name-{{product.id}}" >{{product.name}}</td>
<td id="price-{{product.id}}" >R$ {{product.price}}</td>
{% for product in products %}
{% if product.quantity > 20 %}
<td class="hide-on-mobile" id="quantity-{{product.id}}" >{{product.quantity}}</td>
{% else %}
<td class="hide-on-mobile" id="quantity-{{product.id}}" style="background-color: brown;" >{{product.quantity}}</td>
{% endif %}
<td hidden class="hide-on-mobile" id="image-{{product.id}}" >{{product.image}}</td>
<td class="hide-on-mobile" id="category-{{product.id}}" >{{product.category.name}}</td>
<td>
<div class="grid-buttons">
<img
src="{% static 'midia/icons/edit.svg' %}"
style="width: 35px; height: 35px; cursor: pointer;"
onclick="editProduct({{product.id}})"
>
</img>
<tr>
<td id="name-{{product.id}}" >{{product.name}}</td>
<td id="price-{{product.id}}" >R$ {{product.price}}</td>
<td class="hide-on-mobile" id="quantity-{{product.id}}" >{{product.quantity}}</td>
<td class="hide-on-mobile" id="category-{{product.id}}" >{{product.category.name}}</td>
<td hidden class="hide-on-mobile" id="image-{{product.id}}" >{{product.image}}</td>
<input type="hidden" id="h-category-{{product.id}}" value="{{ product.category.id }}">
<input type="hidden" id="description-{{product.id}}" value="{{ product.description }}">
<input type="hidden" id="cuisine-{{product.id}}" value="{{ product.cuisine }}">
<td>
<div class="grid-buttons">
<img
src="{% static 'midia/icons/edit.svg' %}"
style=" width: 35px; height: 35px; cursor: pointer;"
onclick="editProduct({{product.id}})" >
</img> <input type="hidden" id="h-category-{{product.id}}" value="{{ product.category.id }}">
<input type="hidden" id="description-{{product.id}}" value="{{ product.description }}">
<input type="hidden" id="cuisine-{{product.id}}" value="{{ product.cuisine }}">
<form hx-post="{% url 'onOffproduct' %}" hx-trigger="click" hx-target="#product-list">
{% csrf_token %}
<input type="hidden" name="id-product" id="id-{{product.id}}" value="{{ product.id }}">
{% if product.active == True %}
<button style="background-color: rgba(255, 0, 0, 0); padding: 0px;border: 0px;">
<span data-tooltip="Ativar ou Desativar Produto" data-flow="top">
<img
src="{% static 'midia/icons/toggle-on.svg' %}"
style="width: 35px; height: 35px; cursor: pointer;"
>
</img>
</span>
</button>
{% else %}
<button style="background-color: rgba(0, 128, 0, 0); padding: 0px;border: 0px;" >
<span data-tooltip="Ativar ou Desativar Produto" data-flow="top">
<img
src="{% static 'midia/icons/toggle-off.svg' %}"
style="width: 35px; height: 35px; cursor: pointer;"
>
</img>
<span>
</button>
{% endif %}
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<script>
function listerSortTeable(){
<form hx-post="{% url 'onOffproduct' %}" hx-trigger="click" hx-target="#product-list">
{% csrf_token %}
<input type="hidden" name="id-product" id="id-{{product.id}}" value="{{ product.id }}">
// document.addEventListener('DOMContentLoaded', function() {
const table = document.getElementById('product-list');
const headers = table.querySelectorAll('th');
const tbody = table.querySelector('tbody'); // Seleciona o corpo da tabela
{% if product.active == True %}
<button style="background-color: rgba(255, 0, 0, 0); padding: 0px;border: 0px;">
<span data-tooltip="Ativar ou Desativar Produto" data-flow="top">
<img
src="{% static 'midia/icons/toggle-on.svg' %}"
style=" width: 35px; height: 35px; cursor: pointer;" >
</img>
</span>
</button>
{% else %}
<button style="background-color: rgba(0, 128, 0, 0); padding: 0px;border: 0px;" >
<span data-tooltip="Ativar ou Desativar Produto" data-flow="top">
<img
src="{% static 'midia/icons/toggle-off.svg' %}"
style=" width: 35px; height: 35px; cursor: pointer;" >
</img>
<span>
</button>
{% endif %}
let currentSortColumn = -1; // Armazena o índice da coluna atualmente ordenada
let sortDirection = 'asc'; // 'asc' para ascendente, 'desc' para descendente
</form>
</div>
</td>
</tr>
// Adiciona um ouvinte de evento de clique a cada cabeçalho de coluna
headers.forEach((header, index) => {
if (header.textContent.trim() !== 'Ações') {
header.addEventListener('click', () => {
// Se a mesma coluna for clicada, inverte a direção da ordenação
if (currentSortColumn === index) {
sortDirection = (sortDirection === 'asc') ? 'desc' : 'asc';
} else {
// Se uma nova coluna for clicada, define como a coluna atual
// e inicia a ordenação ascendente
currentSortColumn = index;
sortDirection = 'asc';
}
// Remove as classes de ordenação de todos os cabeçalhos
headers.forEach(h => {
h.classList.remove('sorted-asc', 'sorted-desc');
});
{% endfor %}
// Adiciona a classe de ordenação ao cabeçalho clicado para visualização
header.classList.add(`sorted-${sortDirection}`);
</table>
// Chama a função de ordenação
sortColumn(index, sortDirection, header.dataset.colType);
});
}
})
function sortColumn(columnIndex, direction, columnType) {
// Converte os NodeList de linhas em um Array para poder usar sort()
const rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort((rowA, rowB) => {
// Obtém o texto da célula na coluna clicada para ambas as linhas
let valueA = rowA.children[columnIndex].textContent.trim();
let valueB = rowB.children[columnIndex].textContent.trim();
// Trata valores específicos para colunas como "Preço" que têm "R$"
if (columnIndex === 1) { // Índice da coluna "Preço"
valueA = valueA.replace('R$', '').trim();
valueB = valueB.replace('R$', '').trim();
}
// Converte para número se o tipo da coluna for "number"
// ou se for a coluna de "Preço" após remover "R$"
if (columnType === 'number' || columnIndex === 1) {
valueA = parseFloat(valueA);
valueB = parseFloat(valueB);
// Garante que NaN (Not a Number) sejam tratados para evitar problemas de ordenação
valueA = isNaN(valueA) ? -Infinity : valueA;
valueB = isNaN(valueB) ? -Infinity : valueB;
} else {
// Para texto, use localeCompare para ordenação correta com caracteres acentuados
// e torna minúsculo para ordenação case-insensitive
valueA = valueA.toLowerCase();
valueB = valueB.toLowerCase();
}
let comparison = 0;
if (valueA > valueB) {
comparison = 1;
} else if (valueA < valueB) {
comparison = -1;
}
// Aplica a direção da ordenação
return (direction === 'asc') ? comparison : -comparison;
});
// Remove todas as linhas existentes e adiciona as linhas ordenadas de volta ao tbody
while (tbody.firstChild) {
tbody.removeChild(tbody.firstChild);
}
rows.forEach(row => tbody.appendChild(row));
}
// });
}
listerSortTeable()
</script>

View File

@@ -330,9 +330,9 @@ white-space: nowrap;
.toast {
position: fixed;
top: 40px;
top: 4px;
width: 30%;
left: 50%;
left: 30%;
justify-items: center;
align-items: center;
transform: translateX(-50%);
@@ -344,7 +344,7 @@ white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: opacity 0.5s ease-in-out, visibility 0.5s ease-in-out;
z-index: 999 ;
z-index: 999999999999 !important;
}
.toast p {
@@ -462,4 +462,11 @@ white-space: nowrap;
body {
margin: 8px 0px 0px 0px;
}
.toast {
position: fixed;
top: 5px;
width: 70%;
left: 40%;
}
}

View File

@@ -19,115 +19,86 @@ document.addEventListener('DOMContentLoaded', function() {
function verificarCookieNotificacao() {
var iconNotify = document.getElementById('icon-notify');
if (document.cookie.indexOf('notificacao=') === -1) {
document.cookie = 'notificacao=true; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/';
iconNotify.style.backgroundColor = 'green';
iconNotify.textContent = '🔊';
}else{
let valorAtual = document.cookie.replace(/(?:(?:^|.*;\s*)notificacao\s*\=\s*([^;]*).*$)|^.*$/, "$1");
iconNotify.style.backgroundColor = valorAtual === 'true' ? 'green' : 'red';
iconNotify.textContent = valorAtual === 'true' ? '🔊' : '🔇';
}
}
verificarCookieNotificacao();
function cookieNotificacao() {
if (document.cookie.indexOf('notificacao=') !== -1) {
let valorAtual = document.cookie.replace(/(?:(?:^|.*;\s*)notificacao\s*\=\s*([^;]*).*$)|^.*$/, "$1");
var iconNotify = document.getElementById('icon-notify');
let novoValor = valorAtual === 'true' ? 'false' : 'true';
if (novoValor === 'true') {
iconNotify.style.backgroundColor = 'green';
iconNotify.textContent = '🔊';
}else{
iconNotify.style.backgroundColor = 'red';
iconNotify.textContent = '🔇';
}
document.cookie = 'notificacao=' + novoValor + '; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/';
} else {
document.cookie = 'notificacao=true; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/';
}
}
const websocket = new WebSocket('ws://192.168.0.150:8765');
const nomeUsuario = document.getElementById('user-info').textContent;
websocket.addEventListener('open', (event) => {
console.log('Conectado ao servidor WebSocket');
});
websocket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
switch (data.local) {
case 'cozinha':
if (document.getElementById('Fila') !== null && data.tipo === 'add'){
var novoElemento = document.createElement('div');
novoElemento.innerHTML = data.message;
document.getElementById('Fila').appendChild(novoElemento);
let valorAtual = document.cookie.replace(/(?:(?:^|.*;\s*)notificacao\s*\=\s*([^;]*).*$)|^.*$/, "$1");
if (valorAtual === 'true') {
texto = new SpeechSynthesisUtterance(data.speak);
window.speechSynthesis.speak(texto);
// setTimeout(function() {
// location.reload();
// }, 6000);
}
}
else if (document.getElementById('obs-'+data.id) !== null && data.tipo === 'edit'){
const obs = document.getElementById('obs-'+data.id)
const card = obs.parentNode;
card.style.backgroundColor = 'rgb(243, 165, 75)';
obs.innerHTML = data.message;
let valorAtual = document.cookie.replace(/(?:(?:^|.*;\s*)notificacao\s*\=\s*([^;]*).*$)|^.*$/, "$1");
if (valorAtual === 'true') {
texto = new SpeechSynthesisUtterance(data.speak);
window.speechSynthesis.speak(texto);
// setTimeout(function() {
// location.reload();
// }, 6000);
}
}
else if (document.getElementById('m-card-'+data.id) !== null && data.tipo === 'delete'){
// const card = document.getElementById('m-card-'+data.id)
// card.style.backgroundColor = 'rgb(253, 69, 69)';
let valorAtual = document.cookie.replace(/(?:(?:^|.*;\s*)notificacao\s*\=\s*([^;]*).*$)|^.*$/, "$1");
if (valorAtual === 'true') {
texto = new SpeechSynthesisUtterance(data.speak);
window.speechSynthesis.speak(texto);
setTimeout(function() {
location.reload();
}, 6000);
}
}
// const websocket = new WebSocket('ws://192.168.0.150:8765');
// const nomeUsuario = document.getElementById('user-info').textContent;
// websocket.addEventListener('open', (event) => {
// console.log('Conectado ao servidor WebSocket');
// });
// websocket.addEventListener('message', (event) => {
// const data = JSON.parse(event.data);
// switch (data.local) {
// case 'cozinha':
// if (document.getElementById('Fila') !== null && data.tipo === 'add'){
// var novoElemento = document.createElement('div');
// novoElemento.innerHTML = data.message;
// document.getElementById('Fila').appendChild(novoElemento);
// let valorAtual = document.cookie.replace(/(?:(?:^|.*;\s*)notificacao\s*\=\s*([^;]*).*$)|^.*$/, "$1");
// if (valorAtual === 'true') {
// texto = new SpeechSynthesisUtterance(data.speak);
// window.speechSynthesis.speak(texto);
// // setTimeout(function() {
// // location.reload();
// // }, 6000);
// }
// }
// else if (document.getElementById('obs-'+data.id) !== null && data.tipo === 'edit'){
// const obs = document.getElementById('obs-'+data.id)
// const card = obs.parentNode;
// card.style.backgroundColor = 'rgb(243, 165, 75)';
// obs.innerHTML = data.message;
// let valorAtual = document.cookie.replace(/(?:(?:^|.*;\s*)notificacao\s*\=\s*([^;]*).*$)|^.*$/, "$1");
// if (valorAtual === 'true') {
// texto = new SpeechSynthesisUtterance(data.speak);
// window.speechSynthesis.speak(texto);
// // setTimeout(function() {
// // location.reload();
// // }, 6000);
// }
// }
// else if (document.getElementById('m-card-'+data.id) !== null && data.tipo === 'delete'){
// // const card = document.getElementById('m-card-'+data.id)
// // card.style.backgroundColor = 'rgb(253, 69, 69)';
// let valorAtual = document.cookie.replace(/(?:(?:^|.*;\s*)notificacao\s*\=\s*([^;]*).*$)|^.*$/, "$1");
// if (valorAtual === 'true') {
// texto = new SpeechSynthesisUtterance(data.speak);
// window.speechSynthesis.speak(texto);
// setTimeout(function() {
// location.reload();
// }, 6000);
// }
// }
break;
case 'praca':
console.log('Mensagem para a praca:', data);
break;
case 'guarita':
console.log('Mensagem para a guarita:', data);
break;
case 'balcao':
console.log('Mensagem para a balcao:', data);
break;
default:
console.log('Local desconhecido:', data);
}
// break;
// case 'praca':
// console.log('Mensagem para a praca:', data);
// break;
// case 'guarita':
// console.log('Mensagem para a guarita:', data);
// break;
// case 'balcao':
// console.log('Mensagem para a balcao:', data);
// break;
// default:
// console.log('Local desconhecido:', data);
// }
});
websocket.addEventListener('error', (event) => {
console.error('Erro no WebSocket:', event);
});
websocket.addEventListener('close', (event) => {
console.log("conexão fechada");
});
// });
// websocket.addEventListener('error', (event) => {
// console.error('Erro no WebSocket:', event);
// });
// websocket.addEventListener('close', (event) => {
// console.log("conexão fechada");
// });
@@ -154,7 +125,7 @@ function openFullscreen() {
}
}
function showToast(message, type ,duration = 3000) {
function showToast(message, type ,duration = 2500) {
const toast = document.getElementById('toast');
if (type === 'success') {

View File

@@ -1,5 +1,5 @@
async function openModal() {
async function modalAddProduct() {
var htmlModal = document.getElementById('addProduct').innerHTML
htmlModal = htmlModal.replace('search-product','search-product-modal')
htmlModal = htmlModal.replace('product-list','product-list-modal')
@@ -364,15 +364,7 @@ async function addProductComanda(productId, comandaId, cuisine) {
throw new Error('Token de segurança não encontrado');
}
// if (cuisine === 'ggg') {
// openModalObs();
// return;
// }
// Mostra estado de carregamento
Swal.update({
title: '<span style="color: white;">Adicionando produto...</span>',
});
// Requisição POST
const response = await fetch(`/comandas/product=${productId}/comanda=${comandaId}/`, {
@@ -403,6 +395,8 @@ async function addProductComanda(productId, comandaId, cuisine) {
}
const result = await response.text();
console.log(response);
showToast('Produto adicionado com sucesso!', 'success');
// Atualiza a lista de produtos
const listElement = document.getElementById("list-products-comanda");
@@ -410,17 +404,11 @@ async function addProductComanda(productId, comandaId, cuisine) {
listElement.innerHTML = result;
}
// if (cuisine === 'True') {
// openModalObs();
// return;
// }
Swal.update({
title: '<span style="color: green;">✅ Produto adicionado!</span>',
});
setTimeout(() => {
Swal.update({
title: '<span style="color: white;">Adicionar Produto</span>'
});
}, 2500);
} catch (error) {
console.error('Erro:', error);

View File

@@ -50,6 +50,39 @@
background-color: rgba(0, 0, 0, 0.6);
}
th {
background-color: #170e4f;
cursor: pointer;
position: relative;
}
th.sorted-asc::after {
content: " ▲";
font-size: 0.8em;
vertical-align: super;
}
th.sorted-desc::after {
content: " ▼";
font-size: 0.8em;
vertical-align: super;
}
#product-list {
width: 100%;
border-collapse: collapse;
}
#product-list th,
#product-list td {
padding: 8px;
text-align: left;
}
#product-list th {
position: sticky;
top: 60px;
z-index: 10;
}
@media (max-width: 768px) {
.hide-on-mobile {
@@ -62,4 +95,9 @@
max-width: 100px;
width: 80px;
}
#product-list th {
position: sticky;
top: 0px;
z-index: 10;
}
}

View File

@@ -81,16 +81,96 @@ function editProduct(id) {
// })
}
// document.getElementById('openModal').addEventListener('click', openModal);
// document.getElementById('productForm').addEventListener('submit', function(event) {
// event.preventDefault();
function listerSortTeable(){
// const productName = document.getElementById('productName').value;
// const productPrice = document.getElementById('productPrice').value;
// const productDescription = document.getElementById('productDescription').value;
// closeModal();
// }
// );
// document.addEventListener('DOMContentLoaded', function() {
const table = document.getElementById('product-list');
const headers = table.querySelectorAll('th');
const tbody = table.querySelector('tbody'); // Seleciona o corpo da tabela
let currentSortColumn = -1; // Armazena o índice da coluna atualmente ordenada
let sortDirection = 'asc'; // 'asc' para ascendente, 'desc' para descendente
// Adiciona um ouvinte de evento de clique a cada cabeçalho de coluna
headers.forEach((header, index) => {
if (header.textContent.trim() !== 'Ações') {
header.addEventListener('click', () => {
// Se a mesma coluna for clicada, inverte a direção da ordenação
if (currentSortColumn === index) {
sortDirection = (sortDirection === 'asc') ? 'desc' : 'asc';
} else {
// Se uma nova coluna for clicada, define como a coluna atual
// e inicia a ordenação ascendente
currentSortColumn = index;
sortDirection = 'asc';
}
// Remove as classes de ordenação de todos os cabeçalhos
headers.forEach(h => {
h.classList.remove('sorted-asc', 'sorted-desc');
});
// Adiciona a classe de ordenação ao cabeçalho clicado para visualização
header.classList.add(`sorted-${sortDirection}`);
// Chama a função de ordenação
sortColumn(index, sortDirection, header.dataset.colType);
});
}
})
function sortColumn(columnIndex, direction, columnType) {
// Converte os NodeList de linhas em um Array para poder usar sort()
const rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort((rowA, rowB) => {
// Obtém o texto da célula na coluna clicada para ambas as linhas
let valueA = rowA.children[columnIndex].textContent.trim();
let valueB = rowB.children[columnIndex].textContent.trim();
// Trata valores específicos para colunas como "Preço" que têm "R$"
if (columnIndex === 1) { // Índice da coluna "Preço"
valueA = valueA.replace('R$', '').trim();
valueB = valueB.replace('R$', '').trim();
}
// Converte para número se o tipo da coluna for "number"
// ou se for a coluna de "Preço" após remover "R$"
if (columnType === 'number' || columnIndex === 1) {
valueA = parseFloat(valueA);
valueB = parseFloat(valueB);
// Garante que NaN (Not a Number) sejam tratados para evitar problemas de ordenação
valueA = isNaN(valueA) ? -Infinity : valueA;
valueB = isNaN(valueB) ? -Infinity : valueB;
} else {
// Para texto, use localeCompare para ordenação correta com caracteres acentuados
// e torna minúsculo para ordenação case-insensitive
valueA = valueA.toLowerCase();
valueB = valueB.toLowerCase();
}
let comparison = 0;
if (valueA > valueB) {
comparison = 1;
} else if (valueA < valueB) {
comparison = -1;
}
// Aplica a direção da ordenação
return (direction === 'asc') ? comparison : -comparison;
});
// Remove todas as linhas existentes e adiciona as linhas ordenadas de volta ao tbody
while (tbody.firstChild) {
tbody.removeChild(tbody.firstChild);
}
rows.forEach(row => tbody.appendChild(row));
}
// });
}
listerSortTeable();