From 97a175ba9756e17b1655e195f48c154ad2a20702 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sun, 26 Apr 2026 16:42:14 +0000 Subject: [PATCH] Restaurnate_v1 --- config/__pycache__/settings.cpython-311.pyc | Bin 5552 -> 6531 bytes config/settings.py | 71 +-- core/__pycache__/admin.cpython-311.pyc | Bin 212 -> 2322 bytes core/__pycache__/api.cpython-311.pyc | Bin 0 -> 1241 bytes core/__pycache__/forms.cpython-311.pyc | Bin 0 -> 3899 bytes core/__pycache__/models.cpython-311.pyc | Bin 209 -> 12652 bytes core/__pycache__/tests.cpython-311.pyc | Bin 0 -> 7713 bytes core/__pycache__/urls.cpython-311.pyc | Bin 347 -> 1644 bytes core/__pycache__/views.cpython-311.pyc | Bin 1364 -> 11183 bytes core/admin.py | 36 +- core/api.py | 22 + core/forms.py | 46 ++ core/migrations/0001_initial.py | 78 +++ core/migrations/0002_seed_demo_pos.py | 60 ++ core/migrations/0003_userrole.py | 30 + .../__pycache__/0001_initial.cpython-311.pyc | Bin 0 -> 4077 bytes .../0002_seed_demo_pos.cpython-311.pyc | Bin 0 -> 3100 bytes .../__pycache__/0003_userrole.cpython-311.pyc | Bin 0 -> 1766 bytes core/models.py | 163 ++++- core/templates/base.html | 71 ++- core/templates/core/index.html | 321 ++++++---- core/templates/core/kitchen.html | 52 ++ core/templates/core/order_detail.html | 91 +++ core/templates/core/table_detail.html | 88 +++ core/templates/registration/login.html | 42 ++ core/tests.py | 81 ++- core/urls.py | 22 +- core/views.py | 171 ++++- requirements.txt | 2 + static/css/custom.css | 599 +++++++++++++++++- 30 files changed, 1833 insertions(+), 213 deletions(-) create mode 100644 core/__pycache__/api.cpython-311.pyc create mode 100644 core/__pycache__/forms.cpython-311.pyc create mode 100644 core/__pycache__/tests.cpython-311.pyc create mode 100644 core/api.py create mode 100644 core/forms.py create mode 100644 core/migrations/0001_initial.py create mode 100644 core/migrations/0002_seed_demo_pos.py create mode 100644 core/migrations/0003_userrole.py create mode 100644 core/migrations/__pycache__/0001_initial.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0002_seed_demo_pos.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0003_userrole.cpython-311.pyc create mode 100644 core/templates/core/kitchen.html create mode 100644 core/templates/core/order_detail.html create mode 100644 core/templates/core/table_detail.html create mode 100644 core/templates/registration/login.html diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index d79d6a79890a58443ee5f65d8d558823a19e9aed..c4654772899a324799331b495c5b8dcffc3e6d2f 100644 GIT binary patch delta 2713 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 diff --git a/core/__pycache__/api.cpython-311.pyc b/core/__pycache__/api.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f4e7e525a064538ed392aa2fe13758de4833754 GIT binary patch literal 1241 zcmah|&1=*^6ragv)9gpBZnt!IS!h*kU0fSHw1*;!f`}kJ_<^t#Lb{pStjT7bNm>OD zJqY&bq2OilSS#%xvkN)IfkMGkFS4b2>B%=~w>C?`Ve<0c@4dWv?>8^shKJJ#)~EZw ztRE6WKZVg>@~LvP0m>I-A`{!l!8PniH3HQEsW3(JSPiKiO7SfIrI^2LBLKwY{06eO!Yacqz;6?B5~DdS^BC))_AT(sADl^ zxlO%p8;lWF!F=Qid*FL3r0H38!y_g@>NCPxZj+G%QHuaxGD4rZHsOiKk6zTzDgKi%y~7bL~~4FT0e(5s*x%GzU(& z$stPK`h=j?6qL>H=Zc%vO)V@;w6j6sb~skvTnxsh!_w7uH7MN)i<9kg zP}KJFSHd7vNloM>}}*v);+y=;EmWPxYiM27*8wCXh4@hR&VJEbb`9L%WsJH zVYY`jBTF07eIDxY1Su!&8z-!SR$%0CO9DoUJawe#K0V2S2K>l7=7P>P>WAO69EIxU z&LFB|q+ph9Y8t7|%80SB9KGVCoIoT$={1U8u$)xIT zM`gGAfNa6%WM?G1XqL3BGv}0TC#zl43XW~kEPQd*s=#iu1(Rx7W;&yl>lFFG01w0Z zCBSO?*GL?zIk!G)92(dyx!H^E`$hMqtC$l;Jz;nWqbUl#(k21eMp&I{%B1&&;KOe< z4)r$@Lp8@s47uNX*PW}n=@rb08}-BuFL9$84&lcD+X$;u_@mFa7S&~>EkoRT4Z1(> zGE<-*2jt=R8J`bl=Sj)tdmtw&|IC90B;pXQj;@8Dj+$l4jY){GMs2G^f#Bq9BojOG>{#L<||9m63* zs6sjfMV!#O575Dh-lH|K@!Tnw<(l+(;KeoPcJQeZ5-qG4>!!WVIcXOdiihDsrINBD?LkJF%8tRe} zJ7o5P1ObbermsN2i|JNg`9RU({0xo02xBl?rN^O^mcjKp6$pCZa`m!q=kpYK=!ea@ zBA7cI`@C)d5h$82(nF)yEzS1BD6A_6c!&_NVcriZbOEHVF0pqcpJzcMpSN(^sZ?r} zqPfu10Lw=Y@US&G+L84Wfd8zLhMfGB_(}Ly*pri=9(U#R`Y=F{|DJheBl}fm{L9Sv zBh}4}*E6$TX0{f()w6cNlaG1wcN&?OS@msCegUd3%k0e4(td8B5wMl{@!fS7G{eZ} zA8>RG7lfxg9;`owq6q!)SU(4Vm9~COJ|=>R9fVD6+urLK6vUHF^8Z0#rzx1J%k#FZ zX$#lT?cY1EA?)hVwx@3E-Qc<{FDKz8=umx6Z>L2U7Q5%G?5ZlXSPOgsRAt zHUm|@gDEZj!F3h(Crr1Oj}sx)@PM~+0C&g&sgj?aSmCN%rz@~KmQ`$*_onMsZk5}c ze4hUee=9vpm@h>Jsd~YtIH@lM8ur75MN=qBsGIPr8k?n`P>DKD5o^gkL@M%h0BeQs2NKS!f@aj_^e#FyBO!D7bA?hq_I zusg6Hxrnz3bOy{L`XN?in;e91gIUq*@5twZOZ*tU0#$SnmIu%jIF4(Oq}!bvT1h;o+6Pvg9`jUMdc;15m*OHGmv3+~5Gb3@J?1QkT-7aoo`}R{h H*{=T!0Z6Td literal 0 HcmV?d00001 diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index a251b5fb7c2b603c587f8162d643eb10596d2858..5673bb11efab34133651577e7d573e4c86b486ab 100644 GIT binary patch literal 12652 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 delta 148 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 diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2f0989c7137f7930a63ea76fcd63516c4974e834..049a46fa0e513417afcb359010e27904d15b2e86 100644 GIT binary patch literal 11183 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 Xr9HlS=yH2}4P;gNa)91vejfh;`i@)& diff --git a/core/admin.py b/core/admin.py index 8c38f3f..b939f08 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,37 @@ from django.contrib import admin -# Register your models here. +from .models import Order, OrderItem, Product, Table, UserRole + + +class OrderItemInline(admin.TabularInline): + model = OrderItem + extra = 0 + + +@admin.register(UserRole) +class UserRoleAdmin(admin.ModelAdmin): + list_display = ("user", "role", "updated_at") + list_filter = ("role",) + search_fields = ("user__username", "user__email", "user__first_name", "user__last_name") + + +@admin.register(Table) +class TableAdmin(admin.ModelAdmin): + list_display = ("name", "status", "seats", "area", "updated_at") + list_filter = ("status", "area") + search_fields = ("name",) + + +@admin.register(Product) +class ProductAdmin(admin.ModelAdmin): + list_display = ("name", "category", "price", "station", "is_available") + list_filter = ("category", "station", "is_available") + search_fields = ("name",) + + +@admin.register(Order) +class OrderAdmin(admin.ModelAdmin): + list_display = ("id", "table", "status", "created_at", "paid_at") + list_filter = ("status", "created_at") + search_fields = ("table__name", "guest_name") + inlines = [OrderItemInline] diff --git a/core/api.py b/core/api.py new file mode 100644 index 0000000..6f24dfd --- /dev/null +++ b/core/api.py @@ -0,0 +1,22 @@ +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from .models import UserRole + + +@api_view(["GET"]) +@permission_classes([IsAuthenticated]) +def current_user_api(request): + return Response( + { + "id": request.user.id, + "username": request.user.username, + "email": request.user.email, + "first_name": request.user.first_name, + "last_name": request.user.last_name, + "role": UserRole.resolve_for(request.user), + "role_label": UserRole.label_for(request.user), + "is_superuser": request.user.is_superuser, + } + ) diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..996a71c --- /dev/null +++ b/core/forms.py @@ -0,0 +1,46 @@ +from django import forms +from django.contrib.auth.forms import AuthenticationForm + +from .models import Order, OrderItem, Product + + +class LoginForm(AuthenticationForm): + username = forms.CharField( + label="Usuario", + widget=forms.TextInput(attrs={"class": "form-control form-control-lg", "placeholder": "Tu usuario"}), + ) + password = forms.CharField( + label="Contraseña", + strip=False, + widget=forms.PasswordInput(attrs={"class": "form-control form-control-lg", "placeholder": "••••••••"}), + ) + + +class AddOrderItemForm(forms.ModelForm): + class Meta: + model = OrderItem + fields = ["product", "quantity", "note"] + widgets = { + "product": forms.Select(attrs={"class": "form-select form-select-lg"}), + "quantity": forms.NumberInput(attrs={"class": "form-control form-control-lg", "min": 1}), + "note": forms.Textarea(attrs={"class": "form-control", "rows": 3, "placeholder": "Ej. sin cebolla, extra salsa, término medio"}), + } + labels = { + "product": "Producto", + "quantity": "Cantidad", + "note": "Nota para cocina", + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["product"].queryset = Product.objects.filter(is_available=True) + self.fields["quantity"].initial = 1 + + +class OrderStatusForm(forms.Form): + status = forms.ChoiceField(widget=forms.HiddenInput()) + + def __init__(self, *args, order=None, **kwargs): + super().__init__(*args, **kwargs) + choices = [(status, dict(Order.Status.choices)[status]) for status in (order.allowed_transitions() if order else [])] + self.fields["status"].choices = choices diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..e0f74d4 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,78 @@ +# Generated by Django 5.2.7 on 2026-04-26 16:27 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('open', 'Abierta'), ('preparing', 'En preparación'), ('ready', 'Lista'), ('paid', 'Pagada')], default='open', max_length=20)), + ('guest_name', models.CharField(blank=True, max_length=120)), + ('server_note', models.CharField(blank=True, max_length=200)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('sent_to_kitchen_at', models.DateTimeField(blank=True, null=True)), + ('paid_at', models.DateTimeField(blank=True, null=True)), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=120)), + ('category', models.CharField(max_length=80)), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('is_available', models.BooleanField(default=True)), + ('station', models.CharField(default='Cocina', max_length=80)), + ], + options={ + 'ordering': ['category', 'name'], + }, + ), + migrations.CreateModel( + name='Table', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=40, unique=True)), + ('seats', models.PositiveSmallIntegerField(default=4)), + ('status', models.CharField(choices=[('free', 'Libre'), ('occupied', 'Ocupada')], default='free', max_length=12)), + ('area', models.CharField(blank=True, max_length=50)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='OrderItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(default=1)), + ('note', models.CharField(blank=True, max_length=200)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.order')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='order_items', to='core.product')), + ], + options={ + 'ordering': ['created_at', 'id'], + }, + ), + migrations.AddField( + model_name='order', + name='table', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='orders', to='core.table'), + ), + ] diff --git a/core/migrations/0002_seed_demo_pos.py b/core/migrations/0002_seed_demo_pos.py new file mode 100644 index 0000000..8942e78 --- /dev/null +++ b/core/migrations/0002_seed_demo_pos.py @@ -0,0 +1,60 @@ +from decimal import Decimal + +from django.db import migrations + + +def seed_demo_pos(apps, schema_editor): + Table = apps.get_model("core", "Table") + Product = apps.get_model("core", "Product") + + tables = [ + ("Mesa 1", 2, "Terraza"), + ("Mesa 2", 4, "Salón"), + ("Mesa 3", 4, "Salón"), + ("Mesa 4", 6, "Ventana"), + ("Mesa 5", 2, "Barra"), + ("Mesa 6", 4, "Terraza"), + ("Mesa 7", 4, "Privado"), + ("Mesa 8", 6, "Salón"), + ] + for name, seats, area in tables: + Table.objects.get_or_create(name=name, defaults={"seats": seats, "area": area}) + + products = [ + ("Burger clásica", "Platos", Decimal("12.50"), "Cocina"), + ("Pasta de trufa", "Platos", Decimal("15.00"), "Cocina"), + ("Ensalada César", "Entradas", Decimal("9.50"), "Cocina"), + ("Papas bravas", "Entradas", Decimal("7.00"), "Cocina"), + ("Limonada artesanal", "Bebidas", Decimal("4.50"), "Bar"), + ("Té helado", "Bebidas", Decimal("3.80"), "Bar"), + ] + for name, category, price, station in products: + Product.objects.get_or_create( + name=name, + defaults={"category": category, "price": price, "station": station, "is_available": True}, + ) + + +def remove_demo_pos(apps, schema_editor): + Table = apps.get_model("core", "Table") + Product = apps.get_model("core", "Product") + Table.objects.filter(name__in=[f"Mesa {idx}" for idx in range(1, 9)]).delete() + Product.objects.filter(name__in=[ + "Burger clásica", + "Pasta de trufa", + "Ensalada César", + "Papas bravas", + "Limonada artesanal", + "Té helado", + ]).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0001_initial"), + ] + + operations = [ + migrations.RunPython(seed_demo_pos, remove_demo_pos), + ] diff --git a/core/migrations/0003_userrole.py b/core/migrations/0003_userrole.py new file mode 100644 index 0000000..a425183 --- /dev/null +++ b/core/migrations/0003_userrole.py @@ -0,0 +1,30 @@ +# Generated by Django 5.2.7 on 2026-04-26 16:38 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_seed_demo_pos'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='UserRole', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('role', models.CharField(choices=[('admin', 'Admin'), ('waiter', 'Mesero'), ('kitchen', 'Cocina')], default='waiter', max_length=20)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='role_profile', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Rol de usuario', + 'verbose_name_plural': 'Roles de usuario', + 'ordering': ['user__username'], + }, + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..435975f40d3854792f6c7bb1d59c73b3c5305f2e GIT binary patch literal 4077 zcmbtXO>h&*6`mRW%d(M#v5_nxd+}lf_y;h={O;QF4=ny+!L>k08D1d^croAzPZfLJA*laGfypq5od>a;z9ncCNq7~sZ7q~cu5;^H7 z*+~B%Y@CB^o!{>pUEmSI{&ODGdkBhsb&L8?e@;RJ=;9%DdubnH5nVo{B9BmQoR!28 zDrE2zIJv^3YiXdpdI;KI96<1M_tX%#bgb?ThnMPSi?HOYOP8XJ-tl@5T|?JPH}>ue z*rPW;(OzrA-Ys^q`_Pf zn--6#y2ciO>G`~(EgLZy(I$uvVsr|77K_m;LQ0s zDKJN)Ce|z&7+O}X>>}3KyksPpIk#5K=c$iH4^rya8MOfWX;be9j$EL}Ia^S19sv}+ z;9Qix`-YB+SB!0g5n=KYps2JZ2bXel9gJPAP-PEl>hO3uYH^A%du7y{q z|8Y^#EY(`CvP%+JfxJ);Cyjbk&@i6{v$}!%xCpWz&@~xeB`~Ok44miL;Z#bN4zxS$ z(I&1gI|f4BKz+(%F>ut&HR{&646FzFdkU`}o&oCN37Sr~Q@_l`vP^@r3~I$ZhJHwv z0YRQc03)BOsTOPw8bTO00K!^U#iqgDk!Vwf@1`mCB4jZBXlO*u4Z}VgXEi_rnMK8L zy3L~iYgS#s&M-9UBzJc-qU(9AXwE2hUpK+!Dt-vr&)?OcPVlak*80>QX>c6aRCC%r z*vekCq-Z&vN@$+?IjO87e3{{gnc>kf>bw77W_BzyOZ~$LIqrh6uXo?+{vx~u5W==0lQq8(NPQQy{iiCClRrdGkoI#gR_*q4Tkf51GA9$wAnj(k z-L%_H61FOl80k2_an-v> z{!O%PlGMF&>YkmtN5Yep$SKk}@X~8{4s2RGF48$!?wqtcCrNmUue!`uUEXdbsqu1Z z+)j;?@WclbTeBoJQcjK7sSy&+R3e{~o*OS$?VcOk-8-|S=RvvWf!*_fgdbKSagrF= z7`GGPXS1nT6y7x#9Q z#74`pQ9CwD!ebxJ`3GRX*|_slNE1iM9wcdqC243oLXw$sGGiw*Bs{t|xZ`^%fcieX zQX#SD88_!Q7yho=SI6zEGxpVAl7$sw{DH)umE+IsIO8KS4Co=Fy)2`>Ti$;J?4djM z(AW0RugPK&6n-b(Wh^DXV!S~Myg|!W-jhDy;M>=O|0CfEw(C18?QG*$qNiV+v7@J7 zFO*cf@3!4HVfW3DM+;>635l+iqic3_t>$yHnt|pWYidu#FU6?$`B&^?EYy#iw3o)5 z1F#Rx_c{Nd_SH{8;}NLQKx3Yt;r!Tk&MVBUa8o(x{pi2x+z7XgB^a`+%*;bmbBUr@ o^9o`Y5jvf}ig1=3_E+;xh@!V99NxYN+TrdueDd8#RCqN11+w)$1ONa4 literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/0002_seed_demo_pos.cpython-311.pyc b/core/migrations/__pycache__/0002_seed_demo_pos.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0fcc89fd596550e5d41d564c6acc8a388f37bf43 GIT binary patch literal 3100 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 literal 0 HcmV?d00001 diff --git a/core/models.py b/core/models.py index 71a8362..378601b 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,162 @@ -from django.db import models +from decimal import Decimal -# Create your models here. +from django.conf import settings +from django.db import models +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.utils import timezone + + +class UserRole(models.Model): + class Role(models.TextChoices): + ADMIN = "admin", "Admin" + WAITER = "waiter", "Mesero" + KITCHEN = "kitchen", "Cocina" + + user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="role_profile") + role = models.CharField(max_length=20, choices=Role.choices, default=Role.WAITER) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["user__username"] + verbose_name = "Rol de usuario" + verbose_name_plural = "Roles de usuario" + + def __str__(self): + return f"{self.user.username} · {self.get_role_display()}" + + @classmethod + def resolve_for(cls, user): + if not user or not user.is_authenticated: + return None + if user.is_superuser: + return cls.Role.ADMIN + profile, _ = cls.objects.get_or_create(user=user, defaults={"role": cls.Role.WAITER}) + return profile.role + + @classmethod + def label_for(cls, user): + role = cls.resolve_for(user) + return dict(cls.Role.choices).get(role, "Sin rol") if role else "Invitado" + + +@receiver(post_save, sender=settings.AUTH_USER_MODEL) +def ensure_user_role(sender, instance, created, **kwargs): + if created and not instance.is_superuser: + UserRole.objects.get_or_create(user=instance, defaults={"role": UserRole.Role.WAITER}) + + +class Table(models.Model): + class Status(models.TextChoices): + FREE = "free", "Libre" + OCCUPIED = "occupied", "Ocupada" + + name = models.CharField(max_length=40, unique=True) + seats = models.PositiveSmallIntegerField(default=4) + status = models.CharField(max_length=12, choices=Status.choices, default=Status.FREE) + area = models.CharField(max_length=50, blank=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["name"] + + def __str__(self): + return self.name + + @property + def current_order(self): + cached_orders = getattr(self, "open_orders_cache", None) + if cached_orders is not None: + return cached_orders[0] if cached_orders else None + return self.orders.exclude(status=Order.Status.PAID).order_by("-created_at").first() + + +class Product(models.Model): + name = models.CharField(max_length=120) + category = models.CharField(max_length=80) + price = models.DecimalField(max_digits=10, decimal_places=2) + is_available = models.BooleanField(default=True) + station = models.CharField(max_length=80, default="Cocina") + + class Meta: + ordering = ["category", "name"] + + def __str__(self): + return self.name + + +class Order(models.Model): + class Status(models.TextChoices): + OPEN = "open", "Abierta" + PREPARING = "preparing", "En preparación" + READY = "ready", "Lista" + PAID = "paid", "Pagada" + + table = models.ForeignKey(Table, on_delete=models.PROTECT, related_name="orders") + status = models.CharField(max_length=20, choices=Status.choices, default=Status.OPEN) + guest_name = models.CharField(max_length=120, blank=True) + server_note = models.CharField(max_length=200, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + sent_to_kitchen_at = models.DateTimeField(null=True, blank=True) + paid_at = models.DateTimeField(null=True, blank=True) + + class Meta: + ordering = ["-created_at"] + + def __str__(self): + return f"Orden #{self.pk} · {self.table.name}" + + @property + def subtotal(self): + if hasattr(self, "subtotal_value") and self.subtotal_value is not None: + return self.subtotal_value + total = sum((item.line_total for item in self.items.select_related("product").all()), Decimal("0.00")) + return total.quantize(Decimal("0.01")) + + @property + def total_items(self): + if hasattr(self, "items_count") and self.items_count is not None: + return self.items_count + return sum(item.quantity for item in self.items.all()) + + def allowed_transitions(self): + transitions = { + self.Status.OPEN: [self.Status.PREPARING], + self.Status.PREPARING: [self.Status.READY], + self.Status.READY: [self.Status.PAID], + self.Status.PAID: [], + } + return transitions.get(self.status, []) + + def advance_to(self, new_status): + if new_status not in self.allowed_transitions(): + raise ValueError("Invalid status transition") + + now = timezone.now() + self.status = new_status + if new_status == self.Status.PREPARING and not self.sent_to_kitchen_at: + self.sent_to_kitchen_at = now + if new_status == self.Status.PAID: + self.paid_at = now + self.table.status = Table.Status.FREE + self.table.save(update_fields=["status", "updated_at"]) + self.save(update_fields=["status", "sent_to_kitchen_at", "paid_at", "updated_at"]) + + +class OrderItem(models.Model): + order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="items") + product = models.ForeignKey(Product, on_delete=models.PROTECT, related_name="order_items") + quantity = models.PositiveIntegerField(default=1) + note = models.CharField(max_length=200, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ["created_at", "id"] + + def __str__(self): + return f"{self.quantity} x {self.product.name}" + + @property + def line_total(self): + return (self.product.price * self.quantity).quantize(Decimal("0.01")) diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..4e0ad4a 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,72 @@ +{% load static %} - - + - {% block title %}Knowledge Base{% endblock %} - {% if project_description %} - - - - {% endif %} + + {% block title %}{{ meta_title|default:"Restaurante POS" }}{% endblock %} + {% if project_image_url %} {% endif %} - {% load static %} + + + + {% block head %}{% endblock %} + +
+ - - {% block content %}{% endblock %} + {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} + + {% block content %}{% endblock %} +
+ + - diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..a2376b1 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,190 @@ {% extends "base.html" %} -{% block title %}{{ project_name }}{% endblock %} - -{% block head %} - - - - -{% endblock %} +{% block title %}{{ meta_title }}{% endblock %} +{% block meta_description %}{{ meta_description }}{% endblock %} {% block content %}
-
-

