Compare commits

..

3 Commits

Author SHA1 Message Date
Flatlogic Bot
02ebc63050 Ahmed semifinal version 2026-02-09 17:24:55 +00:00
Flatlogic Bot
70f713fec0 Ahmed version 2 2026-02-09 16:59:02 +00:00
Flatlogic Bot
eaeab96245 Ahmed version 1 2026-02-09 16:33:02 +00:00
21 changed files with 1774 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,225 @@
{% 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>{% block title %}Searing Sandwiches | POS & Stock{% endblock %}</title>
<meta name="description" content="{{ project_description }}"> <!-- Fonts -->
<meta property="og:description" content="{{ project_description }}"> <link rel="preconnect" href="https://fonts.googleapis.com">
<meta property="twitter:description" content="{{ project_description }}"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
{% endif %} <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
{% if project_image_url %} <!-- Bootstrap CSS -->
<meta property="og:image" content="{{ project_image_url }}"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<meta property="twitter:image" content="{{ project_image_url }}"> <!-- Bootstrap Icons -->
{% endif %} <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
{% load static %}
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}"> <style>
{% block head %}{% endblock %} :root {
--primary-vibrant: #FF4500;
--primary-soft: #FFF5F2;
--secondary-vibrant: #00D1B2;
--dark-neutral: #1A1D23;
--light-neutral: #F8F9FA;
--glass-bg: rgba(255, 255, 255, 0.8);
--border-soft: rgba(0, 0, 0, 0.08);
}
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background-color: var(--light-neutral);
color: var(--dark-neutral);
min-height: 100vh;
overflow-x: hidden;
}
/* Modern Navbar */
.navbar {
background-color: var(--glass-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border-soft);
padding: 0.85rem 0;
transition: all 0.3s ease;
}
.navbar-brand {
font-weight: 800;
letter-spacing: -1.5px;
font-size: 1.6rem;
}
.brand-accent {
color: var(--primary-vibrant);
}
.nav-link {
font-weight: 600;
color: var(--dark-neutral) !important;
margin: 0 0.5rem;
transition: color 0.2s;
}
.nav-link:hover {
color: var(--primary-vibrant) !important;
}
/* Buttons */
.btn-vibrant {
background-color: var(--primary-vibrant);
color: white;
border-radius: 12px;
padding: 0.6rem 1.5rem;
font-weight: 700;
border: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 12px rgba(255, 69, 0, 0.2);
}
.btn-vibrant:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(255, 69, 0, 0.3);
color: white;
background-color: #E63E00;
}
.btn-outline-vibrant {
border: 2px solid var(--primary-vibrant);
color: var(--primary-vibrant);
border-radius: 12px;
padding: 0.5rem 1.4rem;
font-weight: 700;
transition: all 0.2s;
}
.btn-outline-vibrant:hover {
background-color: var(--primary-vibrant);
color: white;
}
/* Cards & Containers */
.card {
border: 1px solid var(--border-soft);
border-radius: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.03);
transition: transform 0.3s ease, box-shadow 0.3s ease;
background-color: white;
}
.card:hover {
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.06);
}
.section-title {
font-weight: 800;
letter-spacing: -1px;
margin-bottom: 1.5rem;
}
/* Utility Classes */
.text-vibrant { color: var(--primary-vibrant); }
.bg-soft { background-color: var(--primary-soft); }
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in {
animation: fadeIn 0.5s ease forwards;
}
/* Custom Scrollbar */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: var(--light-neutral); }
::-webkit-scrollbar-thumb { background: #D1D5DB; border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: var(--primary-vibrant); }
/* Responsive POS Adjustments */
@media (max-width: 991.98px) {
.cart-sticky {
position: fixed !important;
bottom: 0;
left: 0;
right: 0;
top: auto !important;
z-index: 1030;
max-height: 60vh !important;
border-radius: 24px 24px 0 0 !important;
box-shadow: 0 -10px 40px rgba(0,0,0,0.1) !important;
}
}
</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" href="{% url 'home' %}">
<span class="brand-accent">SEARING</span> SANDWICHES
</a>
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="bi bi-list fs-1"></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 and user.profile.role == 'manager' %}
<li class="nav-item">
<a class="nav-link" href="{% url 'dashboard' %}">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'manage_menu' %}">Menu</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'manage_users' %}">Users</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" href="{% url 'pos' %}">POS</a>
</li>
<li class="nav-item dropdown ms-lg-3">
<a class="nav-link dropdown-toggle d-flex align-items-center" href="#" role="button" data-bs-toggle="dropdown">
<div class="bg-soft rounded-circle p-2 me-2 d-inline-flex">
<i class="bi bi-person text-vibrant"></i>
</div>
{{ user.username }}
</a>
<ul class="dropdown-menu dropdown-menu-end border-0 shadow-lg rounded-4 p-2">
<li><a class="dropdown-item rounded-3 py-2" href="{% url 'logout' %}"><i class="bi bi-box-arrow-right me-2"></i>Logout</a></li>
</ul>
</li>
{% else %}
<li class="nav-item">
<a href="{% url 'login' %}" class="btn btn-vibrant px-4">Login</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<div class="container mt-3">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{% if message.tags == 'error' %}danger{% else %}primary{% endif %} border-0 shadow-sm rounded-4 alert-dismissible fade show" role="alert">
<i class="bi {% if message.tags == 'error' %}bi-exclamation-circle{% else %}bi-check-circle{% endif %} me-2"></i>
{{ message }}
<button type="button" class="btn-close" data-bs-alert="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
</div>
<main class="py-4">
<div class="container">
{% block content %}{% endblock %}
</div>
</main>
<!-- Bootstrap Bundle JS -->
<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,161 @@
{% extends 'base.html' %}
{% block content %}
<div class="animate-fade-in">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 gap-3">
<div>
<h1 class="section-title mb-1">Manager Dashboard</h1>
<p class="text-secondary mb-0">Overview of your restaurant's performance and stock.</p>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-vibrant" data-bs-toggle="modal" data-bs-target="#addIngredientModal">
<i class="bi bi-plus-circle me-1"></i> Add Ingredient
</button>
<a href="{% url 'manage_menu' %}" class="btn btn-outline-dark">Manage Menu</a>
<a href="{% url 'manage_users' %}" class="btn btn-outline-dark">Manage Users</a>
<a href="{% url 'pos' %}" class="btn btn-vibrant px-4">Go to POS</a>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-md-4">
<div class="card p-4 h-100 border-0 shadow-sm" style="background: linear-gradient(135deg, #FF4500 0%, #FF7A45 100%); color: white;">
<h5 class="opacity-75 mb-3 fw-bold text-uppercase small letter-spacing-1">Total Sales (All Time)</h5>
<h2 class="display-5 fw-bold mb-0">{{ total_sales|stringformat:".2f" }} <small class="fs-4">EGP</small></h2>
<div class="mt-4">
<span class="badge bg-white bg-opacity-25 rounded-pill px-3 py-2"><i class="bi bi-graph-up me-1"></i> Live tracking</span>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card p-4 h-100 border-0 shadow-sm">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="fw-bold mb-0">Ingredient Stock Levels</h5>
<span class="small text-secondary"><i class="bi bi-info-circle me-1"></i> Updated in real-time</span>
</div>
<div class="row g-3">
{% for ingredient in ingredients %}
<div class="col-md-4">
<div class="p-3 rounded-4 border {% if ingredient.stock_quantity < 500 %}bg-danger bg-opacity-10 border-danger border-opacity-25{% else %}bg-light border-light{% endif %} h-100">
<div class="small text-secondary text-uppercase fw-bold mb-1" style="font-size: 0.7rem;">{{ ingredient.name }}</div>
<div class="h3 mb-0 fw-bold {% if ingredient.stock_quantity < 500 %}text-danger{% endif %}">
{{ ingredient.stock_quantity|floatformat:0 }}
<small class="fs-6 fw-normal text-secondary">{{ ingredient.unit }}</small>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-bold mb-0">Recent Orders</h4>
<button class="btn btn-sm btn-light rounded-pill px-3">View All</button>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="bg-light">
<tr class="text-secondary">
<th class="py-3 border-0 rounded-start">Order #</th>
<th class="py-3 border-0">Time</th>
<th class="py-3 border-0">Items</th>
<th class="py-3 border-0">Notes</th>
<th class="py-3 border-0 text-end">Total</th>
<th class="py-3 border-0 rounded-end"></th>
</tr>
</thead>
<tbody>
{% for order in orders %}
<tr>
<td class="fw-bold py-3 text-vibrant">{{ order.order_number }}</td>
<td>
<div class="fw-bold text-dark">{{ order.created_at|date:"H:i" }}</div>
<div class="small text-secondary">{{ order.created_at|date:"d M, Y" }}</div>
</td>
<td>
<div class="d-flex flex-wrap gap-1">
{% for item in order.items.all %}
<span class="badge bg-soft text-vibrant border border-vibrant border-opacity-10 fw-normal">
{{ item.quantity }}x {{ item.menu_item.name }}
</span>
{% endfor %}
</div>
</td>
<td>
<small class="text-secondary">
{% if order.customer_notes %}
<i class="bi bi-chat-left-text me-1"></i> {{ order.customer_notes|truncatechars:30 }}
{% else %}
-
{% endif %}
</small>
</td>
<td class="text-end fw-bold">{{ order.total_price|floatformat:2 }} EGP</td>
<td class="text-end">
<a href="{% url 'receipt' order.order_number %}" class="btn btn-sm btn-outline-dark rounded-pill" target="_blank">
<i class="bi bi-printer me-1"></i> Receipt
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center py-5">
<div class="py-4">
<i class="bi bi-inbox display-1 text-light"></i>
<p class="text-secondary mt-3">No orders found yet. Start by creating one in the POS.</p>
<a href="{% url 'pos' %}" class="btn btn-vibrant mt-2">Open POS</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Add Ingredient Modal -->
<div class="modal fade" id="addIngredientModal" tabindex="-1" aria-labelledby="addIngredientModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow rounded-4">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold" id="addIngredientModalLabel">Add New Ingredient</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{% url 'manage_ingredients' %}" method="post">
{% csrf_token %}
<div class="modal-body p-4">
<div class="mb-3">
<label class="form-label text-secondary small fw-bold text-uppercase">Ingredient Name</label>
<input type="text" name="name" class="form-control rounded-3" placeholder="e.g. Tomato, Beef Patty" required>
</div>
<div class="row g-3">
<div class="col-6">
<label class="form-label text-secondary small fw-bold text-uppercase">Initial Stock</label>
<input type="number" step="0.01" name="stock_quantity" class="form-control rounded-3" value="0">
</div>
<div class="col-6">
<label class="form-label text-secondary small fw-bold text-uppercase">Unit</label>
<select name="unit" class="form-select rounded-3">
<option value="grams">grams (g)</option>
<option value="ml">milliliters (ml)</option>
<option value="pieces">pieces (pcs)</option>
<option value="kg">kilograms (kg)</option>
</select>
</div>
</div>
</div>
<div class="modal-footer border-0 pt-0 p-4">
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-vibrant rounded-pill px-4 shadow-sm">Add Ingredient</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,134 @@
{% extends 'base.html' %}
{% block content %}
<div class="animate-fade-in">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 gap-3">
<div>
<h1 class="section-title mb-1">Edit Menu Item</h1>
<p class="text-secondary mb-0">Updating: <span class="fw-bold text-dark">{{ menu_item.name }}</span></p>
</div>
<div>
<a href="{% url 'manage_menu' %}" class="btn btn-outline-dark">Cancel & Go Back</a>
</div>
</div>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card border-0 shadow-sm">
<div class="card-body p-4 p-md-5">
<form method="POST">
{% csrf_token %}
<div class="row g-4">
<div class="col-md-6">
<label class="form-label small fw-bold text-secondary">Item Name</label>
<input type="text" name="name" class="form-control form-control-lg" value="{{ menu_item.name }}" required>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-secondary">Price (EGP)</label>
<input type="number" step="0.01" name="price" class="form-control form-control-lg" value="{{ menu_item.price|stringformat:'.2f' }}" required>
</div>
<div class="col-12">
<label class="form-label small fw-bold text-secondary">Description</label>
<textarea name="description" class="form-control" rows="3">{{ menu_item.description }}</textarea>
</div>
<div class="col-12">
<label class="form-label small fw-bold text-secondary">Image URL</label>
<input type="url" name="image_url" class="form-control" value="{{ menu_item.image_url|default:'' }}">
{% if menu_item.image_url %}
<div class="mt-2">
<img src="{{ menu_item.image_url }}" alt="Preview" class="rounded-3 shadow-sm" style="max-height: 100px;">
</div>
{% endif %}
</div>
<div class="col-12">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_active" id="isActive" {% if menu_item.is_active %}checked{% endif %}>
<label class="form-check-label small fw-bold text-secondary" for="isActive">Item is Active & Visible in POS</label>
</div>
</div>
<div class="col-12">
<hr class="my-4">
<h5 class="fw-bold mb-4">Recipe / Ingredients</h5>
<div id="ingredient-rows">
{% for current in current_ingredients %}
<div class="ingredient-row row g-3 mb-3 align-items-end">
<div class="col-7">
<label class="form-label small text-secondary">Ingredient</label>
<select name="ingredients" class="form-select">
<option value="">Select Ingredient</option>
{% for ing in ingredients %}
<option value="{{ ing.id }}" {% if current.ingredient.id == ing.id %}selected{% endif %}>
{{ ing.name }} ({{ ing.unit }})
</option>
{% endfor %}
</select>
</div>
<div class="col-3">
<label class="form-label small text-secondary">Qty Required</label>
<input type="number" step="0.01" name="quantities" class="form-control" value="{{ current.quantity_required|stringformat:'g' }}">
</div>
<div class="col-2">
<button type="button" class="btn btn-outline-danger w-100" onclick="this.parentElement.parentElement.remove()">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
{% empty %}
<div class="ingredient-row row g-3 mb-3 align-items-end">
<div class="col-7">
<label class="form-label small text-secondary">Ingredient</label>
<select name="ingredients" class="form-select">
<option value="">Select Ingredient</option>
{% for ing in ingredients %}
<option value="{{ ing.id }}">{{ ing.name }} ({{ ing.unit }})</option>
{% endfor %}
</select>
</div>
<div class="col-3">
<label class="form-label small text-secondary">Qty Required</label>
<input type="number" step="0.01" name="quantities" class="form-control" placeholder="0.00">
</div>
<div class="col-2">
<button type="button" class="btn btn-outline-danger w-100" onclick="this.parentElement.parentElement.remove()">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
{% endfor %}
</div>
<button type="button" class="btn btn-light rounded-pill px-4 mt-3" onclick="addIngredientRow()">
<i class="bi bi-plus-circle me-1"></i> Add Another Ingredient
</button>
</div>
<div class="col-12 mt-5">
<button type="submit" class="btn btn-vibrant btn-lg w-100 py-3">Update Menu Item</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
function addIngredientRow() {
const container = document.getElementById('ingredient-rows');
const firstRow = container.querySelector('.ingredient-row');
const newRow = firstRow.cloneNode(true);
// Clear the values
newRow.querySelectorAll('select').forEach(s => s.selectedIndex = 0);
newRow.querySelectorAll('input').forEach(i => i.value = '');
// Ensure the trash button works on the new row
newRow.querySelector('.btn-outline-danger').onclick = function() {
this.parentElement.parentElement.remove();
};
container.appendChild(newRow);
}
</script>
{% endblock %}

