diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 13f71e7..255213d 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 9998128..933112c 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 7d3121c..cd848e1 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 97afd4a..0c44493 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 49646aa..130daf1 100644 --- a/core/admin.py +++ b/core/admin.py @@ -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') \ No newline at end of file + 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',) diff --git a/core/migrations/0014_loyaltytier_customer_loyalty_points_and_more.py b/core/migrations/0014_loyaltytier_customer_loyalty_points_and_more.py new file mode 100644 index 0000000..2842852 --- /dev/null +++ b/core/migrations/0014_loyaltytier_customer_loyalty_points_and_more.py @@ -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')), + ], + ), + ] diff --git a/core/migrations/0015_userprofile.py b/core/migrations/0015_userprofile.py new file mode 100644 index 0000000..f62b301 --- /dev/null +++ b/core/migrations/0015_userprofile.py @@ -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)), + ], + ), + ] diff --git a/core/migrations/__pycache__/0014_loyaltytier_customer_loyalty_points_and_more.cpython-311.pyc b/core/migrations/__pycache__/0014_loyaltytier_customer_loyalty_points_and_more.cpython-311.pyc new file mode 100644 index 0000000..43af3a2 Binary files /dev/null and b/core/migrations/__pycache__/0014_loyaltytier_customer_loyalty_points_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0015_userprofile.cpython-311.pyc b/core/migrations/__pycache__/0015_userprofile.cpython-311.pyc new file mode 100644 index 0000000..84c92a3 Binary files /dev/null and b/core/migrations/__pycache__/0015_userprofile.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a240b..6d25482 100644 --- a/core/models.py +++ b/core/models.py @@ -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 \ No newline at end of file + 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() diff --git a/core/templates/base.html b/core/templates/base.html index 61ca302..96b77d5 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -201,18 +201,39 @@
@@ -255,6 +283,159 @@
+ + +
+
+
+
{% trans "Loyalty Tiers" %}
+ +
+
+
+ + + + + + + + + + + + {% for tier in loyalty_tiers %} + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Tier Name" %}{% trans "Min. Points" %}{% trans "Multiplier" %}{% trans "Discount" %}{% trans "Actions" %}
+
+
+ {{ tier.name_en }} / {{ tier.name_ar }} +
+
{{ tier.min_points }}{{ tier.point_multiplier }}x{{ tier.discount_percentage }}% + + +
+
+ + {% trans "No loyalty tiers defined." %} +
+
+
+
+
+
+ + + + + @@ -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)); }); -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 13e9226..733c743 100644 --- a/core/urls.py +++ b/core/urls.py @@ -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//', views.group_details_api, name='group_details_api'), @@ -95,4 +96,10 @@ urlpatterns = [ path('settings/payment-methods/edit//', views.edit_payment_method, name='edit_payment_method'), path('settings/payment-methods/delete//', 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//', views.edit_loyalty_tier, name='edit_loyalty_tier'), + path('settings/loyalty/delete//', views.delete_loyalty_tier, name='delete_loyalty_tier'), + path('api/customer-loyalty//', views.get_customer_loyalty_api, name='get_customer_loyalty_api'), ] diff --git a/core/views.py b/core/views.py index dce2c94..42dcb7a 100644 --- a/core/views.py +++ b/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}) \ No newline at end of file + 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') diff --git a/static/css/custom.css b/static/css/custom.css index d5cb873..cb28598 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -193,4 +193,117 @@ body { #content { width: 100%; } -} \ No newline at end of file +} + +/* 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; + } +}