Analyzing your requirements and generating your app…

-
- Loading… +
+
+
+ Primera iteración operativa +

Controla mesas, comandas y cocina con un dashboard táctil y claro.

+

Un punto de partida usable para meseros y cocina: abre una mesa, agrega productos con notas, sigue el estado de la orden y cobra cuando esté lista.

+ +
+
+
+ Mesas ocupadas + {{ occupied_tables }} +
+
+
+
+ Productos activos + {{ available_products }} +
+
+
+
+ Ventas de hoy + ${{ daily_revenue|floatformat:2 }} +
+
+
+
+
+
+ Orden activa + Mesa 4 +

2x Pasta trufa · 1x Limonada

+
Lista para cobrar
+
+
+ Cocina + {{ kitchen_orders|length }} órdenes +

Vista rápida para preparación

+
+
+
+
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
+ + +
+
+
+
+ Flujo principal +

Dashboard de mesas

+
+

Toca una mesa para registrar productos, notas para cocina y revisar la comanda activa.

+
+
+ +
+
+
+
+ Ventas rápidas +

Top productos

+
+
+ {% if top_products %} +
+ {% for product in top_products %} +
+
+ {{ product.name }} +

{{ product.category }}

+
+ {{ product.sold }} vendidos +
+ {% endfor %} +
+ {% else %} +
Todavía no hay ventas pagadas para mostrar tendencias.
+ {% endif %} +
+
+
+
+
+ +
+
+
+
+
+
+
+ Seguimiento +