View File

@ -1,145 +1,62 @@
{% extends "base.html" %} {% extends 'base.html' %}
{% block title %}Welcome | Searing Sandwiches{% 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 animate-fade-in" style="min-height: 80vh;">
<div class="card"> <div class="col-lg-10 text-center">
<h1>Analyzing your requirements and generating your app…</h1> <div class="mb-4">
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes"> <span class="badge bg-soft text-vibrant px-3 py-2 rounded-pill fw-bold text-uppercase letter-spacing-1">Next-Gen POS Solution</span>
<span class="sr-only">Loading…</span> </div>
<h1 class="display-2 fw-extrabold mb-4" style="letter-spacing: -3px;">
The Smarter way to <br><span class="text-vibrant">Manage Your Kitchen.</span>
</h1>
<p class="lead mb-5 text-secondary px-lg-5 mx-lg-5">
Empower your team with a lightning-fast cashier interface, automated inventory control, and detailed sales analytics.
</p>
<div class="row g-4 justify-content-center mt-2">
<div class="col-md-5">
<div class="card h-100 p-4 border-0 shadow-lg text-start rounded-5">
<div class="item-icon mb-4 ms-0" style="width: 50px; height: 50px; font-size: 1.5rem;">
<i class="bi bi-lightning-charge-fill"></i>
</div>
<h3 class="fw-bold mb-3">Speedy Checkout</h3>
<p class="text-secondary mb-4">A touch-friendly interface designed for high-volume service. Add items, take notes, and print receipts in seconds.</p>
<a href="{% url 'pos' %}" class="btn btn-vibrant mt-auto rounded-pill px-4 py-2">
Start Selling <i class="bi bi-arrow-right ms-2"></i>
</a>
</div>
</div>
<div class="col-md-5">
<div class="card h-100 p-4 border-0 shadow-lg text-start rounded-5">
<div class="item-icon mb-4 ms-0" style="width: 50px; height: 50px; font-size: 1.5rem; background-color: #E8F5FF; color: #007BFF;">
<i class="bi bi-pie-chart-fill"></i>
</div>
<h3 class="fw-bold mb-3">Smart Insights</h3>
<p class="text-secondary mb-4">Real-time stock tracking for liver, sausage, and ingredients. Detailed sales reports to help you grow your business.</p>
<a href="{% url 'dashboard' %}" class="btn btn-outline-dark mt-auto rounded-pill px-4 py-2">
View Analytics <i class="bi bi-bar-chart-line ms-2"></i>
</a>
</div>
</div>
</div>
<div class="mt-5 pt-5 opacity-50">
<p class="small text-uppercase fw-bold letter-spacing-2 text-secondary">Trusted by modern fast-food chains</p>
<div class="d-flex justify-content-center gap-5 fs-3 text-secondary">
<i class="bi bi-fire"></i>
<i class="bi bi-cup-hot"></i>
<i class="bi bi-box-seam"></i>
<i class="bi bi-egg"></i>
</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>
<p class="runtime"> <style>
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code> .fw-extrabold { font-weight: 800; }
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code> .letter-spacing-1 { letter-spacing: 1px; }
</p> .letter-spacing-2 { letter-spacing: 2px; }
</div> .rounded-5 { border-radius: 2rem !important; }
</main> </style>
<footer>
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
</footer>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,58 @@
{% extends 'base.html' %}
{% block content %}
<div class="row justify-content-center align-items-center animate-fade-in" style="min-height: 80vh;">
<div class="col-md-5 col-lg-4">
<div class="card border-0 shadow-lg rounded-5 overflow-hidden">
<div class="card-body p-5">
<div class="text-center mb-5">
<div class="bg-soft text-vibrant d-inline-flex p-4 rounded-circle mb-4">
<i class="bi bi-shield-lock-fill display-5"></i>
</div>
<h2 class="fw-bold h1" style="letter-spacing: -1.5px;">Sign In</h2>
<p class="text-secondary">Enter your credentials to access the system</p>
</div>
<form method="post">
{% csrf_token %}
<div class="mb-4">
<label for="id_username" class="form-label text-secondary small fw-bold text-uppercase ms-1">Username</label>
<div class="input-group">
<span class="input-group-text bg-light border-0 rounded-start-4 px-3"><i class="bi bi-person text-secondary"></i></span>
<input type="text" name="username" class="form-control bg-light border-0 rounded-end-4 py-3" id="id_username" placeholder="Username" required>
</div>
</div>
<div class="mb-4">
<label for="id_password" class="form-label text-secondary small fw-bold text-uppercase ms-1">Password</label>
<div class="input-group">
<span class="input-group-text bg-light border-0 rounded-start-4 px-3"><i class="bi bi-key text-secondary"></i></span>
<input type="password" name="password" class="form-control bg-light border-0 rounded-end-4 py-3" id="id_password" placeholder="Password" required>
</div>
</div>
<div class="d-grid gap-2 mt-5">
<button type="submit" class="btn btn-vibrant btn-lg py-3 rounded-4 shadow-sm">
LOGIN <i class="bi bi-arrow-right-short ms-1 fs-4"></i>
</button>
</div>
</form>
{% if form.errors %}
<div class="alert alert-danger mt-4 border-0 rounded-4 bg-danger bg-opacity-10 text-danger small text-center" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> Invalid username or password.
</div>
{% endif %}
</div>
<div class="card-footer bg-light border-0 py-4 text-center">
<small class="text-secondary fw-bold text-uppercase letter-spacing-1">Searing Sandwiches v1.0</small>
</div>
</div>
</div>
</div>
<style>
.rounded-start-4 { border-radius: 1rem 0 0 1rem !important; }
.rounded-end-4 { border-radius: 0 1rem 1rem 0 !important; }
.rounded-4 { border-radius: 1rem !important; }
.rounded-5 { border-radius: 2rem !important; }
</style>
{% endblock %}

View File

@ -0,0 +1,161 @@
{% extends 'base.html' %}
{% block content %}
<div class="animate-fade-in">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 gap-3">
<div>
<h1 class="section-title mb-1">Menu Management</h1>
<p class="text-secondary mb-0">Create and edit your restaurant's menu items.</p>
</div>
<div>
<a href="{% url 'dashboard' %}" class="btn btn-outline-dark">Back to Dashboard</a>
</div>
</div>
<div class="row g-4">
<!-- Add Menu Item Form -->
<div class="col-lg-4">
<div class="card border-0 shadow-sm sticky-top" style="top: 20px;">
<div class="card-body p-4">
<h4 class="fw-bold mb-4">Add New Item</h4>
<form method="POST">
{% csrf_token %}
<div class="mb-3">
<label class="form-label small fw-bold text-secondary">Item Name</label>
<input type="text" name="name" class="form-control" placeholder="e.g. Classic Burger" required>
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-secondary">Price (EGP)</label>
<input type="number" step="0.01" name="price" class="form-control" placeholder="0.00" required>
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-secondary">Description</label>
<textarea name="description" class="form-control" rows="2" placeholder="Brief description..."></textarea>
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-secondary">Image URL (Optional)</label>
<input type="url" name="image_url" class="form-control" placeholder="https://...">
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_active" id="isActive" checked>
<label class="form-check-label small fw-bold text-secondary" for="isActive">Active & Visible</label>
</div>
</div>
<hr class="my-4">
<h6 class="fw-bold mb-3">Recipe / Ingredients</h6>
<div id="ingredient-rows">
<div class="ingredient-row row g-2 mb-2">
<div class="col-7">
<select name="ingredients" class="form-select form-select-sm">
<option value="">Select Ingredient</option>
{% for ing in ingredients %}
<option value="{{ ing.id }}">{{ ing.name }} ({{ ing.unit }})</option>
{% endfor %}
</select>
</div>
<div class="col-5">
<input type="number" step="0.01" name="quantities" class="form-control form-control-sm" placeholder="Qty">
</div>
</div>
</div>
<button type="button" class="btn btn-sm btn-light w-100 mt-2" onclick="addIngredientRow()">
<i class="bi bi-plus-circle me-1"></i> Add Another Ingredient
</button>
<button type="submit" class="btn btn-vibrant w-100 mt-4 py-3">Create Menu Item</button>
</form>
</div>
</div>
</div>
<!-- Menu Item List -->
<div class="col-lg-8">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<h4 class="fw-bold mb-4">Current Menu</h4>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="bg-light">
<tr class="text-secondary small">
<th class="py-3 border-0 rounded-start">Item</th>
<th class="py-3 border-0 text-center">Price</th>
<th class="py-3 border-0 text-center">Status</th>
<th class="py-3 border-0 text-end rounded-end">Actions</th>
</tr>
</thead>
<tbody>
{% for item in menu_items %}
<tr>
<td class="py-3">
<div class="d-flex align-items-center">
{% if item.image_url %}
<img src="{{ item.image_url }}" alt="{{ item.name }}" class="rounded-3 me-3" style="width: 45px; height: 45px; object-fit: cover;">
{% else %}
<div class="bg-light rounded-3 me-3 d-flex align-items-center justify-content-center" style="width: 45px; height: 45px;">
<i class="bi bi-egg-fried text-secondary"></i>
</div>
{% endif %}
<div>
<div class="fw-bold text-dark">{{ item.name }}</div>
<div class="small text-secondary">{{ item.ingredients.count }} ingredients</div>
</div>
</div>
</td>
<td class="text-center fw-bold">{{ item.price|floatformat:2 }} EGP</td>
<td class="text-center">
{% if item.is_active %}
<span class="badge bg-success bg-opacity-10 text-success rounded-pill px-3">Active</span>
{% else %}
<span class="badge bg-secondary bg-opacity-10 text-secondary rounded-pill px-3">Inactive</span>
{% endif %}
</td>
<td class="text-end">
<div class="dropdown">
<button class="btn btn-sm btn-light rounded-pill px-3" data-bs-toggle="dropdown">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end border-0 shadow-sm">
<li><a class="dropdown-item py-2" href="{% url 'edit_menu_item' item.pk %}"><i class="bi bi-pencil me-2"></i> Edit Item</a></li>
<li><a class="dropdown-item py-2" href="{% url 'toggle_menu_item_status' item.pk %}">
{% if item.is_active %}
<i class="bi bi-eye-slash me-2"></i> Deactivate
{% else %}
<i class="bi bi-eye me-2"></i> Activate
{% endif %}
</a></li>
</ul>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center py-5">
<p class="text-secondary mb-0">No menu items found. Add your first one!</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
function addIngredientRow() {
const container = document.getElementById('ingredient-rows');
const firstRow = container.querySelector('.ingredient-row');
const newRow = firstRow.cloneNode(true);
// Clear the selected option and quantity
newRow.querySelector('select').selectedIndex = 0;
newRow.querySelector('input').value = '';
container.appendChild(newRow);
}
</script>
{% endblock %}

View File

@ -0,0 +1,316 @@
{% extends 'base.html' %}
{% block title %}POS | Searing Sandwiches{% endblock %}
{% block extra_css %}
<style>
.menu-item-card {
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid var(--border-soft);
border-radius: 20px;
background: white;
height: 100%;
overflow: hidden;
}
.menu-item-card:hover {
border-color: var(--primary-vibrant);
transform: translateY(-8px);
box-shadow: 0 15px 35px rgba(255, 69, 0, 0.1);
}
.menu-item-card:active {
transform: scale(0.95);
}
.item-icon {
width: 60px;
height: 60px;
background-color: var(--primary-soft);
color: var(--primary-vibrant);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8rem;
margin: 0 auto 1rem;
transition: all 0.3s;
}
.menu-item-card:hover .item-icon {
background-color: var(--primary-vibrant);
color: white;
transform: rotate(-10deg);
}
.cart-sticky {
position: sticky;
top: 100px;
max-height: calc(100vh - 140px);
display: flex;
flex-direction: column;
border-radius: 24px;
border: 0;
box-shadow: 0 10px 40px rgba(0,0,0,0.08);
}
.cart-items-container {
overflow-y: auto;
flex-grow: 1;
padding-right: 5px;
}
.cart-item {
background: white;
border-radius: 12px;
padding: 12px;
margin-bottom: 10px;
border: 1px solid var(--border-soft);
transition: all 0.2s;
}
.cart-item:hover {
border-color: var(--primary-vibrant);
}
.checkout-footer {
background: #F8F9FA;
margin: 0 -1.5rem -1.5rem;
padding: 1.5rem;
border-radius: 0 0 24px 24px;
border-top: 1px solid var(--border-soft);
}
@media (max-width: 991.98px) {
.pos-container { margin-bottom: 350px; }
.cart-sticky {
padding: 1.5rem;
height: 320px;
top: auto;
}
}
</style>
{% endblock %}
{% block content %}
<div class="pos-container animate-fade-in">
<div class="row g-4">
<!-- Menu Section -->
<div class="col-lg-8">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-4 gap-3">
<div>
<h2 class="section-title mb-1">Our Menu</h2>
<p class="text-secondary mb-0">Select items to start a new order</p>
</div>
<div class="input-group" style="max-width: 300px;">
<span class="input-group-text bg-white border-end-0"><i class="bi bi-search text-secondary"></i></span>
<input type="text" class="form-control border-start-0" placeholder="Search menu...">
</div>
</div>
<div class="row g-3">
{% for item in menu_items %}
<div class="col-6 col-md-4">
<div class="card menu-item-card p-3 shadow-sm" onclick="addToCart({{ item.id }}, '{{ item.name }}', {{ item.price }})">
<div class="card-body text-center p-2">
<div class="item-icon">
<i class="bi bi-egg-fried"></i>
</div>
<h5 class="fw-bold mb-1 text-truncate">{{ item.name }}</h5>
<p class="text-vibrant fw-bold mb-0 fs-5">{{ item.price|floatformat:2 }} <small class="fw-normal small">EGP</small></p>
</div>
</div>
</div>
{% empty %}
<div class="col-12 text-center py-5">
<div class="card border-0 bg-transparent">
<i class="bi bi-shop display-1 text-light"></i>
<h4 class="mt-3 text-secondary">No active menu items.</h4>
<p>Please add items in the admin panel.</p>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Cart Section -->
<div class="col-lg-4">
<div class="card cart-sticky p-4 h-100">
<div class="d-flex align-items-center mb-4">
<div class="bg-soft text-vibrant p-2 rounded-3 me-3">
<i class="bi bi-bag-check-fill fs-4"></i>
</div>
<h4 class="mb-0 fw-bold">Current Order</h4>
</div>
<div class="cart-items-container" id="cart-items-wrapper">
<div id="cart-items">
<div class="text-center py-5 text-secondary" id="empty-cart-msg">
<i class="bi bi-cart-x fs-1 opacity-25 d-block mb-3"></i>
Your cart is empty
</div>
</div>
</div>
<div class="mt-3">
<label class="form-label small text-secondary fw-bold text-uppercase">Special Instructions</label>
<input type="text" id="customer-notes" class="form-control rounded-3 py-2 border-soft" placeholder="e.g. No onions, extra spicy">
</div>
<div class="checkout-footer mt-4">
<div class="d-flex justify-content-between mb-2">
<span class="text-secondary fw-medium">Subtotal</span>
<span id="subtotal" class="fw-bold">0.00 EGP</span>
</div>
<div class="d-flex justify-content-between align-items-center pt-2 mb-4">
<h4 class="fw-bold mb-0">Total</h4>
<h3 class="fw-bold mb-0 text-vibrant" id="total">0.00 EGP</h3>
</div>
<button class="btn btn-vibrant w-100 py-3 fw-bold text-uppercase" onclick="completeOrder()" id="checkout-btn" disabled>
Place Order <i class="bi bi-arrow-right-short ms-1 fs-4"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Success Modal -->
<div class="modal fade" id="successModal" data-bs-backdrop="static" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 rounded-5 overflow-hidden">
<div class="card border-0 w-100">
<div class="card-body text-center py-5">
<div class="mb-4">
<div class="d-inline-flex bg-success bg-opacity-10 text-success p-4 rounded-circle">
<i class="bi bi-check2-circle display-1"></i>
</div>
</div>
<h2 class="fw-bold mb-2">Order Confirmed!</h2>
<p class="text-secondary mb-4 px-4" id="order-confirm-msg"></p>
<div class="d-grid gap-3 px-4">
<a id="print-receipt-btn" href="#" class="btn btn-vibrant py-3 fw-bold" target="_blank">
<i class="bi bi-printer me-2"></i> PRINT RECEIPT
</a>
<button type="button" class="btn btn-light py-2 rounded-3 text-secondary" onclick="location.reload()">NEW ORDER</button>
</div>
</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 animate-fade-in';
itemDiv.innerHTML = `
<div class="flex-grow-1">
<div class="fw-bold text-dark">${item.name}</div>
<div class="small text-secondary">${item.price.toFixed(2)} x ${item.quantity}</div>
</div>
<div class="d-flex align-items-center gap-3">
<span class="fw-bold text-vibrant">${(item.price * item.quantity).toFixed(2)}</span>
<div class="btn-group btn-group-sm rounded-pill overflow-hidden border">
<button class="btn btn-light px-2" onclick="removeFromCart(${id})"><i class="bi bi-dash"></i></button>
<button class="btn btn-white px-2 disabled fw-bold">${item.quantity}</button>
<button class="btn btn-light px-2" onclick="addToCart(${id}, '${item.name}', ${item.price})"><i class="bi bi-plus"></i></button>
</div>
</div>
`;
cartItemsDiv.appendChild(itemDiv);
}
if (itemCount > 0) {
if (emptyMsg) emptyMsg.style.display = 'none';
checkoutBtn.disabled = false;
} else {
cartItemsDiv.appendChild(emptyMsg || createEmptyMsg());
if (emptyMsg) emptyMsg.style.display = 'block';
checkoutBtn.disabled = true;
}
document.getElementById('subtotal').innerText = total.toFixed(2) + ' EGP';
document.getElementById('total').innerText = total.toFixed(2) + ' EGP';
}
function createEmptyMsg() {
const div = document.createElement('div');
div.id = 'empty-cart-msg';
div.className = 'text-center py-5 text-secondary';
div.innerHTML = '<i class="bi bi-cart-x fs-1 opacity-25 d-block mb-3"></i>Your cart is empty';
return div;
}
async function completeOrder() {
const btn = document.getElementById('checkout-btn');
const notes = document.getElementById('customer-notes').value;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span> 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.';
document.getElementById('print-receipt-btn').href = '/receipt/' + result.order_number + '/';
const modalElement = document.getElementById('successModal');
const modal = new bootstrap.Modal(modalElement);
modal.show();
} else {
alert('Error: ' + result.error);
btn.disabled = false;
btn.innerHTML = 'Place Order <i class="bi bi-arrow-right-short ms-1 fs-4"></i>';
}
} catch (error) {
alert('Something went wrong. Please check stock levels.');
btn.disabled = false;
btn.innerHTML = 'Place Order <i class="bi bi-arrow-right-short ms-1 fs-4"></i>';
}
}
</script>
{% endblock %}

