making all screens fit
This commit is contained in:
parent
0ae32328a7
commit
473f13fb08
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -6,7 +6,8 @@ from .models import (
|
||||
Quotation, QuotationItem,
|
||||
SaleReturn, SaleReturnItem,
|
||||
PurchaseReturn, PurchaseReturnItem,
|
||||
SystemSetting, PaymentMethod
|
||||
SystemSetting, PaymentMethod, HeldSale,
|
||||
LoyaltyTier, LoyaltyTransaction
|
||||
)
|
||||
|
||||
@admin.register(Category)
|
||||
@ -26,7 +27,7 @@ class ProductAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(Customer)
|
||||
class CustomerAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'phone', 'email')
|
||||
list_display = ('name', 'phone', 'email', 'loyalty_points', 'loyalty_tier')
|
||||
search_fields = ('name', 'phone')
|
||||
|
||||
@admin.register(Supplier)
|
||||
@ -71,4 +72,18 @@ class PurchaseReturnAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(SystemSetting)
|
||||
class SystemSettingAdmin(admin.ModelAdmin):
|
||||
list_display = ('business_name', 'phone', 'email', 'vat_number')
|
||||
list_display = ('business_name', 'phone', 'email', 'vat_number')
|
||||
|
||||
@admin.register(HeldSale)
|
||||
class HeldSaleAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'customer', 'total_amount', 'created_at')
|
||||
|
||||
@admin.register(LoyaltyTier)
|
||||
class LoyaltyTierAdmin(admin.ModelAdmin):
|
||||
list_display = ('name_en', 'name_ar', 'min_points', 'point_multiplier', 'discount_percentage')
|
||||
|
||||
@admin.register(LoyaltyTransaction)
|
||||
class LoyaltyTransactionAdmin(admin.ModelAdmin):
|
||||
list_display = ('customer', 'transaction_type', 'points', 'created_at')
|
||||
list_filter = ('transaction_type', 'created_at')
|
||||
search_fields = ('customer__name',)
|
||||
|
||||
@ -0,0 +1,78 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-02 16:46
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0013_heldsale'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='LoyaltyTier',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name_en', models.CharField(max_length=50, verbose_name='Name (English)')),
|
||||
('name_ar', models.CharField(max_length=50, verbose_name='Name (Arabic)')),
|
||||
('min_points', models.PositiveIntegerField(default=0, verbose_name='Minimum Points')),
|
||||
('point_multiplier', models.DecimalField(decimal_places=2, default=1.0, max_digits=4, verbose_name='Point Multiplier')),
|
||||
('discount_percentage', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='Discount Percentage')),
|
||||
('color_code', models.CharField(default='#6c757d', max_length=20, verbose_name='Color Code')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='loyalty_points',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='Loyalty Points'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sale',
|
||||
name='loyalty_discount_amount',
|
||||
field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Loyalty Discount'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sale',
|
||||
name='loyalty_points_redeemed',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='Loyalty Points Redeemed'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='systemsetting',
|
||||
name='currency_per_point',
|
||||
field=models.DecimalField(decimal_places=3, default=0.01, max_digits=10, verbose_name='Currency Value per Point'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='systemsetting',
|
||||
name='loyalty_enabled',
|
||||
field=models.BooleanField(default=False, verbose_name='Enable Loyalty System'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='systemsetting',
|
||||
name='min_points_to_redeem',
|
||||
field=models.PositiveIntegerField(default=100, verbose_name='Minimum Points to Redeem'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='systemsetting',
|
||||
name='points_per_currency',
|
||||
field=models.DecimalField(decimal_places=2, default=1.0, max_digits=10, verbose_name='Points Earned per Currency Unit'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='loyalty_tier',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customers', to='core.loyaltytier'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LoyaltyTransaction',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('transaction_type', models.CharField(choices=[('earned', 'Earned'), ('redeemed', 'Redeemed'), ('adjusted', 'Adjusted')], max_length=20, verbose_name='Type')),
|
||||
('points', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='Points')),
|
||||
('notes', models.TextField(blank=True, verbose_name='Notes')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='loyalty_transactions', to='core.customer')),
|
||||
('sale', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='loyalty_transactions', to='core.sale')),
|
||||
],
|
||||
),
|
||||
]
|
||||
26
core/migrations/0015_userprofile.py
Normal file
26
core/migrations/0015_userprofile.py
Normal file
@ -0,0 +1,26 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-02 16:58
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0014_loyaltytier_customer_loyalty_points_and_more'),
|
||||
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')),
|
||||
('image', models.ImageField(blank=True, null=True, upload_to='profile_pics/', verbose_name='Profile Picture')),
|
||||
('phone', models.CharField(blank=True, max_length=20, verbose_name='Phone Number')),
|
||||
('bio', models.TextField(blank=True, verbose_name='Bio')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
Binary file not shown.
BIN
core/migrations/__pycache__/0015_userprofile.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0015_userprofile.cpython-311.pyc
Normal file
Binary file not shown.
@ -2,6 +2,8 @@ from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
class Category(models.Model):
|
||||
name_en = models.CharField(_("Name (English)"), max_length=100)
|
||||
@ -42,15 +44,50 @@ class Product(models.Model):
|
||||
def __str__(self):
|
||||
return f"{self.name_en} ({self.sku})"
|
||||
|
||||
class LoyaltyTier(models.Model):
|
||||
name_en = models.CharField(_("Name (English)"), max_length=50)
|
||||
name_ar = models.CharField(_("Name (Arabic)"), max_length=50)
|
||||
min_points = models.PositiveIntegerField(_("Minimum Points"), default=0)
|
||||
point_multiplier = models.DecimalField(_("Point Multiplier"), max_digits=4, decimal_places=2, default=1.0)
|
||||
discount_percentage = models.DecimalField(_("Discount Percentage"), max_digits=5, decimal_places=2, default=0)
|
||||
color_code = models.CharField(_("Color Code"), max_length=20, default="#6c757d")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name_en} / {self.name_ar}"
|
||||
|
||||
class Customer(models.Model):
|
||||
name = models.CharField(_("Name"), max_length=200)
|
||||
phone = models.CharField(_("Phone"), max_length=20, blank=True)
|
||||
email = models.EmailField(_("Email"), blank=True)
|
||||
address = models.TextField(_("Address"), blank=True)
|
||||
loyalty_points = models.DecimalField(_("Loyalty Points"), max_digits=15, decimal_places=2, default=0)
|
||||
loyalty_tier = models.ForeignKey(LoyaltyTier, on_delete=models.SET_NULL, null=True, blank=True, related_name="customers")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def update_tier(self):
|
||||
tiers = LoyaltyTier.objects.filter(min_points__lte=self.loyalty_points).order_by('-min_points')
|
||||
if tiers.exists():
|
||||
self.loyalty_tier = tiers.first()
|
||||
self.save()
|
||||
|
||||
class LoyaltyTransaction(models.Model):
|
||||
TRANSACTION_TYPES = [
|
||||
('earned', _('Earned')),
|
||||
('redeemed', _('Redeemed')),
|
||||
('adjusted', _('Adjusted')),
|
||||
]
|
||||
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="loyalty_transactions")
|
||||
sale = models.ForeignKey('Sale', on_delete=models.SET_NULL, null=True, blank=True, related_name="loyalty_transactions")
|
||||
transaction_type = models.CharField(_("Type"), max_length=20, choices=TRANSACTION_TYPES)
|
||||
points = models.DecimalField(_("Points"), max_digits=15, decimal_places=2)
|
||||
notes = models.TextField(_("Notes"), blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.transaction_type} {self.points} for {self.customer.name}"
|
||||
|
||||
class Supplier(models.Model):
|
||||
name = models.CharField(_("Name"), max_length=200)
|
||||
contact_person = models.CharField(_("Contact Person"), max_length=200, blank=True)
|
||||
@ -86,6 +123,8 @@ class Sale(models.Model):
|
||||
paid_amount = models.DecimalField(_("Paid Amount"), max_digits=15, decimal_places=3, default=0)
|
||||
balance_due = models.DecimalField(_("Balance Due"), max_digits=15, decimal_places=3, default=0)
|
||||
discount = models.DecimalField(_("Discount"), max_digits=15, decimal_places=3, default=0)
|
||||
loyalty_points_redeemed = models.DecimalField(_("Loyalty Points Redeemed"), max_digits=15, decimal_places=2, default=0)
|
||||
loyalty_discount_amount = models.DecimalField(_("Loyalty Discount"), max_digits=15, decimal_places=3, default=0)
|
||||
payment_type = models.CharField(_("Payment Type"), max_length=20, choices=PAYMENT_TYPE_CHOICES, default='cash')
|
||||
status = models.CharField(_("Status"), max_length=20, choices=STATUS_CHOICES, default='paid')
|
||||
due_date = models.DateField(_("Due Date"), null=True, blank=True)
|
||||
@ -290,6 +329,32 @@ class SystemSetting(models.Model):
|
||||
logo = models.ImageField(_("Logo"), upload_to="business_logos/", blank=True, null=True)
|
||||
vat_number = models.CharField(_("VAT Number"), max_length=50, blank=True)
|
||||
registration_number = models.CharField(_("Registration Number"), max_length=50, blank=True)
|
||||
|
||||
# Loyalty Settings
|
||||
loyalty_enabled = models.BooleanField(_("Enable Loyalty System"), default=False)
|
||||
points_per_currency = models.DecimalField(_("Points Earned per Currency Unit"), max_digits=10, decimal_places=2, default=1.0)
|
||||
currency_per_point = models.DecimalField(_("Currency Value per Point"), max_digits=10, decimal_places=3, default=0.010)
|
||||
min_points_to_redeem = models.PositiveIntegerField(_("Minimum Points to Redeem"), default=100)
|
||||
|
||||
def __str__(self):
|
||||
return self.business_name
|
||||
return self.business_name
|
||||
|
||||
class UserProfile(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
|
||||
image = models.ImageField(_("Profile Picture"), upload_to="profile_pics/", blank=True, null=True)
|
||||
phone = models.CharField(_("Phone Number"), max_length=20, blank=True)
|
||||
bio = models.TextField(_("Bio"), blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_user_profile(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
UserProfile.objects.create(user=instance)
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def save_user_profile(sender, instance, **kwargs):
|
||||
if not hasattr(instance, 'profile'):
|
||||
UserProfile.objects.create(user=instance)
|
||||
instance.profile.save()
|
||||
|
||||
@ -201,18 +201,39 @@
|
||||
|
||||
<div class="ms-auto d-flex align-items-center">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-light dropdown-toggle d-flex align-items-center" type="button" id="userDropdown" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-person-circle fs-5 me-2"></i>
|
||||
<button class="btn btn-light dropdown-toggle d-flex align-items-center rounded-pill px-3" type="button" id="userDropdown" data-bs-toggle="dropdown">
|
||||
{% if user.is_authenticated and user.profile.image %}
|
||||
<img src="{{ user.profile.image.url }}" alt="{{ user.username }}" class="rounded-circle me-2" style="width: 28px; height: 28px; object-fit: cover;">
|
||||
{% else %}
|
||||
<i class="bi bi-person-circle fs-5 me-2"></i>
|
||||
{% endif %}
|
||||
<span>{% if user.is_authenticated %}{{ user.username }}{% else %}{% trans "Guest" %}{% endif %}</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow border-0 mt-2">
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow-lg border-0 mt-2 rounded-4 overflow-hidden">
|
||||
{% if user.is_authenticated %}
|
||||
<li><a class="dropdown-item" href="{% url 'settings' %}"><i class="bi bi-person-gear me-2"></i> {% trans "Profile & Settings" %}</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li class="px-3 py-2 bg-light d-flex align-items-center">
|
||||
{% if user.profile.image %}
|
||||
<img src="{{ user.profile.image.url }}" alt="{{ user.username }}" class="rounded-circle me-3" style="width: 40px; height: 40px; object-fit: cover;">
|
||||
{% else %}
|
||||
<div class="bg-primary-subtle text-primary rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">
|
||||
<i class="bi bi-person-fill"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<div class="fw-bold small">{{ user.get_full_name|default:user.username }}</div>
|
||||
<div class="text-muted" style="font-size: 0.7rem;">{{ user.email|truncatechars:20 }}</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider m-0"></li>
|
||||
<li><a class="dropdown-item py-2" href="{% url 'profile' %}"><i class="bi bi-person me-2"></i> {% trans "My Profile" %}</a></li>
|
||||
{% if user.is_superuser or user.is_staff %}
|
||||
<li><a class="dropdown-item py-2" href="{% url 'settings' %}"><i class="bi bi-gear me-2"></i> {% trans "System Settings" %}</a></li>
|
||||
{% endif %}
|
||||
<li><hr class="dropdown-divider m-0"></li>
|
||||
<li>
|
||||
<form action="{% url 'logout' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="dropdown-item text-danger border-0 bg-transparent w-100 text-start">
|
||||
<button type="submit" class="dropdown-item text-danger py-2 border-0 bg-transparent w-100 text-start">
|
||||
<i class="bi bi-box-arrow-right me-2"></i> {% trans "Logout" %}
|
||||
</button>
|
||||
</form>
|
||||
@ -235,8 +256,21 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
{% if user.is_authenticated %}
|
||||
document.getElementById('sidebarCollapse').addEventListener('click', function () {
|
||||
document.getElementById('sidebar').classList.toggle('active');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const sidebarCollapse = document.getElementById('sidebarCollapse');
|
||||
|
||||
sidebarCollapse.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
sidebar.classList.toggle('active');
|
||||
});
|
||||
|
||||
// Close sidebar when clicking outside on mobile
|
||||
document.addEventListener('click', function (e) {
|
||||
if (window.innerWidth <= 992 && sidebar.classList.contains('active')) {
|
||||
if (!sidebar.contains(e.target) && e.target !== sidebarCollapse) {
|
||||
sidebar.classList.remove('active');
|
||||
}
|
||||
}
|
||||
});
|
||||
{% endif %}
|
||||
</script>
|
||||
|
||||
@ -58,6 +58,14 @@
|
||||
border-color: var(--bs-primary);
|
||||
}
|
||||
|
||||
/* Loyalty Styles */
|
||||
.loyalty-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Invoice Print Styles */
|
||||
@media print {
|
||||
body * {
|
||||
@ -114,13 +122,13 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4 no-print">
|
||||
<div class="row g-4">
|
||||
<div class="container-fluid px-2 px-md-4 no-print">
|
||||
<div class="row g-3 g-lg-4">
|
||||
<!-- Products Section -->
|
||||
<div class="col-lg-8">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-4 gap-3">
|
||||
<h4 class="fw-bold mb-0">{% trans "Point of Sale" %}</h4>
|
||||
<div class="input-group w-50">
|
||||
<div class="input-group w-100 w-md-50">
|
||||
<span class="input-group-text bg-white border-end-0"><i class="bi bi-search"></i></span>
|
||||
<input type="text" id="productSearch" class="form-control border-start-0 shadow-none" placeholder="{% trans 'Search products...' %}">
|
||||
</div>
|
||||
@ -162,9 +170,14 @@
|
||||
|
||||
<!-- Cart Section -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0 shadow-sm rounded-4 cart-container">
|
||||
<div class="card border-0 shadow-sm rounded-4 cart-container" id="posCart">
|
||||
<div class="card-header bg-white border-0 pt-4 px-4 d-flex justify-content-between align-items-center">
|
||||
<h5 class="fw-bold mb-0">{% trans "Current Order" %}</h5>
|
||||
<div class="d-flex align-items-center">
|
||||
<button class="btn btn-link d-lg-none text-dark p-0 me-3" onclick="toggleMobileCart()">
|
||||
<i class="bi bi-chevron-left fs-4"></i>
|
||||
</button>
|
||||
<h5 class="fw-bold mb-0">{% trans "Current Order" %}</h5>
|
||||
</div>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<button class="btn btn-sm btn-outline-warning shadow-none position-relative" onclick="loadHeldSales()" data-bs-toggle="modal" data-bs-target="#heldSalesModal">
|
||||
<i class="bi bi-clock-history"></i>
|
||||
@ -177,8 +190,8 @@
|
||||
</div>
|
||||
|
||||
<div class="px-4 mt-2">
|
||||
<div class="d-flex gap-2">
|
||||
<select id="customerSelect" class="form-select form-select-sm shadow-none">
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<select id="customerSelect" class="form-select form-select-sm shadow-none" onchange="onCustomerChange()">
|
||||
<option value="">{% trans "Walking Customer" %}</option>
|
||||
{% for customer in customers %}
|
||||
<option value="{{ customer.id }}">{{ customer.name }}</option>
|
||||
@ -188,6 +201,18 @@
|
||||
<i class="bi bi-person-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loyalty Info Display -->
|
||||
<div id="loyaltyInfo" class="d-none bg-light p-2 rounded-3 mb-2 animate__animated animate__fadeIn">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="small fw-bold text-muted"><i class="bi bi-star-fill text-warning me-1"></i> {% trans "Loyalty" %}</span>
|
||||
<span id="loyaltyTierBadge" class="loyalty-badge text-white"></span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mt-1">
|
||||
<span class="small text-muted">{% trans "Available Points" %}</span>
|
||||
<span id="loyaltyPointsDisplay" class="small fw-bold">0.00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body px-4 py-3 cart-items">
|
||||
@ -200,7 +225,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer bg-light border-0 p-4 rounded-bottom-4">
|
||||
<div class="card-footer bg-light border-0 p-4 rounded-bottom-4 mt-auto">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="small">{% trans "Subtotal" %}</span>
|
||||
<span id="subtotalAmount" class="small">{{ site_settings.currency_symbol }}0.000</span>
|
||||
@ -233,6 +258,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile View Cart FAB -->
|
||||
<button class="btn btn-primary mobile-cart-toggle shadow-lg" onclick="toggleMobileCart()">
|
||||
<i class="bi bi-cart-fill me-2"></i>
|
||||
<span>{% trans "View Cart" %}</span>
|
||||
<span class="badge bg-white text-primary ms-2 rounded-pill" id="mobileCartCountBadge">0</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Customer Modal -->
|
||||
@ -312,16 +344,18 @@
|
||||
<div class="modal-body p-4">
|
||||
<div class="row g-4">
|
||||
<!-- Left: Payment Types -->
|
||||
<div class="col-md-3 border-end">
|
||||
<div class="col-md-3 border-md-end">
|
||||
<label class="small fw-bold text-muted mb-2 d-block">{% trans "Payment Method" %}</label>
|
||||
<div class="d-grid gap-2" id="paymentMethodButtons">
|
||||
<div class="row g-2 row-cols-3 row-cols-md-1" id="paymentMethodButtons">
|
||||
{% for method in payment_methods %}
|
||||
<button class="btn btn-outline-primary payment-method-btn py-3 {% if forloop.first %}active{% endif %}"
|
||||
data-id="{{ method.id }}" data-name-en="{{ method.name_en|lower }}"
|
||||
onclick="selectPaymentMethod(this, '{{ method.id }}')">
|
||||
{{ method.name_ar }}<br>
|
||||
<small class="fw-normal">{{ method.name_en }}</small>
|
||||
</button>
|
||||
<div class="col">
|
||||
<button class="btn btn-outline-primary payment-method-btn w-100 py-2 py-md-3 {% if forloop.first %}active{% endif %}"
|
||||
data-id="{{ method.id }}" data-name-en="{{ method.name_en|lower }}"
|
||||
onclick="selectPaymentMethod(this, '{{ method.id }}')">
|
||||
<span class="d-md-block">{{ method.name_ar }}</span>
|
||||
<small class="fw-normal d-none d-md-block">{{ method.name_en }}</small>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
@ -333,6 +367,19 @@
|
||||
<h2 class="fw-bold mb-0 text-primary" id="modalTotalAmount">{{ site_settings.currency_symbol }}0.000</h2>
|
||||
</div>
|
||||
|
||||
<!-- Loyalty Redemption Section -->
|
||||
<div id="modalLoyaltySection" class="d-none mb-3 p-3 border rounded-4 bg-warning-soft">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="small fw-bold text-muted">{% trans "Redeem Points" %}</label>
|
||||
<span class="small">{% trans "Max" %}: <span id="modalMaxPoints" class="fw-bold">0</span></span>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input type="number" id="loyaltyRedeemInput" class="form-control shadow-none" placeholder="0" oninput="calculateLoyaltyDiscount()">
|
||||
<span class="input-group-text bg-white border-start-0 small text-success fw-bold" id="loyaltyValueDisplay">- {{ site_settings.currency_symbol }}0.000</span>
|
||||
</div>
|
||||
<small class="text-muted" style="font-size: 0.65rem;">{% trans "Points to spend for a discount" %}</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="small fw-bold text-muted mb-1">{% trans "Cash Received" %}</label>
|
||||
<input type="number" id="cashReceivedInput" class="form-control form-control-lg fw-bold text-center shadow-none"
|
||||
@ -349,11 +396,11 @@
|
||||
<div class="col-md-4">
|
||||
<label class="small fw-bold text-muted mb-2 d-block">{% trans "Quick Cash" %}</label>
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6"><button class="btn btn-light w-100 py-3 fw-bold border" onclick="addCash(1)">1</button></div>
|
||||
<div class="col-6"><button class="btn btn-light w-100 py-3 fw-bold border" onclick="addCash(5)">5</button></div>
|
||||
<div class="col-6"><button class="btn btn-light w-100 py-3 fw-bold border" onclick="addCash(10)">10</button></div>
|
||||
<div class="col-6"><button class="btn btn-light w-100 py-3 fw-bold border" onclick="addCash(20)">20</button></div>
|
||||
<div class="col-12"><button class="btn btn-light w-100 py-3 fw-bold border" onclick="addCash(50)">50</button></div>
|
||||
<div class="col-4 col-md-6"><button class="btn btn-light w-100 py-3 fw-bold border" onclick="addCash(1)">1</button></div>
|
||||
<div class="col-4 col-md-6"><button class="btn btn-light w-100 py-3 fw-bold border" onclick="addCash(5)">5</button></div>
|
||||
<div class="col-4 col-md-6"><button class="btn btn-light w-100 py-3 fw-bold border" onclick="addCash(10)">10</button></div>
|
||||
<div class="col-4 col-md-6"><button class="btn btn-light w-100 py-3 fw-bold border" onclick="addCash(20)">20</button></div>
|
||||
<div class="col-8 col-md-12"><button class="btn btn-light w-100 py-3 fw-bold border" onclick="addCash(50)">50</button></div>
|
||||
</div>
|
||||
<button class="btn btn-secondary w-100 mb-2 py-2" onclick="setExactAmount()">{% trans "Exact Amount" %}</button>
|
||||
<button class="btn btn-danger w-100 py-2" onclick="clearCash()">{% trans "Clear Cash" %}</button>
|
||||
@ -450,14 +497,22 @@
|
||||
let cart = [];
|
||||
let lastSaleData = null;
|
||||
let selectedPaymentMethodId = null;
|
||||
let customerLoyalty = null;
|
||||
const lang = '{{ LANGUAGE_CODE }}';
|
||||
const currency = '{{ site_settings.currency_symbol }}';
|
||||
const decimalPlaces = {{ decimal_places|default:3 }};
|
||||
const decimalPlaces = {{ site_settings.decimal_places|default:3 }};
|
||||
const loyaltyEnabled = {{ settings.loyalty_enabled|yesno:"true,false" }};
|
||||
|
||||
function formatAmount(amount) {
|
||||
return parseFloat(amount).toFixed(decimalPlaces);
|
||||
}
|
||||
|
||||
function toggleMobileCart() {
|
||||
const cart = document.getElementById('posCart');
|
||||
cart.classList.toggle('show');
|
||||
document.body.classList.toggle('cart-open');
|
||||
}
|
||||
|
||||
function addToCart(id, nameEn, nameAr, price) {
|
||||
const existing = cart.find(item => item.id === id);
|
||||
if (existing) {
|
||||
@ -512,7 +567,11 @@
|
||||
const emptyMsg = document.getElementById('emptyCartMsg');
|
||||
const payBtn = document.getElementById('payNowBtn');
|
||||
const holdBtn = document.getElementById('holdBtn');
|
||||
const mobileBadge = document.getElementById('mobileCartCountBadge');
|
||||
|
||||
const totalItems = cart.reduce((acc, item) => acc + item.quantity, 0);
|
||||
mobileBadge.innerText = totalItems;
|
||||
|
||||
if (cart.length === 0) {
|
||||
emptyMsg.classList.remove('d-none');
|
||||
listContainer.innerHTML = '';
|
||||
@ -559,6 +618,19 @@
|
||||
document.getElementById('cashReceivedInput').value = '';
|
||||
document.getElementById('balanceAmount').innerText = `${currency} ${formatAmount(0)}`;
|
||||
|
||||
// Reset loyalty redemption
|
||||
document.getElementById('loyaltyRedeemInput').value = '';
|
||||
document.getElementById('loyaltyValueDisplay').innerText = `- ${currency} ${formatAmount(0)}`;
|
||||
|
||||
// Show loyalty section if customer has points
|
||||
const loyaltySection = document.getElementById('modalLoyaltySection');
|
||||
if (customerLoyalty && customerLoyalty.points >= customerLoyalty.min_points_to_redeem) {
|
||||
loyaltySection.classList.remove('d-none');
|
||||
document.getElementById('modalMaxPoints').innerText = customerLoyalty.points;
|
||||
} else {
|
||||
loyaltySection.classList.add('d-none');
|
||||
}
|
||||
|
||||
// Default to first payment method
|
||||
const firstBtn = document.querySelector('.payment-method-btn');
|
||||
if (firstBtn) {
|
||||
@ -569,6 +641,57 @@
|
||||
paymentModal.show();
|
||||
}
|
||||
|
||||
function onCustomerChange() {
|
||||
const customerId = document.getElementById('customerSelect').value;
|
||||
const loyaltyInfo = document.getElementById('loyaltyInfo');
|
||||
|
||||
if (!customerId || !loyaltyEnabled) {
|
||||
loyaltyInfo.classList.add('d-none');
|
||||
customerLoyalty = null;
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/customer-loyalty/${customerId}/`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
customerLoyalty = data;
|
||||
loyaltyInfo.classList.remove('d-none');
|
||||
document.getElementById('loyaltyPointsDisplay').innerText = data.points.toFixed(2);
|
||||
|
||||
const badge = document.getElementById('loyaltyTierBadge');
|
||||
if (data.tier) {
|
||||
badge.innerText = lang === 'ar' ? data.tier.name_ar : data.tier.name_en;
|
||||
badge.style.backgroundColor = data.tier.color;
|
||||
badge.classList.remove('d-none');
|
||||
} else {
|
||||
badge.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function calculateLoyaltyDiscount() {
|
||||
if (!customerLoyalty) return;
|
||||
|
||||
let points = parseFloat(document.getElementById('loyaltyRedeemInput').value) || 0;
|
||||
if (points > customerLoyalty.points) {
|
||||
points = customerLoyalty.points;
|
||||
document.getElementById('loyaltyRedeemInput').value = points;
|
||||
}
|
||||
|
||||
const value = points * customerLoyalty.currency_per_point;
|
||||
document.getElementById('loyaltyValueDisplay').innerText = `- ${currency} ${formatAmount(value)}`;
|
||||
|
||||
// Update Total Payable
|
||||
const subtotal = cart.reduce((acc, item) => acc + item.line_total, 0);
|
||||
const discount = parseFloat(document.getElementById('discountInput').value) || 0;
|
||||
const totalAmount = Math.max(0, subtotal - discount - value);
|
||||
|
||||
document.getElementById('modalTotalAmount').innerText = `${currency} ${formatAmount(totalAmount)}`;
|
||||
calculateBalance();
|
||||
}
|
||||
|
||||
function selectPaymentMethod(btn, id) {
|
||||
document.querySelectorAll('.payment-method-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
@ -591,7 +714,10 @@
|
||||
function setExactAmount() {
|
||||
const subtotal = cart.reduce((acc, item) => acc + item.line_total, 0);
|
||||
const discount = parseFloat(document.getElementById('discountInput').value) || 0;
|
||||
const totalAmount = Math.max(0, subtotal - discount);
|
||||
const loyaltyRedeem = parseFloat(document.getElementById('loyaltyRedeemInput').value) || 0;
|
||||
const loyaltyDiscount = customerLoyalty ? loyaltyRedeem * customerLoyalty.currency_per_point : 0;
|
||||
|
||||
const totalAmount = Math.max(0, subtotal - discount - loyaltyDiscount);
|
||||
|
||||
document.getElementById('cashReceivedInput').value = totalAmount.toFixed(decimalPlaces);
|
||||
calculateBalance();
|
||||
@ -605,7 +731,10 @@
|
||||
function calculateBalance() {
|
||||
const subtotal = cart.reduce((acc, item) => acc + item.line_total, 0);
|
||||
const discount = parseFloat(document.getElementById('discountInput').value) || 0;
|
||||
const totalAmount = Math.max(0, subtotal - discount);
|
||||
const loyaltyRedeem = parseFloat(document.getElementById('loyaltyRedeemInput').value) || 0;
|
||||
const loyaltyDiscount = customerLoyalty ? loyaltyRedeem * customerLoyalty.currency_per_point : 0;
|
||||
|
||||
const totalAmount = Math.max(0, subtotal - discount - loyaltyDiscount);
|
||||
|
||||
const received = parseFloat(document.getElementById('cashReceivedInput').value) || 0;
|
||||
const balance = Math.max(0, received - totalAmount);
|
||||
@ -621,7 +750,10 @@
|
||||
|
||||
const subtotal = cart.reduce((acc, item) => acc + item.line_total, 0);
|
||||
const discount = parseFloat(document.getElementById('discountInput').value) || 0;
|
||||
const totalAmount = Math.max(0, subtotal - discount);
|
||||
const loyaltyRedeem = parseFloat(document.getElementById('loyaltyRedeemInput').value) || 0;
|
||||
const loyaltyDiscount = customerLoyalty ? loyaltyRedeem * customerLoyalty.currency_per_point : 0;
|
||||
|
||||
const totalAmount = Math.max(0, subtotal - discount - loyaltyDiscount);
|
||||
|
||||
const data = {
|
||||
customer_id: document.getElementById('customerSelect').value,
|
||||
@ -630,6 +762,7 @@
|
||||
total_amount: totalAmount,
|
||||
paid_amount: totalAmount,
|
||||
discount: discount,
|
||||
loyalty_points_redeemed: loyaltyRedeem,
|
||||
payment_type: 'cash'
|
||||
};
|
||||
|
||||
@ -659,6 +792,8 @@
|
||||
|
||||
cart = [];
|
||||
document.getElementById('discountInput').value = 0;
|
||||
customerLoyalty = null;
|
||||
document.getElementById('loyaltyInfo').classList.add('d-none');
|
||||
renderCart();
|
||||
|
||||
// Show receipt modal
|
||||
@ -745,6 +880,7 @@
|
||||
const select = document.getElementById('customerSelect');
|
||||
const option = new Option(data.name, data.id, true, true);
|
||||
select.add(option);
|
||||
onCustomerChange();
|
||||
|
||||
// Close modal
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('addCustomerModal'));
|
||||
@ -785,6 +921,9 @@
|
||||
if (data.success) {
|
||||
clearCart(false);
|
||||
updateHeldCount();
|
||||
if (document.getElementById('posCart').classList.contains('show')) {
|
||||
toggleMobileCart();
|
||||
}
|
||||
} else {
|
||||
alert('Error holding sale: ' + data.error);
|
||||
}
|
||||
@ -862,11 +1001,17 @@
|
||||
cart = data.items;
|
||||
document.getElementById('customerSelect').value = data.customer_id || "";
|
||||
renderCart();
|
||||
onCustomerChange();
|
||||
updateHeldCount();
|
||||
|
||||
// Close modal
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('heldSalesModal'));
|
||||
modal.hide();
|
||||
|
||||
// On mobile, show the cart automatically when recalled
|
||||
if (!document.getElementById('posCart').classList.contains('show') && window.innerWidth < 992) {
|
||||
toggleMobileCart();
|
||||
}
|
||||
} else {
|
||||
alert('Error recalling sale: ' + data.error);
|
||||
}
|
||||
|
||||
192
core/templates/core/profile.html
Normal file
192
core/templates/core/profile.html
Normal file
@ -0,0 +1,192 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% trans "My Profile" %} - {{ site_settings.business_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-4 align-items-center">
|
||||
<div class="col-md-6">
|
||||
<h1 class="h3 mb-0 text-gray-800">{% trans "My Profile" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage your personal information and account security." %}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if messages %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-4">
|
||||
<!-- Profile Card -->
|
||||
<div class="card shadow-sm border-0 rounded-4 glassmorphism mb-4">
|
||||
<div class="card-body text-center py-5">
|
||||
<div class="position-relative d-inline-block mb-4">
|
||||
{% if user.profile.image %}
|
||||
<img src="{{ user.profile.image.url }}" alt="{{ user.username }}" class="rounded-circle img-thumbnail shadow-sm" style="width: 150px; height: 150px; object-fit: cover;">
|
||||
{% else %}
|
||||
<div class="bg-primary-subtle text-primary rounded-circle d-flex align-items-center justify-content-center mx-auto shadow-sm" style="width: 150px; height: 150px;">
|
||||
<i class="bi bi-person-fill" style="font-size: 5rem;"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h4 class="fw-bold mb-1">{{ user.get_full_name|default:user.username }}</h4>
|
||||
<p class="text-muted mb-3">{{ user.email|default:"No email provided" }}</p>
|
||||
<div class="d-flex justify-content-center gap-2">
|
||||
{% for group in user.groups.all %}
|
||||
<span class="badge bg-primary-soft text-primary px-3 rounded-pill">{{ group.name }}</span>
|
||||
{% empty %}
|
||||
<span class="badge bg-secondary-soft text-secondary px-3 rounded-pill">{% trans "User" %}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent border-0 pb-4 px-4">
|
||||
<div class="d-flex justify-content-between small text-muted mb-2">
|
||||
<span>{% trans "Joined" %}</span>
|
||||
<span class="fw-bold text-dark">{{ user.date_joined|date:"M Y" }}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between small text-muted">
|
||||
<span>{% trans "Last Login" %}</span>
|
||||
<span class="fw-bold text-dark">{{ user.last_login|date:"d M, H:i"|default:"Never" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Security Tip -->
|
||||
<div class="card shadow-sm border-0 rounded-4 glassmorphism bg-light">
|
||||
<div class="card-body py-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="bg-warning-subtle text-warning p-2 rounded-3 me-3">
|
||||
<i class="bi bi-shield-lock fs-4"></i>
|
||||
</div>
|
||||
<h6 class="fw-bold mb-0">{% trans "Security Tip" %}</h6>
|
||||
</div>
|
||||
<p class="small text-muted mb-0">
|
||||
{% trans "Always use a strong, unique password for your account. Avoid using the same password across multiple sites." %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm border-0 rounded-4 glassmorphism">
|
||||
<div class="card-header bg-transparent border-0 py-4 px-4">
|
||||
<ul class="nav nav-tabs nav-tabs-custom border-0" id="profileTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active fw-bold px-4 border-0" id="edit-tab" data-bs-toggle="tab" data-bs-target="#edit" type="button" role="tab">
|
||||
<i class="bi bi-person-lines-fill me-2"></i>{% trans "Edit Profile" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link fw-bold px-4 border-0" id="security-tab" data-bs-toggle="tab" data-bs-target="#security" type="button" role="tab">
|
||||
<i class="bi bi-lock me-2"></i>{% trans "Security" %}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="tab-content" id="profileTabsContent">
|
||||
<!-- Edit Profile Tab -->
|
||||
<div class="tab-pane fade show active" id="edit" role="tabpanel">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="row g-4">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label small fw-bold text-uppercase text-muted">{% trans "Profile Picture" %}</label>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<input type="file" name="image" class="form-control" accept="image/*">
|
||||
</div>
|
||||
<div class="form-text">{% trans "Recommended: Square image, max 2MB." %}</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold text-uppercase text-muted">{% trans "First Name" %}</label>
|
||||
<input type="text" name="first_name" class="form-control rounded-3" value="{{ user.first_name }}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold text-uppercase text-muted">{% trans "Last Name" %}</label>
|
||||
<input type="text" name="last_name" class="form-control rounded-3" value="{{ user.last_name }}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold text-uppercase text-muted">{% trans "Email Address" %}</label>
|
||||
<input type="email" name="email" class="form-control rounded-3" value="{{ user.email }}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold text-uppercase text-muted">{% trans "Phone Number" %}</label>
|
||||
<input type="text" name="phone" class="form-control rounded-3" value="{{ user.profile.phone }}">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label small fw-bold text-uppercase text-muted">{% trans "Bio" %}</label>
|
||||
<textarea name="bio" class="form-control rounded-3" rows="3" placeholder="{% trans "A little bit about yourself..." %}">{{ user.profile.bio }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 pt-3 border-top d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-primary px-5 py-2 rounded-3 fw-bold">
|
||||
<i class="bi bi-save me-2"></i> {% trans "Update Profile" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Security Tab -->
|
||||
<div class="tab-pane fade" id="security" role="tabpanel">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="row g-4">
|
||||
<div class="col-md-12">
|
||||
<div class="alert alert-info border-0 rounded-4">
|
||||
<i class="bi bi-info-circle-fill me-2"></i>
|
||||
{% trans "Leave password fields blank if you don't want to change your password." %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold text-uppercase text-muted">{% trans "New Password" %}</label>
|
||||
<input type="password" name="password" class="form-control rounded-3" minlength="8">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold text-uppercase text-muted">{% trans "Confirm New Password" %}</label>
|
||||
<input type="password" name="confirm_password" class="form-control rounded-3" minlength="8">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 pt-3 border-top d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-primary px-5 py-2 rounded-3 fw-bold">
|
||||
<i class="bi bi-key me-2"></i> {% trans "Change Password" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<style>
|
||||
.nav-tabs-custom .nav-link {
|
||||
color: #6c757d;
|
||||
background: transparent;
|
||||
border-bottom: 3px solid transparent !important;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
.nav-tabs-custom .nav-link.active {
|
||||
color: var(--bs-primary);
|
||||
border-bottom: 3px solid var(--bs-primary) !important;
|
||||
background: transparent;
|
||||
}
|
||||
.bg-primary-soft { background-color: rgba(13, 110, 253, 0.1); }
|
||||
.bg-secondary-soft { background-color: rgba(108, 117, 125, 0.1); }
|
||||
</style>
|
||||
{% endblock %}
|
||||
@ -7,7 +7,7 @@
|
||||
<div class="row mb-4 align-items-center">
|
||||
<div class="col-md-6">
|
||||
<h1 class="h3 mb-0 text-gray-800">{% trans "System Settings" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage your business profile and payments." %}</p>
|
||||
<p class="text-muted">{% trans "Manage your business profile, payments, and loyalty system." %}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -35,6 +35,11 @@
|
||||
<i class="bi bi-credit-card me-2"></i>{% trans "Payment Methods" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link fw-bold px-4" id="loyalty-tab" data-bs-toggle="pill" data-bs-target="#loyalty" type="button" role="tab">
|
||||
<i class="bi bi-star me-2"></i>{% trans "Loyalty System" %}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="settingsTabsContent">
|
||||
@ -104,6 +109,29 @@
|
||||
<input type="number" name="decimal_places" class="form-control" value="{{ settings.decimal_places }}" min="0" max="5" required>
|
||||
<div class="form-text">{% trans "For price display" %}</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
<h5 class="fw-bold mb-3">{% trans "Loyalty Configuration" %}</h5>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input class="form-check-input" type="checkbox" name="loyalty_enabled" {% if settings.loyalty_enabled %}checked{% endif %}>
|
||||
<label class="form-check-label fw-bold">{% trans "Enable Loyalty System" %}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">{% trans "Minimum Points to Redeem" %}</label>
|
||||
<input type="number" name="min_points_to_redeem" class="form-control" value="{{ settings.min_points_to_redeem }}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">{% trans "Points per Currency Unit Spent" %}</label>
|
||||
<input type="number" step="0.01" name="points_per_currency" class="form-control" value="{{ settings.points_per_currency }}">
|
||||
<div class="form-text">{% trans "e.g., 1.0 means 1 point for every 1 OMR spent." %}</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">{% trans "Currency Value per Point (Redemption)" %}</label>
|
||||
<input type="number" step="0.001" name="currency_per_point" class="form-control" value="{{ settings.currency_per_point }}">
|
||||
<div class="form-text">{% trans "e.g., 0.010 means 100 points = 1 OMR." %}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-3 border-top d-flex justify-content-end">
|
||||
@ -255,6 +283,159 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loyalty Tab -->
|
||||
<div class="tab-pane fade" id="loyalty" role="tabpanel">
|
||||
<div class="card shadow-sm border-0 glassmorphism mb-4">
|
||||
<div class="card-header bg-transparent border-0 py-3 d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0 fw-bold">{% trans "Loyalty Tiers" %}</h5>
|
||||
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addTierModal">
|
||||
<i class="bi bi-plus-lg me-1"></i> {% trans "Add Tier" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="ps-4">{% trans "Tier Name" %}</th>
|
||||
<th>{% trans "Min. Points" %}</th>
|
||||
<th>{% trans "Multiplier" %}</th>
|
||||
<th>{% trans "Discount" %}</th>
|
||||
<th class="text-end pe-4">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for tier in loyalty_tiers %}
|
||||
<tr>
|
||||
<td class="ps-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="rounded-circle me-2" style="width: 15px; height: 15px; background-color: {{ tier.color_code }};"></div>
|
||||
<span class="fw-bold">{{ tier.name_en }} / {{ tier.name_ar }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ tier.min_points }}</td>
|
||||
<td>{{ tier.point_multiplier }}x</td>
|
||||
<td>{{ tier.discount_percentage }}%</td>
|
||||
<td class="text-end pe-4">
|
||||
<button class="btn btn-sm btn-light text-primary" data-bs-toggle="modal" data-bs-target="#editTierModal{{ tier.id }}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-light text-danger" data-bs-toggle="modal" data-bs-target="#deleteTierModal{{ tier.id }}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Edit Tier Modal -->
|
||||
<div class="modal fade" id="editTierModal{{ tier.id }}" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content border-0">
|
||||
<form action="{% url 'edit_loyalty_tier' tier.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title fw-bold">{% trans "Edit Loyalty Tier" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">{% trans "Name (EN)" %}</label>
|
||||
<input type="text" name="name_en" class="form-control" value="{{ tier.name_en }}" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">{% trans "Name (AR)" %}</label>
|
||||
<input type="text" name="name_ar" class="form-control" value="{{ tier.name_ar }}" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">{% trans "Min. Points" %}</label>
|
||||
<input type="number" name="min_points" class="form-control" value="{{ tier.min_points }}" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">{% trans "Point Multiplier" %}</label>
|
||||
<input type="number" step="0.01" name="point_multiplier" class="form-control" value="{{ tier.point_multiplier }}" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">{% trans "Discount (%)" %}</label>
|
||||
<input type="number" step="0.01" name="discount_percentage" class="form-control" value="{{ tier.discount_percentage }}" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">{% trans "Color Code" %}</label>
|
||||
<input type="color" name="color_code" class="form-control form-control-color w-100" value="{{ tier.color_code }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light border-0">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||
<button type="submit" class="btn btn-primary">{% trans "Save Changes" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-5">
|
||||
<div class="text-muted">
|
||||
<i class="bi bi-star fs-1 d-block mb-3"></i>
|
||||
{% trans "No loyalty tiers defined." %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Tier Modal -->
|
||||
<div class="modal fade" id="addTierModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<form action="{% url 'add_loyalty_tier' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title fw-bold">{% trans "Add Loyalty Tier" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">{% trans "Name (EN)" %}</label>
|
||||
<input type="text" name="name_en" class="form-control" required placeholder="e.g. Gold">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">{% trans "Name (AR)" %}</label>
|
||||
<input type="text" name="name_ar" class="form-control" required placeholder="e.g. ذهبي">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">{% trans "Min. Points" %}</label>
|
||||
<input type="number" name="min_points" class="form-control" required value="0">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">{% trans "Point Multiplier" %}</label>
|
||||
<input type="number" step="0.01" name="point_multiplier" class="form-control" required value="1.00">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">{% trans "Discount (%)" %}</label>
|
||||
<input type="number" step="0.01" name="discount_percentage" class="form-control" required value="0">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">{% trans "Color Code" %}</label>
|
||||
<input type="color" name="color_code" class="form-control form-control-color w-100" value="#6c757d">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light border-0">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
|
||||
<button type="submit" class="btn btn-primary">{% trans "Save Tier" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -337,8 +518,6 @@
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
// Refresh table or append row (for simplicity, we suggest a reload or partial update)
|
||||
// For a smooth experience, let's append the row
|
||||
const tableBody = document.querySelector('#paymentMethodsTable tbody');
|
||||
const noItems = document.getElementById('noPaymentMethods');
|
||||
if (noItems) noItems.parentElement.remove();
|
||||
@ -359,16 +538,14 @@
|
||||
`;
|
||||
tableBody.appendChild(newRow);
|
||||
|
||||
// Success Toast/Alert (Simplified)
|
||||
alert('{% trans "Payment method added successfully!" %}');
|
||||
|
||||
if (stayOpen) {
|
||||
// Clear fields
|
||||
document.getElementById('addPmNameEn').value = '';
|
||||
document.getElementById('addPmNameAr').value = '';
|
||||
} else {
|
||||
bootstrap.Modal.getInstance(document.getElementById('addPaymentModal')).hide();
|
||||
window.location.reload(); // Reload to get modals for the new row
|
||||
window.location.reload();
|
||||
}
|
||||
} else {
|
||||
alert('Error: ' + data.error);
|
||||
@ -383,4 +560,4 @@
|
||||
document.getElementById('saveAndAddAnotherPm').addEventListener('click', () => savePaymentMethod(true));
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@ -10,6 +10,7 @@ urlpatterns = [
|
||||
path('purchases/', views.purchases, name='purchases'),
|
||||
path('reports/', views.reports, name='reports'),
|
||||
path('settings/', views.settings_view, name='settings'),
|
||||
path('profile/', views.profile_view, name='profile'),
|
||||
path('users/', views.user_management, name='user_management'),
|
||||
path('api/group-details/<int:pk>/', views.group_details_api, name='group_details_api'),
|
||||
|
||||
@ -95,4 +96,10 @@ urlpatterns = [
|
||||
path('settings/payment-methods/edit/<int:pk>/', views.edit_payment_method, name='edit_payment_method'),
|
||||
path('settings/payment-methods/delete/<int:pk>/', views.delete_payment_method, name='delete_payment_method'),
|
||||
path('api/add-payment-method-ajax/', views.add_payment_method_ajax, name='add_payment_method_ajax'),
|
||||
|
||||
# Loyalty
|
||||
path('settings/loyalty/add/', views.add_loyalty_tier, name='add_loyalty_tier'),
|
||||
path('settings/loyalty/edit/<int:pk>/', views.edit_loyalty_tier, name='edit_loyalty_tier'),
|
||||
path('settings/loyalty/delete/<int:pk>/', views.delete_loyalty_tier, name='delete_loyalty_tier'),
|
||||
path('api/customer-loyalty/<int:pk>/', views.get_customer_loyalty_api, name='get_customer_loyalty_api'),
|
||||
]
|
||||
|
||||
170
core/views.py
170
core/views.py
@ -1,3 +1,4 @@
|
||||
import decimal
|
||||
from django.contrib.auth.models import User, Group, Permission
|
||||
from django.urls import reverse
|
||||
import random
|
||||
@ -14,7 +15,7 @@ from .models import (
|
||||
SaleItem, SalePayment, SystemSetting,
|
||||
Quotation, QuotationItem,
|
||||
SaleReturn, SaleReturnItem, PurchaseReturn, PurchaseReturnItem,
|
||||
PaymentMethod, HeldSale
|
||||
PaymentMethod, HeldSale, LoyaltyTier, LoyaltyTransaction
|
||||
)
|
||||
import json
|
||||
from datetime import timedelta
|
||||
@ -90,6 +91,7 @@ def pos(request):
|
||||
customers = Customer.objects.all()
|
||||
categories = Category.objects.all()
|
||||
payment_methods = PaymentMethod.objects.filter(is_active=True)
|
||||
settings = SystemSetting.objects.first()
|
||||
|
||||
# Ensure at least Cash exists
|
||||
if not payment_methods.exists():
|
||||
@ -100,7 +102,8 @@ def pos(request):
|
||||
'products': products,
|
||||
'customers': customers,
|
||||
'categories': categories,
|
||||
'payment_methods': payment_methods
|
||||
'payment_methods': payment_methods,
|
||||
'settings': settings
|
||||
}
|
||||
return render(request, 'core/pos.html', context)
|
||||
|
||||
@ -305,11 +308,23 @@ def create_sale_api(request):
|
||||
payment_method_id = data.get('payment_method_id')
|
||||
due_date = data.get('due_date')
|
||||
notes = data.get('notes', '')
|
||||
|
||||
# Loyalty data
|
||||
points_to_redeem = data.get('loyalty_points_redeemed', 0)
|
||||
|
||||
customer = None
|
||||
if customer_id:
|
||||
customer = Customer.objects.get(id=customer_id)
|
||||
|
||||
settings = SystemSetting.objects.first()
|
||||
if not settings:
|
||||
settings = SystemSetting.objects.create()
|
||||
|
||||
loyalty_discount = 0
|
||||
if settings.loyalty_enabled and customer and points_to_redeem > 0:
|
||||
if customer.loyalty_points >= points_to_redeem:
|
||||
loyalty_discount = float(points_to_redeem) * float(settings.currency_per_point)
|
||||
|
||||
sale = Sale.objects.create(
|
||||
customer=customer,
|
||||
invoice_number=invoice_number,
|
||||
@ -317,6 +332,8 @@ def create_sale_api(request):
|
||||
paid_amount=paid_amount,
|
||||
balance_due=float(total_amount) - float(paid_amount),
|
||||
discount=discount,
|
||||
loyalty_points_redeemed=points_to_redeem,
|
||||
loyalty_discount_amount=loyalty_discount,
|
||||
payment_type=payment_type,
|
||||
due_date=due_date if due_date else None,
|
||||
notes=notes,
|
||||
@ -359,9 +376,36 @@ def create_sale_api(request):
|
||||
product.stock_quantity -= int(item['quantity'])
|
||||
product.save()
|
||||
|
||||
settings = SystemSetting.objects.first()
|
||||
if not settings:
|
||||
settings = SystemSetting.objects.create()
|
||||
# Handle Loyalty Points
|
||||
if settings.loyalty_enabled and customer:
|
||||
# Earn Points
|
||||
points_earned = float(total_amount) * float(settings.points_per_currency)
|
||||
if customer.loyalty_tier:
|
||||
points_earned *= float(customer.loyalty_tier.point_multiplier)
|
||||
|
||||
if points_earned > 0:
|
||||
customer.loyalty_points += decimal.Decimal(str(points_earned))
|
||||
LoyaltyTransaction.objects.create(
|
||||
customer=customer,
|
||||
sale=sale,
|
||||
transaction_type='earned',
|
||||
points=points_earned,
|
||||
notes=f"Points earned from Sale #{sale.id}"
|
||||
)
|
||||
|
||||
# Redeem Points
|
||||
if points_to_redeem > 0:
|
||||
customer.loyalty_points -= decimal.Decimal(str(points_to_redeem))
|
||||
LoyaltyTransaction.objects.create(
|
||||
customer=customer,
|
||||
sale=sale,
|
||||
transaction_type='redeemed',
|
||||
points=-points_to_redeem,
|
||||
notes=f"Points redeemed for Sale #{sale.id}"
|
||||
)
|
||||
|
||||
customer.update_tier()
|
||||
customer.save()
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
@ -755,6 +799,12 @@ def settings_view(request):
|
||||
settings.vat_number = request.POST.get('vat_number')
|
||||
settings.registration_number = request.POST.get('registration_number')
|
||||
|
||||
# Loyalty Settings
|
||||
settings.loyalty_enabled = request.POST.get('loyalty_enabled') == 'on'
|
||||
settings.points_per_currency = request.POST.get('points_per_currency', 1.0)
|
||||
settings.currency_per_point = request.POST.get('currency_per_point', 0.010)
|
||||
settings.min_points_to_redeem = request.POST.get('min_points_to_redeem', 100)
|
||||
|
||||
if 'logo' in request.FILES:
|
||||
settings.logo = request.FILES['logo']
|
||||
|
||||
@ -763,10 +813,12 @@ def settings_view(request):
|
||||
return redirect(reverse('settings') + '#profile')
|
||||
|
||||
payment_methods = PaymentMethod.objects.all()
|
||||
loyalty_tiers = LoyaltyTier.objects.all().order_by('min_points')
|
||||
|
||||
return render(request, 'core/settings.html', {
|
||||
'settings': settings,
|
||||
'payment_methods': payment_methods,
|
||||
'loyalty_tiers': loyalty_tiers
|
||||
})
|
||||
|
||||
@login_required
|
||||
@ -1315,7 +1367,6 @@ def add_customer_ajax(request):
|
||||
return JsonResponse({'success': False, 'error': str(e)}, status=400)
|
||||
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
|
||||
|
||||
@csrf_exempt
|
||||
@login_required
|
||||
def hold_sale_api(request):
|
||||
if request.method == 'POST':
|
||||
@ -1357,7 +1408,6 @@ def get_held_sales_api(request):
|
||||
})
|
||||
return JsonResponse({'success': True, 'held_sales': data})
|
||||
|
||||
@csrf_exempt
|
||||
@login_required
|
||||
def recall_held_sale_api(request, pk):
|
||||
held_sale = get_object_or_404(HeldSale, pk=pk, created_by=request.user)
|
||||
@ -1371,9 +1421,111 @@ def recall_held_sale_api(request, pk):
|
||||
held_sale.delete()
|
||||
return JsonResponse(data)
|
||||
|
||||
@csrf_exempt
|
||||
@login_required
|
||||
def delete_held_sale_api(request, pk):
|
||||
held_sale = get_object_or_404(HeldSale, pk=pk, created_by=request.user)
|
||||
held_sale.delete()
|
||||
return JsonResponse({'success': True})
|
||||
return JsonResponse({'success': True})
|
||||
|
||||
@login_required
|
||||
def add_loyalty_tier(request):
|
||||
if request.method == 'POST':
|
||||
name_en = request.POST.get('name_en')
|
||||
name_ar = request.POST.get('name_ar')
|
||||
min_points = request.POST.get('min_points', 0)
|
||||
multiplier = request.POST.get('point_multiplier', 1.0)
|
||||
discount = request.POST.get('discount_percentage', 0)
|
||||
color = request.POST.get('color_code', '#6c757d')
|
||||
|
||||
LoyaltyTier.objects.create(
|
||||
name_en=name_en, name_ar=name_ar,
|
||||
min_points=min_points, point_multiplier=multiplier,
|
||||
discount_percentage=discount, color_code=color
|
||||
)
|
||||
messages.success(request, "Loyalty tier added successfully!")
|
||||
return redirect(reverse('settings') + '#loyalty')
|
||||
|
||||
@login_required
|
||||
def edit_loyalty_tier(request, pk):
|
||||
tier = get_object_or_404(LoyaltyTier, pk=pk)
|
||||
if request.method == 'POST':
|
||||
tier.name_en = request.POST.get('name_en')
|
||||
tier.name_ar = request.POST.get('name_ar')
|
||||
tier.min_points = request.POST.get('min_points')
|
||||
tier.point_multiplier = request.POST.get('point_multiplier')
|
||||
tier.discount_percentage = request.POST.get('discount_percentage')
|
||||
tier.color_code = request.POST.get('color_code')
|
||||
tier.save()
|
||||
messages.success(request, "Loyalty tier updated successfully!")
|
||||
return redirect(reverse('settings') + '#loyalty')
|
||||
|
||||
@login_required
|
||||
def delete_loyalty_tier(request, pk):
|
||||
tier = get_object_or_404(LoyaltyTier, pk=pk)
|
||||
tier.delete()
|
||||
messages.success(request, "Loyalty tier deleted successfully!")
|
||||
return redirect(reverse('settings') + '#loyalty')
|
||||
|
||||
@login_required
|
||||
def get_customer_loyalty_api(request, pk):
|
||||
customer = get_object_or_404(Customer, pk=pk)
|
||||
settings = SystemSetting.objects.first()
|
||||
|
||||
tier_info = None
|
||||
if customer.loyalty_tier:
|
||||
tier_info = {
|
||||
'name_en': customer.loyalty_tier.name_en,
|
||||
'name_ar': customer.loyalty_tier.name_ar,
|
||||
'multiplier': float(customer.loyalty_tier.point_multiplier),
|
||||
'discount': float(customer.loyalty_tier.discount_percentage),
|
||||
'color': customer.loyalty_tier.color_code
|
||||
}
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'points': float(customer.loyalty_points),
|
||||
'tier': tier_info,
|
||||
'currency_per_point': float(settings.currency_per_point) if settings else 0.01,
|
||||
'min_points_to_redeem': settings.min_points_to_redeem if settings else 100
|
||||
})
|
||||
|
||||
@login_required
|
||||
def profile_view(request):
|
||||
"""
|
||||
User Profile View
|
||||
"""
|
||||
if request.method == 'POST':
|
||||
user = request.user
|
||||
user.first_name = request.POST.get('first_name')
|
||||
user.last_name = request.POST.get('last_name')
|
||||
user.email = request.POST.get('email')
|
||||
|
||||
# Profile specific
|
||||
profile = user.profile
|
||||
profile.phone = request.POST.get('phone')
|
||||
profile.bio = request.POST.get('bio')
|
||||
|
||||
if 'image' in request.FILES:
|
||||
profile.image = request.FILES['image']
|
||||
|
||||
user.save()
|
||||
profile.save()
|
||||
|
||||
# Password change
|
||||
password = request.POST.get('password')
|
||||
confirm_password = request.POST.get('confirm_password')
|
||||
if password:
|
||||
if password == confirm_password:
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
update_session_auth_hash(request, user)
|
||||
messages.success(request, "Profile and password updated successfully!")
|
||||
else:
|
||||
messages.error(request, "Passwords do not match.")
|
||||
else:
|
||||
messages.success(request, "Profile updated successfully!")
|
||||
|
||||
return redirect('profile')
|
||||
|
||||
return render(request, 'core/profile.html')
|
||||
|
||||
@ -193,4 +193,117 @@ body {
|
||||
#content {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced Responsiveness */
|
||||
|
||||
/* Container & Spacing Adjustments */
|
||||
@media (max-width: 576px) {
|
||||
main.p-4 {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
.hero-gradient {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.top-navbar {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
h1, .h1 { font-size: 1.5rem; }
|
||||
h2, .h2 { font-size: 1.25rem; }
|
||||
h3, .h3 { font-size: 1.1rem; }
|
||||
h4, .h4 { font-size: 1rem; }
|
||||
}
|
||||
|
||||
/* Sidebar Overlay for Mobile */
|
||||
@media (max-width: 992px) {
|
||||
#sidebar {
|
||||
box-shadow: 0 0 20px rgba(0,0,0,0.1);
|
||||
}
|
||||
#sidebar.active {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
#sidebar:not(.active) {
|
||||
margin-inline-start: calc(-1 * var(--sidebar-width));
|
||||
}
|
||||
|
||||
/* Overlay effect when sidebar is active */
|
||||
#sidebar.active::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0,0,0,0.2);
|
||||
z-index: -1;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* Table Card Styling for Mobile */
|
||||
@media (max-width: 768px) {
|
||||
.table-responsive {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.btn-sm {
|
||||
padding: 0.4rem 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* POS Specific Responsive Utilities */
|
||||
.mobile-cart-toggle {
|
||||
display: none;
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 1030;
|
||||
border-radius: 50px;
|
||||
padding: 12px 24px;
|
||||
box-shadow: 0 4px 15px rgba(46, 91, 255, 0.4);
|
||||
}
|
||||
|
||||
[dir="rtl"] .mobile-cart-toggle {
|
||||
right: auto;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.mobile-cart-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.cart-container {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
right: -100% !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
z-index: 1040 !important;
|
||||
transition: right 0.3s ease;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
[dir="rtl"] .cart-container {
|
||||
right: auto !important;
|
||||
left: -100% !important;
|
||||
transition: left 0.3s ease;
|
||||
}
|
||||
.cart-container.show {
|
||||
right: 0 !important;
|
||||
}
|
||||
[dir="rtl"] .cart-container.show {
|
||||
left: 0 !important;
|
||||
}
|
||||
|
||||
/* Ensure body doesn't scroll when cart is open */
|
||||
body.cart-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* Grid adjustments */
|
||||
@media (max-width: 400px) {
|
||||
#productGrid .col {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user