Órdenes recientes

+
+
+ {% if recent_orders %} + + {% else %} +
Aún no hay órdenes. Abre una mesa y agrega el primer producto.
+ {% endif %} +
+
+
+
+
+
+ Cocina +

Vista previa KDS

+
+ Pantalla completa +
+ {% if kitchen_orders %} +
+ {% for order in kitchen_orders %} +
+
+
+ #{{ order.id }} · {{ order.table.name }} +

{{ order.get_status_display }}

+
+ {{ order.get_status_display }} +
+
    + {% for item in order.items.all %} +
  • {{ item.quantity }} × {{ item.product.name }}{% if item.note %} — {{ item.note }}{% endif %}
  • + {% endfor %} +
+
+ {% endfor %} +
+ {% else %} +
La cocina está al día. No hay comandas pendientes.
+ {% endif %} +
+
+
+
+
-
- Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC) -
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/core/templates/core/kitchen.html b/core/templates/core/kitchen.html new file mode 100644 index 0000000..9021c6b --- /dev/null +++ b/core/templates/core/kitchen.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} + +{% block title %}{{ meta_title }}{% endblock %} +{% block meta_description %}{{ meta_description }}{% endblock %} + +{% block content %} +
+
+
+
+ KDS +

Pantalla de cocina

+

Órdenes pendientes y en preparación, listas para seguir en prioridad visual.