View File

@ -0,0 +1,58 @@
<!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="location.href='{% url 'pos' %}'">Back to POS</button>
</div>
<div class="text-center header">
<h2 style="margin-bottom: 0;">SEARING SANDWICHES</h2>
<p style="margin-top: 5px;">Quality Fresh Food</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|floatformat:2 }}</span>
</div>
{% endfor %}
</div>
<div class="footer">
<div class="item total">
<span>TOTAL</span>
<span>{{ order.total_price|floatformat:2 }} 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 FOR YOUR ORDER!</p>
<p style="font-size: 0.8em;">Visit us again soon</p>
</div>
</body>
</html>

View File

@ -0,0 +1,128 @@
{% extends 'base.html' %}
{% block content %}
<div class="py-4 animate-fade-in">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 gap-3">
<div>
<h1 class="section-title mb-1">User Management</h1>
<p class="text-secondary mb-0">Create and manage access for your team members.</p>
</div>
<a href="{% url 'dashboard' %}" class="btn btn-outline-dark rounded-pill">
<i class="bi bi-arrow-left me-2"></i> Back to Dashboard
</a>
</div>
<div class="row g-4">
<!-- Create User Form -->
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm rounded-4">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-4">
<div class="bg-soft text-vibrant p-2 rounded-3 me-3">
<i class="bi bi-person-plus-fill fs-4"></i>
</div>
<h3 class="fw-bold mb-0">Add User</h3>
</div>
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label class="form-label text-secondary small fw-bold text-uppercase ms-1">Username</label>
<input type="text" name="username" class="form-control rounded-3 py-2" placeholder="Enter username" required>
</div>
<div class="mb-3">
<label class="form-label text-secondary small fw-bold text-uppercase ms-1">Password</label>
<input type="password" name="password" class="form-control rounded-3 py-2" placeholder="Enter password" required>
</div>
<div class="mb-4">
<label class="form-label text-secondary small fw-bold text-uppercase ms-1">Access Role</label>
<select name="role" class="form-select rounded-3 py-2" required>
<option value="cashier" selected>Cashier (POS Only)</option>
<option value="manager">Manager (Full Access)</option>
</select>
</div>
<div class="d-grid mt-4">
<button type="submit" class="btn btn-vibrant py-3 rounded-4 shadow-sm">
<i class="bi bi-plus-lg me-1"></i> CREATE ACCOUNT
</button>
</div>
</form>
</div>
</div>
</div>
<!-- User List -->
<div class="col-md-8">
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-bold mb-0">System Accounts</h4>
<span class="badge bg-light text-dark rounded-pill px-3">{{ users|length }} Total Users</span>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="bg-light">
<tr class="text-secondary">
<th class="py-3 border-0 rounded-start">User</th>
<th class="py-3 border-0">Role</th>
<th class="py-3 border-0">Joined Date</th>
<th class="py-3 border-0 rounded-end"></th>
</tr>
</thead>
<tbody>
{% for u in users %}
<tr>
<td class="py-3">
<div class="d-flex align-items-center">
<div class="bg-light rounded-circle p-2 me-3">
<i class="bi bi-person-circle fs-4 text-secondary"></i>
</div>
<span class="fw-bold text-dark">{{ u.username }}</span>
</div>
</td>
<td>
{% if u.profile and u.profile.role == 'manager' %}
<span class="badge bg-primary bg-opacity-10 text-primary border border-primary border-opacity-10 px-3 py-2 rounded-pill">
<i class="bi bi-shield-check me-1"></i> Manager
</span>
{% else %}
<span class="badge bg-info bg-opacity-10 text-info border border-info border-opacity-10 px-3 py-2 rounded-pill">
<i class="bi bi-cart me-1"></i> Cashier
</span>
{% endif %}
</td>
<td class="text-secondary">
<div class="fw-medium">{{ u.date_joined|date:"M d, Y" }}</div>
<small class="opacity-50">at {{ u.date_joined|date:"H:i" }}</small>
</td>
<td class="text-end">
{% if u.id != request.user.id %}
<a href="{% url 'delete_user' u.id %}" class="btn btn-sm btn-outline-danger rounded-circle mx-1"
onclick="return confirm('Are you sure you want to delete user {{ u.username }}?')" title="Delete User">
<i class="bi bi-trash"></i>
</a>
{% endif %}
<button class="btn btn-sm btn-light rounded-circle" title="Edit User"><i class="bi bi-pencil"></i></button>
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center py-5 text-secondary">
No users found.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.rounded-4 { border-radius: 1rem !important; }
</style>
{% endblock %}

