Ahmed version 1

This commit is contained in:
Flatlogic Bot 2026-02-09 16:33:02 +00:00
parent 0021eef6ea
commit eaeab96245
19 changed files with 913 additions and 185 deletions

View File

@ -1,3 +1,34 @@
from django.contrib import admin from django.contrib import admin
from .models import Ingredient, MenuItem, MenuItemIngredient, Order, OrderItem, UserProfile
# Register your models here. @admin.register(UserProfile)
class UserProfileAdmin(admin.ModelAdmin):
list_display = ('user', 'role')
list_filter = ('role',)
search_fields = ('user__username',)
class MenuItemIngredientInline(admin.TabularInline):
model = MenuItemIngredient
extra = 1
@admin.register(Ingredient)
class IngredientAdmin(admin.ModelAdmin):
list_display = ('name', 'stock_quantity', 'unit')
search_fields = ('name',)
@admin.register(MenuItem)
class MenuItemAdmin(admin.ModelAdmin):
list_display = ('name', 'price', 'is_active')
inlines = [MenuItemIngredientInline]
search_fields = ('name',)
class OrderItemInline(admin.TabularInline):
model = OrderItem
extra = 0
readonly_fields = ('menu_item', 'quantity', 'price_at_order')
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ('order_number', 'total_price', 'created_at')
inlines = [OrderItemInline]
readonly_fields = ('order_number', 'total_price', 'created_at', 'customer_notes')

View File

@ -0,0 +1,65 @@
# Generated by Django 5.2.7 on 2026-02-09 16:12
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Ingredient',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
('stock_quantity', models.DecimalField(decimal_places=2, default=0.0, max_digits=10)),
('unit', models.CharField(default='grams', max_length=20)),
],
),
migrations.CreateModel(
name='MenuItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
('description', models.TextField(blank=True)),
('image_url', models.URLField(blank=True, null=True)),
('is_active', models.BooleanField(default=True)),
],
),
migrations.CreateModel(
name='Order',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order_number', models.CharField(editable=False, max_length=12, unique=True)),
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
('total_price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10)),
('customer_notes', models.TextField(blank=True)),
],
),
migrations.CreateModel(
name='MenuItemIngredient',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity_required', models.DecimalField(decimal_places=2, max_digits=10)),
('ingredient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.ingredient')),
('menu_item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ingredients', to='core.menuitem')),
],
),
migrations.CreateModel(
name='OrderItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.PositiveIntegerField(default=1)),
('price_at_order', models.DecimalField(decimal_places=2, max_digits=10)),
('menu_item', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='core.menuitem')),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.order')),
],
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 5.2.7 on 2026-02-09 16:25
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('role', models.CharField(choices=[('manager', 'Manager'), ('cashier', 'Cashier')], default='cashier', max_length=10)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -1,3 +1,65 @@
from django.db import models from django.db import models
from django.db import transaction
from django.utils import timezone
from django.contrib.auth.models import User
import uuid
# Create your models here. class UserProfile(models.Model):
ROLE_CHOICES = [
('manager', 'Manager'),
('cashier', 'Cashier'),
]
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
role = models.CharField(max_length=10, choices=ROLE_CHOICES, default='cashier')
def __str__(self):
return f"{self.user.username} - {self.get_role_display()}"
class Ingredient(models.Model):
name = models.CharField(max_length=100, unique=True)
stock_quantity = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)
unit = models.CharField(max_length=20, default="grams")
def __str__(self):
return f"{self.name} ({self.stock_quantity} {self.unit})"
class MenuItem(models.Model):
name = models.CharField(max_length=100, unique=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
description = models.TextField(blank=True)
image_url = models.URLField(blank=True, null=True)
is_active = models.BooleanField(default=True)
def __str__(self):
return self.name
class MenuItemIngredient(models.Model):
menu_item = models.ForeignKey(MenuItem, related_name='ingredients', on_delete=models.CASCADE)
ingredient = models.ForeignKey(Ingredient, on_delete=models.CASCADE)
quantity_required = models.DecimalField(max_digits=10, decimal_places=2)
def __str__(self):
return f"{self.quantity_required} {self.ingredient.unit} of {self.ingredient.name} for {self.menu_item.name}"
class Order(models.Model):
order_number = models.CharField(max_length=12, unique=True, editable=False)
created_at = models.DateTimeField(default=timezone.now)
total_price = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)
customer_notes = models.TextField(blank=True)
def save(self, *args, **kwargs):
if not self.order_number:
self.order_number = str(uuid.uuid4().hex[:8]).upper()
super().save(*args, **kwargs)
def __str__(self):
return f"Order {self.order_number}"
class OrderItem(models.Model):
order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE)
menu_item = models.ForeignKey(MenuItem, on_delete=models.PROTECT)
quantity = models.PositiveIntegerField(default=1)
price_at_order = models.DecimalField(max_digits=10, decimal_places=2)
def __str__(self):
return f"{self.quantity} x {self.menu_item.name}"