+
+ +
+ + {% if orders %} +
+ {% for order in orders %} +
+
+
+ {{ order.table.name }} +

Orden #{{ order.id }}

+
+ {{ order.get_status_display }} +
+
    + {% for item in order.items.all %} +
  • + {{ item.quantity }} × {{ item.product.name }} +

    {% if item.note %}{{ item.note }}{% else %}Sin nota adicional{% endif %}

    +
  • + {% endfor %} +
+ Abrir detalle +
+ {% endfor %} +
+ {% else %} +
+

No hay órdenes pendientes

+

Cuando un mesero envíe una comanda, aparecerá aquí automáticamente.

+ Volver al dashboard +
+ {% endif %} +
+
+{% endblock %} diff --git a/core/templates/core/order_detail.html b/core/templates/core/order_detail.html new file mode 100644 index 0000000..b31c91d --- /dev/null +++ b/core/templates/core/order_detail.html @@ -0,0 +1,91 @@ +{% extends "base.html" %} + +{% block title %}{{ meta_title }}{% endblock %} +{% block meta_description %}{{ meta_description }}{% endblock %} + +{% block content %} +
+
+
+
+ Orden / Facturación +

Orden #{{ order.id }}

+

{{ order.table.name }} · {{ order.get_status_display }} · {{ order.created_at|date:"d M Y · H:i" }}