View File

@ -1,7 +1,18 @@
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'),
path('delete-user/<int:user_id>/', views.delete_user, name='delete_user'),
path('manage-ingredients/', views.manage_ingredients, name='manage_ingredients'),
path('manage-menu/', views.manage_menu, name='manage_menu'),
path('edit-menu-item/<int:pk>/', views.edit_menu_item, name='edit_menu_item'),
path('toggle-menu-item/<int:pk>/', views.toggle_menu_item_status, name='toggle_menu_item_status'),
] ]

View File

@ -1,25 +1,288 @@
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})
@manager_required
def delete_user(request, user_id):
"""Manager only: Delete a user account."""
if request.user.id == user_id:
messages.error(request, "You cannot delete your own account.")
return redirect('manage_users')
user = get_object_or_404(User, id=user_id)
username = user.username
user.delete()
messages.success(request, f"User {username} has been deleted.")
return redirect('manage_users')
@manager_required
def manage_ingredients(request):
"""Manager only: View and add ingredients."""
if request.method == 'POST':
name = request.POST.get('name')
stock_quantity = request.POST.get('stock_quantity', 0)
unit = request.POST.get('unit', 'grams')
if name:
if Ingredient.objects.filter(name__iexact=name).exists():
messages.error(request, f"Ingredient '{name}' already exists.")
else:
Ingredient.objects.create(
name=name,
stock_quantity=stock_quantity,
unit=unit
)
messages.success(request, f"Ingredient '{name}' added successfully.")
else:
messages.error(request, "Ingredient name is required.")
return redirect('dashboard')
return redirect('dashboard')
@manager_required
def manage_menu(request):
"""Manager only: View and create menu items."""
menu_items = MenuItem.objects.all()
ingredients = Ingredient.objects.all()
if request.method == 'POST':
name = request.POST.get('name')
price = request.POST.get('price')
description = request.POST.get('description', '')
image_url = request.POST.get('image_url', '')
is_active = request.POST.get('is_active') == 'on'
# Handle ingredient IDs and quantities from the form
ingredient_ids = request.POST.getlist('ingredients')
quantities = request.POST.getlist('quantities')
if name and price:
try:
with transaction.atomic():
menu_item = MenuItem.objects.create(
name=name,
price=price,
description=description,
image_url=image_url,
is_active=is_active
)
# Add ingredients if provided
for i_id, qty in zip(ingredient_ids, quantities):
if i_id and qty and float(qty) > 0:
ingredient = get_object_or_404(Ingredient, id=i_id)
MenuItemIngredient.objects.create(
menu_item=menu_item,
ingredient=ingredient,
quantity_required=qty
)
messages.success(request, f"Menu item '{name}' created successfully.")
return redirect('manage_menu')
except Exception as e:
messages.error(request, f"Error creating menu item: {str(e)}")
else:
messages.error(request, "Name and price are required.")
return render(request, 'core/menu_management.html', {
'menu_items': menu_items,
'ingredients': ingredients
})
@manager_required
def edit_menu_item(request, pk):
"""Manager only: Edit an existing menu item."""
menu_item = get_object_or_404(MenuItem, pk=pk)
ingredients = Ingredient.objects.all()
if request.method == 'POST':
menu_item.name = request.POST.get('name')
menu_item.price = request.POST.get('price')
menu_item.description = request.POST.get('description', '')
menu_item.image_url = request.POST.get('image_url', '')
menu_item.is_active = request.POST.get('is_active') == 'on'
ingredient_ids = request.POST.getlist('ingredients')
quantities = request.POST.getlist('quantities')
if menu_item.name and menu_item.price:
try:
with transaction.atomic():
menu_item.save()
# Update ingredients: Clear existing and re-add
menu_item.ingredients.all().delete()
for i_id, qty in zip(ingredient_ids, quantities):
if i_id and qty and float(qty) > 0:
ingredient = get_object_or_404(Ingredient, id=i_id)
MenuItemIngredient.objects.create(
menu_item=menu_item,
ingredient=ingredient,
quantity_required=qty
)
messages.success(request, f"Menu item '{menu_item.name}' updated successfully.")
return redirect('manage_menu')
except Exception as e:
messages.error(request, f"Error updating menu item: {str(e)}")
else:
messages.error(request, "Name and price are required.")
return render(request, 'core/edit_menu_item.html', {
'menu_item': menu_item,
'ingredients': ingredients,
'current_ingredients': menu_item.ingredients.all()
})
@manager_required
def toggle_menu_item_status(request, pk):
"""Manager only: Toggle menu item active/inactive status."""
menu_item = get_object_or_404(MenuItem, pk=pk)
menu_item.is_active = not menu_item.is_active
menu_item.save()
status = "activated" if menu_item.is_active else "deactivated"
messages.success(request, f"Menu item '{menu_item.name}' {status}.")
return redirect('manage_menu')