View File

@ -1,25 +1,101 @@
{% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{% block title %}Knowledge Base{% endblock %}</title> <meta name="viewport" content="width=device-width, initial-scale=1.0">
{% if project_description %} <title>Searing Sandwiches | POS & Stock</title>
<meta name="description" content="{{ project_description }}"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet">
<meta property="og:description" content="{{ project_description }}"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<meta property="twitter:description" content="{{ project_description }}"> <style>
{% endif %} :root {
{% if project_image_url %} --deep-obsidian: #1A1A1D;
<meta property="og:image" content="{{ project_image_url }}"> --vibrant-accent: #FF4500;
<meta property="twitter:image" content="{{ project_image_url }}"> --slate-gray: #4E4E50;
{% endif %} --soft-white: #F5F5F7;
{% load static %} }
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
{% block head %}{% endblock %} body {
font-family: 'Inter', sans-serif;
background-color: var(--deep-obsidian);
color: var(--soft-white);
min-height: 100vh;
}
.navbar {
background-color: rgba(26, 26, 29, 0.9);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--slate-gray);
}
.vibrant-text {
color: var(--vibrant-accent);
}
.btn-vibrant {
background-color: var(--vibrant-accent);
color: white;
border: none;
transition: all 0.3s ease;
}
.btn-vibrant:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(255, 69, 0, 0.4);
color: white;
}
.card {
background-color: #242428;
border: 1px solid var(--slate-gray);
}
</style>
{% block extra_css %}{% endblock %}
</head> </head>
<body> <body>
{% block content %}{% endblock %} <nav class="navbar navbar-expand-lg sticky-top">
</body> <div class="container">
<a class="navbar-brand fw-bolder fs-3" href="{% url 'home' %}">
<span class="vibrant-text">SEARING</span> SANDWICHES
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto align-items-center">
{% if user.is_authenticated %}
{% if user.profile.role == 'manager' %}
<li class="nav-item">
<a class="nav-link text-light me-3" href="{% url 'dashboard' %}">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link text-light me-3" href="{% url 'manage_users' %}">Users</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link text-light me-3" href="{% url 'pos' %}">POS</a>
</li>
<li class="nav-item">
<span class="text-secondary me-3">Hi, {{ user.username }}</span>
</li>
<li class="nav-item">
<a href="{% url 'logout' %}" class="btn btn-sm btn-outline-secondary">Logout</a>
</li>
{% else %}
<li class="nav-item">
<a href="{% url 'login' %}" class="btn btn-vibrant px-4">Login</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<main>
{% block content %}{% endblock %}
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
{% block extra_js %}{% endblock %}
</body>
</html> </html>

View File