+
+ +
+ +
+
+
+
+
+ Detalle +

Recibo en vivo

+
+ {{ order.get_status_display }} +
+
+ {% for item in order.items.all %} +
+
+ {{ item.quantity }} × {{ item.product.name }} +

{% if item.note %}{{ item.note }}{% else %}Preparación estándar{% endif %}

+
+ ${{ item.line_total|floatformat:2 }} +
+ {% endfor %} +
+ +
+
+
+
+
+
+ Siguiente acción +

Flujo operativo

+
+
+
+
Abierta
+
En preparación
+
Lista
+
Pagada
+
+ {% if status_forms %} +
+ {% for status, form in status_forms.items %} +
+ {% csrf_token %} + {{ form.status }} + +
+ {% endfor %} +
+ {% else %} + + {% endif %} +
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/table_detail.html b/core/templates/core/table_detail.html new file mode 100644 index 0000000..5fe1049 --- /dev/null +++ b/core/templates/core/table_detail.html @@ -0,0 +1,88 @@ +{% extends "base.html" %} + +{% block title %}{{ meta_title }}{% endblock %} +{% block meta_description %}{{ meta_description }}{% endblock %} + +{% block content %} +
+
+
+
+ Mesero / Comanda +

{{ table.name }}

+

{{ table.seats }} puestos · Estado actual: {{ table.get_status_display }}

+
+
+ Volver al dashboard + {% if current_order %} + Ver recibo actual + {% endif %} +
+
+ +
+
+
+
+
+ Agregar ítems +

Nueva línea de comanda

+
+
+
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.errors %} +
{{ field.errors|join:", " }}
+ {% endif %} +
+ {% endfor %} + +
+
+
+
+
+
+
+ Orden activa +

{% if current_order %}Comanda #{{ current_order.id }}{% else %}Sin comanda todavía{% endif %}

+
+ {% if current_order %} + {{ current_order.get_status_display }} + {% endif %} +
+ {% if current_order %} +
+ {% for item in current_order.items.all %} +
+
+ {{ item.quantity }} × {{ item.product.name }} +

{% if item.note %}{{ item.note }}{% else %}Sin nota adicional{% endif %}

+
+ ${{ item.line_total|floatformat:2 }} +
+ {% endfor %} +
+ + {% else %} +
+

Mesa lista para abrir

+

Agrega el primer producto para crear automáticamente la comanda activa.

+
+ {% endif %} +
+
+
+
+
+{% endblock %} diff --git a/core/templates/registration/login.html b/core/templates/registration/login.html new file mode 100644 index 0000000..90d37f0 --- /dev/null +++ b/core/templates/registration/login.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} + +{% block title %}Acceso POS | Restaurante{% endblock %} +{% block meta_description %}Acceso seguro para Admin, Mesero y Cocina dentro del POS del restaurante.{% endblock %} + +{% block content %} +
+
+
+
+ Acceso seguro +

Iniciar sesión

+

Usa tu cuenta para entrar como Admin, Mesero o Cocina.

+ +
+ {% csrf_token %} +
+ + {{ form.username }} +
+
+ + {{ form.password }} +
+ {% if form.errors %} +
Usuario o contraseña incorrectos.
+ {% endif %} + {% if next %} + + {% endif %} + +
+ +
+
JWT API: /api/auth/token/
+
Perfil actual: /api/auth/me/
+
+
+
+
+
+{% endblock %} diff --git a/core/tests.py b/core/tests.py index 7ce503c..d165c31 100644 --- a/core/tests.py +++ b/core/tests.py @@ -1,3 +1,82 @@ +from django.contrib.auth.models import User from django.test import TestCase +from django.urls import reverse +from rest_framework.test import APIClient -# Create your tests here. +from .models import Order, Product, Table, UserRole + + +class PosWorkflowTests(TestCase): + def setUp(self): + self.table = Table.objects.create(name="Mesa 1", seats=4) + self.product = Product.objects.create(name="Burger clásica", category="Cocina", price="12.50") + self.waiter = User.objects.create_user(username="mesero", password="clave12345") + UserRole.objects.filter(user=self.waiter).update(role=UserRole.Role.WAITER) + self.kitchen = User.objects.create_user(username="cocina", password="clave12345") + UserRole.objects.filter(user=self.kitchen).update(role=UserRole.Role.KITCHEN) + + def test_home_requires_login(self): + response = self.client.get(reverse("home")) + self.assertEqual(response.status_code, 302) + self.assertIn(reverse("login"), response.url) + + def test_add_item_creates_active_order_and_marks_table_occupied(self): + self.client.login(username="mesero", password="clave12345") + response = self.client.post( + reverse("table_detail", args=[self.table.pk]), + {"product": self.product.pk, "quantity": 2, "note": "sin cebolla"}, + follow=True, + ) + + self.table.refresh_from_db() + order = Order.objects.get(table=self.table) + + self.assertEqual(response.status_code, 200) + self.assertEqual(order.status, Order.Status.OPEN) + self.assertEqual(order.items.count(), 1) + self.assertEqual(self.table.status, Table.Status.OCCUPIED) + + def test_mark_paid_frees_table(self): + self.client.login(username="mesero", password="clave12345") + order = Order.objects.create(table=self.table, status=Order.Status.READY) + self.table.status = Table.Status.OCCUPIED + self.table.save(update_fields=["status", "updated_at"]) + + response = self.client.post( + reverse("order_detail", args=[order.pk]), + {"status": Order.Status.PAID}, + follow=True, + ) + + order.refresh_from_db() + self.table.refresh_from_db() + + self.assertEqual(response.status_code, 200) + self.assertEqual(order.status, Order.Status.PAID) + self.assertEqual(self.table.status, Table.Status.FREE) + + def test_kitchen_screen_blocks_waiter_role(self): + self.client.login(username="mesero", password="clave12345") + response = self.client.get(reverse("kitchen_board"), follow=True) + self.assertEqual(response.status_code, 200) + self.assertRedirects(response, reverse("home")) + + def test_kitchen_role_can_open_kds(self): + self.client.login(username="cocina", password="clave12345") + response = self.client.get(reverse("kitchen_board")) + self.assertEqual(response.status_code, 200) + + def test_jwt_login_and_me_endpoint(self): + api_client = APIClient() + token_response = api_client.post( + reverse("token_obtain_pair"), + {"username": "mesero", "password": "clave12345"}, + format="json", + ) + self.assertEqual(token_response.status_code, 200) + self.assertIn("access", token_response.data) + + api_client.credentials(HTTP_AUTHORIZATION=f"Bearer {token_response.data['access']}") + me_response = api_client.get(reverse("current_user_api")) + self.assertEqual(me_response.status_code, 200) + self.assertEqual(me_response.data["role"], UserRole.Role.WAITER) diff --git a/core/urls.py b/core/urls.py index 6299e3d..2cdb4cc 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,27 @@ +from django.contrib.auth.views import LoginView, LogoutView from django.urls import path +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView -from .views import home +from .api import current_user_api +from .forms import LoginForm +from .views import home, kitchen_board, order_detail, table_detail urlpatterns = [ path("", home, name="home"), + path( + "login/", + LoginView.as_view( + template_name="registration/login.html", + authentication_form=LoginForm, + redirect_authenticated_user=True, + ), + name="login", + ), + path("logout/", LogoutView.as_view(), name="logout"), + path("kitchen/", kitchen_board, name="kitchen_board"), + path("tables//", table_detail, name="table_detail"), + path("orders//", order_detail, name="order_detail"), + path("api/auth/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), + path("api/auth/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), + path("api/auth/me/", current_user_api, name="current_user_api"), ] diff --git a/core/views.py b/core/views.py index c9aed12..d0be6b8 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,162 @@ -import os -import platform +from functools import wraps +from urllib.parse import quote -from django import get_version as django_version -from django.shortcuts import render +from django.contrib import messages +from django.db import transaction +from django.db.models import DecimalField, ExpressionWrapper, F, Prefetch, Sum +from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone +from .forms import AddOrderItemForm, OrderStatusForm +from .models import Order, Product, Table, UserRole + +LINE_TOTAL = ExpressionWrapper( + F("items__quantity") * F("items__product__price"), + output_field=DecimalField(max_digits=10, decimal_places=2), +) + + +def role_required(*allowed_roles): + def decorator(view_func): + @wraps(view_func) + def wrapped(request, *args, **kwargs): + if not request.user.is_authenticated: + return redirect(f"/login/?next={quote(request.get_full_path())}") + + resolved_role = UserRole.resolve_for(request.user) + if allowed_roles and resolved_role not in allowed_roles: + messages.error(request, "Tu rol no tiene acceso a esta sección.") + return redirect("home") + return view_func(request, *args, **kwargs) + + return wrapped + + return decorator + + +def _dashboard_context(): + active_orders = Order.objects.exclude(status=Order.Status.PAID).prefetch_related("items__product").order_by("-created_at") + tables = Table.objects.prefetch_related( + Prefetch("orders", queryset=active_orders, to_attr="open_orders_cache") + ) + recent_orders = Order.objects.select_related("table").prefetch_related("items__product")[:6] + kitchen_orders = ( + Order.objects.filter(status__in=[Order.Status.OPEN, Order.Status.PREPARING]) + .select_related("table") + .prefetch_related("items__product")[:5] + ) + daily_revenue = ( + Order.objects.filter(status=Order.Status.PAID, paid_at__date=timezone.localdate()) + .aggregate(total=Sum(LINE_TOTAL))["total"] + or 0 + ) + top_products = ( + Product.objects.filter(order_items__order__status=Order.Status.PAID) + .annotate(sold=Sum("order_items__quantity")) + .order_by("-sold", "name")[:4] + ) + + return { + "tables": tables, + "recent_orders": recent_orders, + "kitchen_orders": kitchen_orders, + "available_products": Product.objects.filter(is_available=True).count(), + "occupied_tables": Table.objects.filter(status=Table.Status.OCCUPIED).count(), + "daily_revenue": daily_revenue, + "top_products": top_products, + "meta_title": "Restaurante POS | Operación de mesas, cocina y cobro", + "meta_description": "Dashboard táctil para controlar mesas, comandas, cocina y cobro desde una sola interfaz ligera.", + } + + +@role_required(UserRole.Role.ADMIN, UserRole.Role.WAITER, UserRole.Role.KITCHEN) def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() + context = _dashboard_context() + context["resolved_user_role"] = UserRole.label_for(request.user) + return render(request, "core/index.html", context) + + +@role_required(UserRole.Role.ADMIN, UserRole.Role.WAITER) +def table_detail(request, table_id): + table = get_object_or_404( + Table.objects.prefetch_related( + Prefetch( + "orders", + queryset=Order.objects.exclude(status=Order.Status.PAID).prefetch_related("items__product").order_by("-created_at"), + to_attr="open_orders_cache", + ) + ), + pk=table_id, + ) + current_order = table.current_order + + if request.method == "POST": + form = AddOrderItemForm(request.POST) + if form.is_valid(): + with transaction.atomic(): + if current_order is None: + current_order = Order.objects.create(table=table) + item = form.save(commit=False) + item.order = current_order + item.save() + if table.status != Table.Status.OCCUPIED: + table.status = Table.Status.OCCUPIED + table.save(update_fields=["status", "updated_at"]) + messages.success(request, f"Se agregó {item.product.name} a {table.name}.") + return redirect("table_detail", table_id=table.pk) + else: + form = AddOrderItemForm() context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + "table": table, + "current_order": current_order, + "form": form, + "meta_title": f"{table.name} | Comanda activa", + "meta_description": f"Captura de comandas y notas para {table.name}.", + "resolved_user_role": UserRole.label_for(request.user), } - return render(request, "core/index.html", context) + return render(request, "core/table_detail.html", context) + + +@role_required(UserRole.Role.ADMIN, UserRole.Role.WAITER, UserRole.Role.KITCHEN) +def order_detail(request, order_id): + order = get_object_or_404(Order.objects.select_related("table").prefetch_related("items__product"), pk=order_id) + + if request.method == "POST": + form = OrderStatusForm(request.POST, order=order) + if form.is_valid(): + next_status = form.cleaned_data["status"] + with transaction.atomic(): + order.advance_to(next_status) + messages.success(request, f"La orden #{order.pk} ahora está {order.get_status_display().lower()}.") + return redirect("order_detail", order_id=order.pk) + status_forms = { + status: OrderStatusForm(order=order, initial={"status": status}) + for status in order.allowed_transitions() + } + + context = { + "order": order, + "status_forms": status_forms, + "meta_title": f"Orden #{order.pk} | {order.table.name}", + "meta_description": f"Detalle de la orden #{order.pk} con total, ítems y estado operativo.", + "resolved_user_role": UserRole.label_for(request.user), + } + return render(request, "core/order_detail.html", context) + + +@role_required(UserRole.Role.ADMIN, UserRole.Role.KITCHEN) +def kitchen_board(request): + orders = ( + Order.objects.filter(status__in=[Order.Status.OPEN, Order.Status.PREPARING]) + .select_related("table") + .prefetch_related("items__product") + ) + context = { + "orders": orders, + "meta_title": "KDS | Cocina pendiente", + "meta_description": "Pantalla de cocina con órdenes pendientes de preparación.", + "resolved_user_role": UserRole.label_for(request.user), + } + return render(request, "core/kitchen.html", context) diff --git a/requirements.txt b/requirements.txt index e22994c..a415ed8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ Django==5.2.7 mysqlclient==2.2.7 python-dotenv==1.1.1 +djangorestframework==3.16.1 +djangorestframework-simplejwt==5.5.1 diff --git a/static/css/custom.css b/static/css/custom.css index 925f6ed..7e5daed 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,4 +1,597 @@ -/* Custom styles for the application */ -body { - font-family: system-ui, -apple-system, sans-serif; +/* Restaurant POS custom theme */ +:root { + --pos-primary: #0f766e; + --pos-primary-dark: #0b5d57; + --pos-secondary: #16324f; + --pos-accent: #f97316; + --pos-accent-soft: #fff1e8; + --pos-surface: #ffffff; + --pos-surface-soft: #f5f8fb; + --pos-surface-muted: #eef4f7; + --pos-border: #d6e1e8; + --pos-text: #11243d; + --pos-muted: #61728a; + --pos-success: #15803d; + --pos-shadow: 0 26px 65px rgba(17, 36, 61, 0.12); + --pos-radius-xl: 32px; + --pos-radius-lg: 24px; + --pos-radius-md: 18px; + --pos-radius-sm: 12px; +} + +html { + scroll-behavior: smooth; +} + +body.pos-app { + margin: 0; + min-height: 100vh; + font-family: 'Inter', sans-serif; + color: var(--pos-text); + background: + radial-gradient(circle at top left, rgba(15, 118, 110, 0.18), transparent 32%), + radial-gradient(circle at 85% 10%, rgba(249, 115, 22, 0.14), transparent 25%), + linear-gradient(180deg, #fcfefd 0%, #f4f8fb 100%); +} + +h1, +h2, +h3, +h4, +.navbar-brand strong { + font-family: 'Manrope', sans-serif; + letter-spacing: -0.03em; +} + +p, +span, +a, +button, +label, +input, +select, +textarea { + font-family: 'Inter', sans-serif; +} + +.page-shell { + position: relative; + overflow: hidden; +} + +.site-header { + background: rgba(255, 255, 255, 0.82); + backdrop-filter: blur(18px); + border-bottom: 1px solid rgba(214, 225, 232, 0.72); +} + +.navbar { + padding: 1rem 0; +} + +.navbar-brand { + display: inline-flex; + align-items: center; + gap: 0.85rem; + color: var(--pos-text); + text-decoration: none; +} + +.navbar-brand small { + display: block; + color: var(--pos-muted); + font-size: 0.8rem; + font-weight: 600; +} + +.brand-mark { + width: 48px; + height: 48px; + border-radius: 16px; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 800; + color: #fff; + background: linear-gradient(135deg, var(--pos-primary) 0%, #15b39d 100%); + box-shadow: 0 16px 30px rgba(15, 118, 110, 0.28); +} + +.nav-link { + color: var(--pos-muted); + font-weight: 600; + border-radius: 999px; + padding: 0.7rem 1rem !important; +} + +.nav-link:hover, +.nav-link:focus { + color: var(--pos-secondary); + background: rgba(15, 118, 110, 0.08); +} + +.navbar-toggler { + border: 1px solid var(--pos-border); +} + +.pos-alert { + border-radius: var(--pos-radius-sm); + border: none; + box-shadow: var(--pos-shadow); +} + +.hero-section, +.dashboard-section, +.content-section { + padding: 2.5rem 0 5rem; +} + +.hero-grid { + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.8fr); + gap: 2rem; + align-items: center; +} + +.hero-copy { + padding: 2rem 0 1rem; +} + +.eyebrow { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.55rem 0.9rem; + border-radius: 999px; + color: var(--pos-primary-dark); + background: rgba(15, 118, 110, 0.11); + font-size: 0.78rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.hero-copy h1, +.page-intro h1 { + font-size: clamp(2.8rem, 5vw, 4.7rem); + line-height: 0.97; + margin: 1.25rem 0 1rem; +} + +.hero-text, +.page-intro p, +.section-heading p, +.empty-state-card p, +.stack-item p, +.receipt-line p, +.kds-card p, +.table-order-preview p, +.empty-mini, +.metric-card span { + color: var(--pos-muted); +} + +.hero-text { + max-width: 56ch; + font-size: 1.05rem; + line-height: 1.7; +} + +.hero-actions, +.page-actions, +.action-stack { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +.hero-actions { + margin: 2rem 0; +} + +.btn { + border-radius: 16px; + font-weight: 700; + padding: 0.88rem 1.35rem; + border: none; +} + +.btn:focus-visible, +.form-control:focus, +.form-select:focus, +textarea:focus { + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(15, 118, 110, 0.18); +} + +.btn-pos-primary { + color: #fff; + background: linear-gradient(135deg, var(--pos-primary) 0%, #18a58f 100%); + box-shadow: 0 18px 32px rgba(15, 118, 110, 0.24); +} + +.btn-pos-primary:hover, +.btn-pos-primary:focus { + color: #fff; + background: linear-gradient(135deg, var(--pos-primary-dark) 0%, #108d79 100%); +} + +.btn-pos-soft { + color: var(--pos-secondary); + background: rgba(255, 255, 255, 0.88); + border: 1px solid rgba(214, 225, 232, 0.95); + box-shadow: 0 14px 30px rgba(17, 36, 61, 0.08); +} + +.btn-pos-soft:hover, +.btn-pos-soft:focus { + color: var(--pos-secondary); + background: #fff; +} + +.hero-kpis { + max-width: 760px; +} + +.metric-card, +.side-panel-card, +.panel-card, +.kds-card, +.empty-state-card, +.floating-card { + background: rgba(255, 255, 255, 0.92); + border: 1px solid rgba(214, 225, 232, 0.84); + box-shadow: var(--pos-shadow); +} + +.metric-card { + height: 100%; + padding: 1.2rem 1.25rem; + border-radius: var(--pos-radius-md); +} + +.metric-card strong { + display: block; + margin-top: 0.5rem; + font-size: 1.65rem; +} + +.hero-visual { + position: relative; + min-height: 500px; + display: flex; + align-items: center; + justify-content: center; +} + +.floating-card { + position: absolute; + width: min(100%, 320px); + padding: 1.5rem; + border-radius: 28px; +} + +.primary-card { + top: 2rem; + right: 2rem; +} + +.secondary-card { + left: 0; + bottom: 5rem; +} + +.floating-card strong { + font-size: 1.6rem; +} + +.floating-card p, +.stack-item p, +.kitchen-ticket p, +.receipt-line p, +.kds-items p { + margin: 0.4rem 0 0; +} + +.floating-label, +.table-label, +.text-link { + color: var(--pos-accent); + font-weight: 700; + text-decoration: none; +} + +.shape { + position: absolute; + border-radius: 28px; + filter: blur(0.2px); +} + +.shape-sphere { + width: 180px; + height: 180px; + border-radius: 50%; + background: radial-gradient(circle at 30% 30%, #ffffff 0%, #a7f3d0 42%, rgba(15, 118, 110, 0.1) 72%, transparent 75%); + bottom: 0; + right: 18%; +} + +.shape-cube { + width: 120px; + height: 120px; + background: linear-gradient(135deg, rgba(249, 115, 22, 0.16), rgba(22, 50, 79, 0.12)); + transform: rotate(18deg); + top: 0; + left: 20%; +} + +.section-muted { + background: linear-gradient(180deg, rgba(238, 244, 247, 0.55), rgba(255, 255, 255, 0)); +} + +.section-heading, +.page-intro { + display: flex; + justify-content: space-between; + align-items: end; + gap: 1rem; + margin-bottom: 1.75rem; +} + +.section-heading.compact h2, +.panel-card h2, +.kds-card h2, +.table-card h3 { + margin: 0.45rem 0 0; + font-size: 1.45rem; +} + +.table-grid, +.kds-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1.15rem; +} + +.table-card, +.stack-item-link { + text-decoration: none; + color: inherit; +} + +.table-card { + padding: 1.3rem; + border-radius: 24px; + min-height: 190px; + border: 1px solid transparent; + transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; + background: rgba(255, 255, 255, 0.88); + box-shadow: 0 18px 42px rgba(17, 36, 61, 0.08); +} + +.table-card:hover, +.stack-item-link:hover, +.stack-item-link:focus { + transform: translateY(-3px); + box-shadow: 0 22px 46px rgba(17, 36, 61, 0.12); +} + +.table-card.is-free { + border-color: rgba(21, 128, 61, 0.16); + background: linear-gradient(180deg, #ffffff 0%, #f5fff8 100%); +} + +.table-card.is-occupied { + border-color: rgba(249, 115, 22, 0.18); + background: linear-gradient(180deg, #ffffff 0%, #fff8f3 100%); +} + +.table-card-top, +.receipt-line, +.receipt-footer, +.stack-item, +.kitchen-ticket, +.workflow-steps { + display: flex; + justify-content: space-between; + gap: 1rem; +} + +.table-card-top { + align-items: start; +} + +.table-order-preview, +.empty-mini { + margin-top: 2.5rem; +} + +.status-pill { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.5rem 0.85rem; + border-radius: 999px; + font-size: 0.82rem; + font-weight: 800; +} + +.status-free { + color: var(--pos-primary-dark); + background: rgba(15, 118, 110, 0.12); +} + +.status-busy { + color: #b45309; + background: rgba(249, 115, 22, 0.14); +} + +.status-ready { + color: var(--pos-success); + background: rgba(21, 128, 61, 0.13); +} + +.side-panel-card, +.panel-card { + padding: 1.5rem; + border-radius: var(--pos-radius-lg); + height: 100%; +} + +.stack-list, +.kitchen-preview-list, +.receipt-lines, +.kds-items { + display: grid; + gap: 0.95rem; +} + +.stack-item, +.kitchen-ticket, +.receipt-line, +.workflow-step, +.kds-items li { + padding: 1rem 1.1rem; + border-radius: 18px; + background: var(--pos-surface-soft); + border: 1px solid rgba(214, 225, 232, 0.7); +} + +.receipt-line, +.stack-item, +.kitchen-ticket { + align-items: center; +} + +.receipt-line strong, +.stack-item strong, +.kds-items strong, +.kitchen-ticket strong { + font-size: 1.02rem; +} + +.receipt-footer { + align-items: center; + margin-top: 1.2rem; + padding-top: 1.2rem; + border-top: 1px solid rgba(214, 225, 232, 0.8); +} + +.receipt-footer.large strong { + font-size: 1.7rem; +} + +.page-intro { + align-items: center; + margin-bottom: 2rem; +} + +.panel-card .form-control, +.panel-card .form-select { + border-radius: 14px; + border-color: var(--pos-border); + padding: 0.95rem 1rem; +} + +.form-label { + font-weight: 700; + color: var(--pos-secondary); +} + +.workflow-steps { + flex-direction: column; + margin-bottom: 1.2rem; +} + +.workflow-step { + font-weight: 700; + color: var(--pos-muted); +} + +.workflow-step.active { + color: var(--pos-primary-dark); + background: rgba(15, 118, 110, 0.12); +} + +.kds-items { + list-style: none; + padding: 0; + margin: 0 0 1rem; +} + +.kds-card { + padding: 1.4rem; + border-radius: 28px; +} + +.empty-state-card { + padding: 2rem; + border-radius: 28px; +} + +.empty-state-card.small { + padding: 1.5rem; +} + +.empty-state-card.centered { + text-align: center; +} + +.paid-state { + background: linear-gradient(180deg, #ffffff 0%, #f4fff7 100%); +} + +@media (max-width: 991px) { + .hero-grid, + .section-heading, + .page-intro { + grid-template-columns: 1fr; + display: grid; + align-items: start; + } + + .hero-visual { + min-height: 360px; + margin-top: 1rem; + } + + .primary-card { + right: 1rem; + } +} + +@media (max-width: 767px) { + .hero-copy h1, + .page-intro h1 { + font-size: 2.5rem; + } + + .hero-section, + .dashboard-section, + .content-section { + padding: 1.5rem 0 3rem; + } + + .hero-actions, + .page-actions, + .action-stack { + flex-direction: column; + } + + .primary-card, + .secondary-card { + position: relative; + inset: auto; + width: 100%; + } + + .hero-visual { + display: grid; + gap: 1rem; + min-height: auto; + } + + .shape { + display: none; + } }