@ -0,0 +1,79 @@
{% extends 'base.html' %}
{% block content %}
<div class="container py-5">
<div class="d-flex justify-content-between align-items-center mb-5">
<h1 class="display-4 fw-bold" style="color: var(--vibrant-accent);">Manager Dashboard</h1>
<div>
<a href="{% url 'manage_users' %}" class="btn btn-outline-light me-2">Manage Users</a>
<a href="{% url 'pos' %}" class="btn btn-vibrant px-4">Go to POS</a>
</div>
</div>
<div class="row mb-5">
<div class="col-md-4">
<div class="card p-4 shadow-sm h-100">
<h5 class="text-secondary mb-3">Total Sales (All Time)</h5>
<h2 class="display-5 fw-bold">${{ total_sales|stringformat:".2f" }}</h2>
</div>
</div>
<div class="col-md-8">
<div class="card p-4 shadow-sm h-100">
<h5 class="text-secondary mb-3">Ingredient Stock Levels</h5>
<div class="row">
{% for ingredient in ingredients %}
<div class="col-md-4 mb-3">
<div class="p-3 bg-dark rounded border {% if ingredient.stock_quantity < 500 %}border-danger{% else %}border-secondary{% endif %}">
<div class="small text-secondary">{{ ingredient.name }}</div>
<div class="h4 mb-0">{{ ingredient.stock_quantity|floatformat:0 }} <small class="fs-6">{{ ingredient.unit }}</small></div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<div class="card shadow-sm">
<div class="card-body p-4">
<h3 class="mb-4">Recent Orders</h3>
<div class="table-responsive">
<table class="table table-dark table-hover align-middle border-0">
<thead class="text-secondary">
<tr>
<th>Order #</th>
<th>Time</th>
<th>Items</th>
<th>Notes</th>
<th class="text-end">Total</th>
<th></th>
</tr>
</thead>
<tbody>
{% for order in orders %}
<tr>
<td class="fw-bold">{{ order.order_number }}</td>
<td>{{ order.created_at|date:"H:i" }} <small class="text-secondary">{{ order.created_at|date:"d M" }}</small></td>
<td>
{% for item in order.items.all %}
<span class="badge bg-secondary">{{ item.quantity }}x {{ item.menu_item.name }}</span>
{% endfor %}
</td>
<td><small class="text-secondary">{{ order.customer_notes|truncatechars:30 }}</small></td>
<td class="text-end fw-bold">${{ order.total_price }}</td>
<td class="text-end">
<a href="{% url 'receipt' order.order_number %}" class="btn btn-sm btn-outline-light">Receipt</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center py-5 text-secondary">No orders found yet.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,145 +1,34 @@
{% extends "base.html" %} {% extends 'base.html' %}
{% block title %}Welcome | Liver & Sausage POS{% endblock %}
{% block title %}{{ project_name }}{% endblock %}
{% block head %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% {
background-position: 0% 0%;
}
100% {
background-position: 100% 100%;
}
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2.5rem 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
}
h1 {
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
font-weight: 700;
margin: 0 0 1.2rem;
letter-spacing: -0.02em;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
opacity: 0.92;
}
.loader {
margin: 1.5rem auto;
width: 56px;
height: 56px;
border: 4px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.runtime code {
background: rgba(0, 0, 0, 0.25);
padding: 0.15rem 0.45rem;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
footer {
position: absolute;
bottom: 1rem;
width: 100%;
text-align: center;
font-size: 0.85rem;
opacity: 0.75;
}
</style>
{% endblock %}
{% block content %} {% block content %}
<main> <div class="row justify-content-center align-items-center" style="min-height: 80vh;">
<div class="card"> <div class="col-lg-8 text-center">
<h1>Analyzing your requirements and generating your app…</h1> <h1 class="display-3 fw-bold mb-4" style="color: var(--text-light);">
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes"> Professional <span style="color: var(--orange);">Cashier System</span>
<span class="sr-only">Loading…</span> </h1>
<p class="lead mb-5 text-secondary">
Streamlined order management, automated stock tracking, and real-time reporting for your restaurant.
</p>
<div class="row g-4 justify-content-center">
<div class="col-md-5">
<div class="card h-100 p-4 border-0 shadow-lg text-start" style="background: linear-gradient(145deg, #242427, #1c1c1f);">
<div class="display-6 mb-3 text-orange" style="color: var(--orange);"><i class="bi bi-cart4"></i></div>
<h3 class="fw-bold">Cashier POS</h3>
<p class="text-secondary">Create orders, customize sandwiches, and print receipts instantly.</p>
<a href="{% url 'pos' %}" class="btn btn-primary mt-auto">Open POS</a>
</div>
</div>
<div class="col-md-5">
<div class="card h-100 p-4 border-0 shadow-lg text-start" style="background: linear-gradient(145deg, #242427, #1c1c1f);">
<div class="display-6 mb-3 text-info"><i class="bi bi-graph-up"></i></div>
<h3 class="fw-bold">Manager Dashboard</h3>
<p class="text-secondary">Track liver, sausage, and fries stock. View sales reports and manage prices.</p>
<a href="{% url 'dashboard' %}" class="btn btn-outline-light mt-auto">View Reports</a>
</div>
</div>
</div>
</div> </div>
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p> </div>
<p class="hint">This page will refresh automatically as the plan is implemented.</p> {% endblock %}
<p class="runtime">
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
</p>
</div>
</main>
<footer>
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
</footer>
{% endblock %}

View File

@ -0,0 +1,32 @@
{% extends 'base.html' %}
{% block content %}
<div class="row justify-content-center align-items-center" style="min-height: 80vh;">
<div class="col-md-4">
<div class="card bg-dark text-light border-0 shadow-lg" style="border-radius: 15px;">
<div class="card-body p-5">
<h2 class="text-center mb-4" style="color: var(--vibrant-accent);">Login</h2>
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="id_username" class="form-label">Username</label>
<input type="text" name="username" class="form-control bg-secondary text-light border-0" id="id_username" required>
</div>
<div class="mb-4">
<label for="id_password" class="form-label">Password</label>
<input type="password" name="password" class="form-control bg-secondary text-light border-0" id="id_password" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-lg" style="background-color: var(--vibrant-accent); color: white; font-weight: bold;">Sign In</button>
</div>
</form>
{% if form.errors %}
<div class="alert alert-danger mt-3 bg-danger text-light border-0">
Invalid username or password.
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,200 @@
{% extends 'base.html' %}
{% block title %}POS | Cashier Interface{% endblock %}
{% block extra_css %}
.menu-item-card {
cursor: pointer;
transition: all 0.2s;
}
.menu-item-card:hover {
border-color: var(--orange);
background-color: #2a2a2d;
}
.cart-sticky {
position: sticky;
top: 90px;
max-height: calc(100vh - 120px);
overflow-y: auto;
}
.cart-item {
border-bottom: 1px solid var(--slate);
padding: 10px 0;
}
.cart-item:last-child {
border-bottom: none;
}
{% endblock %}
{% block content %}
<div class="row g-4">
<!-- Menu Section -->
<div class="col-lg-8">
<h2 class="mb-4 fw-bold">Menu</h2>
<div class="row g-3">
{% for item in menu_items %}
<div class="col-md-4">
<div class="card h-100 menu-item-card shadow-sm" onclick="addToCart({{ item.id }}, '{{ item.name }}', {{ item.price }})">
<div class="card-body text-center py-4">
<h5 class="fw-bold mb-1">{{ item.name }}</h5>
<p class="text-orange fw-bold mb-0" style="color: var(--orange);">{{ item.price }} EGP</p>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Cart Section -->
<div class="col-lg-4">
<div class="card cart-sticky shadow-lg">
<div class="card-header py-3">
<h4 class="mb-0 fw-bold"><i class="bi bi-receipt me-2"></i> Current Order</h4>
</div>
<div class="card-body">
<div id="cart-items" class="mb-4">
<p class="text-center text-secondary py-4" id="empty-cart-msg">Cart is empty</p>
</div>
<div class="mb-3">
<label class="form-label small text-secondary">Customer Notes / Customization</label>
<textarea id="customer-notes" class="form-control bg-dark text-white border-secondary" rows="2" placeholder="e.g., No onion, extra spicy..."></textarea>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-secondary">Subtotal</span>
<span id="subtotal">0.00 EGP</span>
</div>
<div class="d-flex justify-content-between mb-4 border-top pt-2">
<h4 class="fw-bold">Total</h4>
<h4 class="fw-bold" id="total" style="color: var(--orange);">0.00 EGP</h4>
</div>
<button class="btn btn-primary w-100 py-3 fw-bold" onclick="completeOrder()" id="checkout-btn" disabled>
COMPLETE ORDER
</button>
</div>
</div>
</div>
</div>
<!-- Success Modal -->
<div class="modal fade" id="successModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="card w-100">
<div class="card-body text-center py-5">
<div class="display-1 text-success mb-4"><i class="bi bi-check-circle"></i></div>
<h2 class="fw-bold mb-3">Order Completed!</h2>
<p class="text-secondary mb-4" id="order-confirm-msg"></p>
<div class="d-grid gap-2">
<a id="print-receipt-btn" href="#" class="btn btn-primary" target="_blank">PRINT RECEIPT</a>
<button type="button" class="btn btn-outline-light" onclick="location.reload()">NEW ORDER</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let cart = {};
function addToCart(id, name, price) {
if (cart[id]) {
cart[id].quantity += 1;
} else {
cart[id] = { id, name, price, quantity: 1 };
}
renderCart();
}
function removeFromCart(id) {
if (cart[id]) {
if (cart[id].quantity > 1) {
cart[id].quantity -= 1;
} else {
delete cart[id];
}
}
renderCart();
}
function renderCart() {
const cartItemsDiv = document.getElementById('cart-items');
const emptyMsg = document.getElementById('empty-cart-msg');
const checkoutBtn = document.getElementById('checkout-btn');
cartItemsDiv.innerHTML = '';
let total = 0;
let itemCount = 0;
for (const id in cart) {
const item = cart[id];
itemCount += item.quantity;
total += item.price * item.quantity;
const itemDiv = document.createElement('div');
itemDiv.className = 'cart-item d-flex justify-content-between align-items-center';
itemDiv.innerHTML = `
<div>
<h6 class="mb-0 fw-bold">${item.name}</h6>
<small class="text-secondary">${item.price} x ${item.quantity}</small>
</div>
<div class="d-flex align-items-center">
<span class="fw-bold me-3">${(item.price * item.quantity).toFixed(2)}</span>
<button class="btn btn-sm btn-outline-danger" onclick="removeFromCart(${id})"><i class="bi bi-dash"></i></button>
</div>
`;
cartItemsDiv.appendChild(itemDiv);
}
if (itemCount > 0) {
emptyMsg.style.display = 'none';
checkoutBtn.disabled = false;
} else {
cartItemsDiv.appendChild(emptyMsg);
emptyMsg.style.display = 'block';
checkoutBtn.disabled = true;
}
document.getElementById('subtotal').innerText = total.toFixed(2) + ' EGP';
document.getElementById('total').innerText = total.toFixed(2) + ' EGP';
}
async function completeOrder() {
const btn = document.getElementById('checkout-btn');
const notes = document.getElementById('customer-notes').value;
btn.disabled = true;
btn.innerText = 'PROCESSING...';
const cartArray = Object.values(cart);
try {
const response = await fetch('{% url "create_order" %}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify({ cart: cartArray, notes: notes })
});
const result = await response.json();
if (result.success) {
document.getElementById('order-confirm-msg').innerText = 'Order #' + result.order_number + ' has been successfully placed and stock has been updated.';
document.getElementById('print-receipt-btn').href = '/receipt/' + result.order_number + '/';
const modal = new bootstrap.Modal(document.getElementById('successModal'));
modal.show();
} else {
alert('Error: ' + result.error);
btn.disabled = false;
btn.innerText = 'COMPLETE ORDER';
}
} catch (error) {
alert('Something went wrong. Please check stock levels.');
btn.disabled = false;
btn.innerText = 'COMPLETE ORDER';
}
}
</script>
{% endblock %}

View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Receipt - {{ order.order_number }}</title>
<style>
body { font-family: 'Courier New', Courier, monospace; color: #000; background: #fff; padding: 20px; width: 300px; margin: 0 auto; }
.text-center { text-align: center; }
.header { border-bottom: 1px dashed #000; padding-bottom: 10px; margin-bottom: 10px; }
.item { display: flex; justify-content: space-between; margin-bottom: 5px; }
.footer { border-top: 1px dashed #000; margin-top: 10px; padding-top: 10px; }
.total { font-weight: bold; font-size: 1.2em; }
@media print {
.no-print { display: none; }
}
</style>
</head>
<body onload="window.print()">
<div class="no-print" style="margin-bottom: 20px; text-align: center;">
<button onclick="window.print()">Print Receipt</button>
<button onclick="window.close()">Close</button>
</div>
<div class="text-center header">
<h2>LIVER & SAUSAGE</h2>
<p>RESTAURANT POS</p>
<p>Order #: {{ order.order_number }}</p>
<p>{{ order.created_at|date:"Y-m-d H:i" }}</p>
</div>
<div class="items">
{% for item in order.items.all %}
<div class="item">
<span>{{ item.quantity }} x {{ item.menu_item.name }}</span>
<span>{{ item.price_at_order }}</span>
</div>
{% endfor %}
</div>
<div class="footer">
<div class="item total">
<span>TOTAL</span>
<span>{{ order.total_price }} EGP</span>
</div>
{% if order.customer_notes %}
<div style="margin-top: 10px; font-size: 0.9em;">
<strong>Notes:</strong><br>
{{ order.customer_notes }}
</div>
{% endif %}
</div>
<div class="text-center" style="margin-top: 20px;">
<p>THANK YOU!</p>
</div>
</body>
</html>

View File

@ -0,0 +1,86 @@
{% extends 'base.html' %}
{% block content %}
<div class="container py-5">
<div class="d-flex justify-content-between align-items-center mb-5">
<h1 class="display-4 fw-bold" style="color: var(--vibrant-accent);">User Management</h1>
<a href="{% url 'dashboard' %}" class="btn btn-outline-light">Back to Dashboard</a>
</div>
<div class="row">
<!-- Create User Form -->
<div class="col-md-4">
<div class="card bg-dark text-light border-0 shadow-lg mb-4" style="border-radius: 15px;">
<div class="card-body p-4">
<h3 class="mb-4">Create New Account</h3>
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label class="form-label">Username</label>
<input type="text" name="username" class="form-control bg-secondary text-light border-0" required>
</div>
<div class="mb-3">
<label class="form-label">Password</label>
<input type="password" name="password" class="form-control bg-secondary text-light border-0" required>
</div>
<div class="mb-3">
<label class="form-label">Role</label>
<select name="role" class="form-select bg-secondary text-light border-0" required>
<option value="cashier">Cashier</option>
<option value="manager">Manager</option>
</select>
</div>
<div class="d-grid mt-4">
<button type="submit" class="btn" style="background-color: var(--vibrant-accent); color: white;">Create User</button>
</div>
</form>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{% if message.tags == 'error' %}danger{% else %}success{% endif %} mt-3 bg-opacity-25 border-0">
{{ message }}
</div>
{% endfor %}
{% endif %}
</div>
</div>
</div>
<!-- User List -->
<div class="col-md-8">
<div class="card bg-dark text-light border-0 shadow-lg" style="border-radius: 15px;">
<div class="card-body p-4">
<h3 class="mb-4">Existing Accounts</h3>
<div class="table-responsive">
<table class="table table-dark table-hover border-0">
<thead>
<tr class="text-secondary">
<th>Username</th>
<th>Role</th>
<th>Date Joined</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td class="fw-bold">{{ user.username }}</td>
<td>
<span class="badge {% if user.profile.role == 'manager' %}bg-primary{% else %}bg-info{% endif %}">
{{ user.profile.role|title }}
</span>
</td>
<td class="text-secondary">{{ user.date_joined|date:"M d, Y" }}</td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-center text-secondary py-4">No users found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,7 +1,13 @@
from django.urls import path from django.urls import path
from . import views
from .views import home
urlpatterns = [ urlpatterns = [
path("", home, name="home"), path('', views.home, name='home'),
path('login/', views.login_view, name='login'),
path('logout/', views.logout_view, name='logout'),
path('pos/', views.pos_view, name='pos'),
path('api/create-order/', views.create_order, name='create_order'),
path('receipt/<str:order_number>/', views.receipt_view, name='receipt'),
path('dashboard/', views.dashboard_view, name='dashboard'),
path('manage-users/', views.manage_users, name='manage_users'),
] ]

View File

@ -1,25 +1,146 @@
import os from django.shortcuts import render, get_object_or_404, redirect
import platform from django.http import JsonResponse
from django.db import transaction
from django import get_version as django_version from django.db.models import Sum, F
from django.shortcuts import render
from django.utils import timezone from django.utils import timezone
from django.contrib import messages
from django.contrib.auth import login, logout, authenticate
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.models import User
from .models import Ingredient, MenuItem, MenuItemIngredient, Order, OrderItem, UserProfile
import json
def is_manager(user):
return user.is_authenticated and hasattr(user, 'profile') and user.profile.role == 'manager'
def is_cashier(user):
return user.is_authenticated and hasattr(user, 'profile') and user.profile.role == 'cashier'
def manager_required(view_func):
return user_passes_test(is_manager, login_url='login')(view_func)
def cashier_or_manager_required(view_func):
return user_passes_test(lambda u: is_manager(u) or is_cashier(u), login_url='login')(view_func)
def login_view(request):
if request.method == 'POST':
form = AuthenticationForm(request, data=request.POST)
if form.is_valid():
user = form.get_user()
login(request, user)
if is_manager(user):
return redirect('dashboard')
return redirect('pos')
else:
form = AuthenticationForm()
return render(request, 'core/login.html', {'form': form})
def logout_view(request):
logout(request)
return redirect('login')
def home(request): def home(request):
"""Render the landing screen with loader and environment details.""" """Redirect home to login or dashboard/pos based on role."""
host_name = request.get_host().lower() if request.user.is_authenticated:
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" if is_manager(request.user):
now = timezone.now() return redirect('dashboard')
return redirect('pos')
return render(request, "core/index.html")
context = { @cashier_or_manager_required
"project_name": "New Style", def pos_view(request):
"agent_brand": agent_brand, """Cashier POS interface."""
"django_version": django_version(), menu_items = MenuItem.objects.filter(is_active=True)
"python_version": platform.python_version(), return render(request, "core/pos.html", {"menu_items": menu_items})
"current_time": now,
"host_name": host_name, @cashier_or_manager_required
"project_description": os.getenv("PROJECT_DESCRIPTION", ""), def create_order(request):
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), """Handle order creation and stock deduction."""
} if request.method == "POST":
return render(request, "core/index.html", context) try:
data = json.loads(request.body)
cart = data.get("cart", [])
notes = data.get("notes", "")
if not cart:
return JsonResponse({"success": False, "error": "Cart is empty"}, status=400)
with transaction.atomic():
order = Order.objects.create(customer_notes=notes)
total_price = 0
for item in cart:
menu_item = get_object_or_404(MenuItem, id=item["id"])
quantity = int(item["quantity"])
# Deduct stock
for recipe_item in menu_item.ingredients.all():
required_qty = recipe_item.quantity_required * quantity
ingredient = recipe_item.ingredient
if ingredient.stock_quantity < required_qty:
raise Exception(f"Insufficient stock for {ingredient.name}")
ingredient.stock_quantity = F('stock_quantity') - required_qty
ingredient.save()
# Create OrderItem
OrderItem.objects.create(
order=order,
menu_item=menu_item,
quantity=quantity,
price_at_order=menu_item.price
)
total_price += menu_item.price * quantity
order.total_price = total_price
order.save()
return JsonResponse({"success": True, "order_number": order.order_number})
except Exception as e:
return JsonResponse({"success": False, "error": str(e)}, status=400)
return JsonResponse({"success": False, "error": "Invalid request"}, status=405)
@cashier_or_manager_required
def receipt_view(request, order_number):
"""Printable receipt view."""
order = get_object_or_404(Order, order_number=order_number)
return render(request, "core/receipt.html", {"order": order})
@manager_required
def dashboard_view(request):
"""Manager dashboard for stock and reports."""
ingredients = Ingredient.objects.all()
# Summary of last 30 orders
orders = Order.objects.prefetch_related('items__menu_item').order_by('-created_at')[:50]
total_sales = Order.objects.aggregate(Sum('total_price'))['total_price__sum'] or 0
return render(request, "core/dashboard.html", {
"ingredients": ingredients,
"orders": orders,
"total_sales": total_sales,
})
@manager_required
def manage_users(request):
"""Manager only: Create and view accounts."""
users = User.objects.all().select_related('profile')
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
role = request.POST.get('role')
if username and password and role:
if User.objects.filter(username=username).exists():
messages.error(request, f"User {username} already exists.")
else:
user = User.objects.create_user(username=username, password=password)
profile, created = UserProfile.objects.get_or_create(user=user)
profile.role = role
profile.save()
messages.success(request, f"User {username} created as {role}.")
return redirect('manage_users')
else:
messages.error(request, "Please fill all fields.")
return render(request, 'core/user_management.html', {'users': users})