Autosave: 20260202-073948
This commit is contained in:
parent
a5d1439f46
commit
7d1c8df2b2
Binary file not shown.
Binary file not shown.
@ -62,6 +62,7 @@ MIDDLEWARE = [
|
|||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
@ -85,6 +86,7 @@ TEMPLATES = [
|
|||||||
'django.contrib.messages.context_processors.messages',
|
'django.contrib.messages.context_processors.messages',
|
||||||
# IMPORTANT: do not remove – injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp
|
# IMPORTANT: do not remove – injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp
|
||||||
'core.context_processors.project_context',
|
'core.context_processors.project_context',
|
||||||
|
'core.context_processors.global_settings',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -134,6 +136,12 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
LANGUAGES = [
|
||||||
|
('en', 'English'),
|
||||||
|
('ar', 'Arabic'),
|
||||||
|
]
|
||||||
|
|
||||||
|
LOCALE_PATHS = [BASE_DIR / 'locale']
|
||||||
|
|
||||||
TIME_ZONE = 'UTC'
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
@ -148,6 +156,8 @@ USE_TZ = True
|
|||||||
STATIC_URL = 'static/'
|
STATIC_URL = 'static/'
|
||||||
# Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS.
|
# Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS.
|
||||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||||
|
MEDIA_URL = 'media/'
|
||||||
|
MEDIA_ROOT = BASE_DIR / 'media'
|
||||||
|
|
||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [
|
||||||
BASE_DIR / 'static',
|
BASE_DIR / 'static',
|
||||||
@ -179,4 +189,4 @@ if EMAIL_USE_SSL:
|
|||||||
# Default primary key field type
|
# Default primary key field type
|
||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
@ -1,19 +1,3 @@
|
|||||||
"""
|
|
||||||
URL configuration for config project.
|
|
||||||
|
|
||||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
|
||||||
https://docs.djangoproject.com/en/5.2/topics/http/urls/
|
|
||||||
Examples:
|
|
||||||
Function views
|
|
||||||
1. Add an import: from my_app import views
|
|
||||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
|
||||||
Class-based views
|
|
||||||
1. Add an import: from other_app.views import Home
|
|
||||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
|
||||||
Including another URLconf
|
|
||||||
1. Import the include() function: from django.urls import include, path
|
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
|
||||||
"""
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -21,9 +5,11 @@ from django.conf.urls.static import static
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
|
path("i18n/", include("django.conf.urls.i18n")),
|
||||||
path("", include("core.urls")),
|
path("", include("core.urls")),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
||||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,3 +1,36 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from .models import Category, Product, Customer, Supplier, Sale, SaleItem, Purchase
|
||||||
|
|
||||||
# Register your models here.
|
@admin.register(Category)
|
||||||
|
class CategoryAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name_en', 'name_ar', 'slug')
|
||||||
|
prepopulated_fields = {'slug': ('name_en',)}
|
||||||
|
|
||||||
|
@admin.register(Product)
|
||||||
|
class ProductAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name_en', 'name_ar', 'sku', 'price', 'stock_quantity', 'category')
|
||||||
|
list_filter = ('category',)
|
||||||
|
search_fields = ('name_en', 'name_ar', 'sku')
|
||||||
|
|
||||||
|
@admin.register(Customer)
|
||||||
|
class CustomerAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'phone', 'email')
|
||||||
|
search_fields = ('name', 'phone')
|
||||||
|
|
||||||
|
@admin.register(Supplier)
|
||||||
|
class SupplierAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'contact_person', 'phone')
|
||||||
|
|
||||||
|
class SaleItemInline(admin.TabularInline):
|
||||||
|
model = SaleItem
|
||||||
|
extra = 1
|
||||||
|
|
||||||
|
@admin.register(Sale)
|
||||||
|
class SaleAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'customer', 'total_amount', 'created_at')
|
||||||
|
inlines = [SaleItemInline]
|
||||||
|
|
||||||
|
@admin.register(Purchase)
|
||||||
|
class PurchaseAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'supplier', 'total_amount', 'created_at')
|
||||||
|
list_filter = ('supplier', 'created_at')
|
||||||
|
|||||||
@ -1,13 +1,23 @@
|
|||||||
|
from .models import SystemSetting
|
||||||
import os
|
import os
|
||||||
import time
|
from django.utils import timezone
|
||||||
|
|
||||||
def project_context(request):
|
def project_context(request):
|
||||||
"""
|
"""
|
||||||
Adds project-specific environment variables to the template context globally.
|
Injects project description and social image URL from environment variables.
|
||||||
|
Also injects a deployment timestamp for cache-busting.
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||||
# Used for cache-busting static assets
|
"deployment_timestamp": int(timezone.now().timestamp()),
|
||||||
"deployment_timestamp": int(time.time()),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def global_settings(request):
|
||||||
|
try:
|
||||||
|
settings = SystemSetting.objects.first()
|
||||||
|
if not settings:
|
||||||
|
settings = SystemSetting.objects.create()
|
||||||
|
return {'site_settings': settings}
|
||||||
|
except:
|
||||||
|
return {}
|
||||||
|
|||||||
91
core/migrations/0001_initial.py
Normal file
91
core/migrations/0001_initial.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 06:51
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Category',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name_en', models.CharField(max_length=100, verbose_name='Name (English)')),
|
||||||
|
('name_ar', models.CharField(max_length=100, verbose_name='Name (Arabic)')),
|
||||||
|
('slug', models.SlugField(unique=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'Categories',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Customer',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=200, verbose_name='Name')),
|
||||||
|
('phone', models.CharField(blank=True, max_length=20, verbose_name='Phone')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=254, verbose_name='Email')),
|
||||||
|
('address', models.TextField(blank=True, verbose_name='Address')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Supplier',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=200, verbose_name='Name')),
|
||||||
|
('contact_person', models.CharField(blank=True, max_length=200, verbose_name='Contact Person')),
|
||||||
|
('phone', models.CharField(blank=True, max_length=20, verbose_name='Phone')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Product',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name_en', models.CharField(max_length=200, verbose_name='Name (English)')),
|
||||||
|
('name_ar', models.CharField(max_length=200, verbose_name='Name (Arabic)')),
|
||||||
|
('sku', models.CharField(max_length=50, unique=True, verbose_name='SKU')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='Description')),
|
||||||
|
('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Price')),
|
||||||
|
('stock_quantity', models.PositiveIntegerField(default=0, verbose_name='Stock Quantity')),
|
||||||
|
('image', models.URLField(blank=True, null=True, verbose_name='Product Image')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='core.category')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Sale',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('total_amount', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||||
|
('discount', models.DecimalField(decimal_places=2, default=0, max_digits=12)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.customer')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SaleItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('quantity', models.PositiveIntegerField()),
|
||||||
|
('unit_price', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||||
|
('line_total', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')),
|
||||||
|
('sale', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.sale')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Purchase',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('total_amount', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('supplier', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.supplier')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
26
core/migrations/0002_systemsetting.py
Normal file
26
core/migrations/0002_systemsetting.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 07:30
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SystemSetting',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('business_name', models.CharField(default='Meezan Accounting', max_length=200, verbose_name='Business Name')),
|
||||||
|
('address', models.TextField(blank=True, verbose_name='Address')),
|
||||||
|
('phone', models.CharField(blank=True, max_length=20, verbose_name='Phone')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=254, verbose_name='Email')),
|
||||||
|
('currency_symbol', models.CharField(default='$', max_length=10, verbose_name='Currency Symbol')),
|
||||||
|
('tax_rate', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='Tax Rate (%)')),
|
||||||
|
('logo_url', models.URLField(blank=True, null=True, verbose_name='Logo URL')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/migrations/__pycache__/0002_systemsetting.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0002_systemsetting.cpython-311.pyc
Normal file
Binary file not shown.
@ -1,3 +1,77 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
# Create your models here.
|
class Category(models.Model):
|
||||||
|
name_en = models.CharField(_("Name (English)"), max_length=100)
|
||||||
|
name_ar = models.CharField(_("Name (Arabic)"), max_length=100)
|
||||||
|
slug = models.SlugField(unique=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name_plural = _("Categories")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name_en} / {self.name_ar}"
|
||||||
|
|
||||||
|
class Product(models.Model):
|
||||||
|
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="products")
|
||||||
|
name_en = models.CharField(_("Name (English)"), max_length=200)
|
||||||
|
name_ar = models.CharField(_("Name (Arabic)"), max_length=200)
|
||||||
|
sku = models.CharField(_("SKU"), max_length=50, unique=True)
|
||||||
|
description = models.TextField(_("Description"), blank=True)
|
||||||
|
price = models.DecimalField(_("Price"), max_digits=10, decimal_places=2)
|
||||||
|
stock_quantity = models.PositiveIntegerField(_("Stock Quantity"), default=0)
|
||||||
|
image = models.URLField(_("Product Image"), blank=True, null=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name_en} ({self.sku})"
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Supplier(models.Model):
|
||||||
|
name = models.CharField(_("Name"), max_length=200)
|
||||||
|
contact_person = models.CharField(_("Contact Person"), max_length=200, blank=True)
|
||||||
|
phone = models.CharField(_("Phone"), max_length=20, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Sale(models.Model):
|
||||||
|
customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
|
total_amount = models.DecimalField(max_digits=12, decimal_places=2)
|
||||||
|
discount = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Sale #{self.id} - {self.total_amount}"
|
||||||
|
|
||||||
|
class SaleItem(models.Model):
|
||||||
|
sale = models.ForeignKey(Sale, on_delete=models.CASCADE, related_name="items")
|
||||||
|
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||||
|
quantity = models.PositiveIntegerField()
|
||||||
|
unit_price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
|
line_total = models.DecimalField(max_digits=12, decimal_places=2)
|
||||||
|
|
||||||
|
class Purchase(models.Model):
|
||||||
|
supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True)
|
||||||
|
total_amount = models.DecimalField(max_digits=12, decimal_places=2)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class SystemSetting(models.Model):
|
||||||
|
business_name = models.CharField(_("Business Name"), max_length=200, default="Meezan Accounting")
|
||||||
|
address = models.TextField(_("Address"), blank=True)
|
||||||
|
phone = models.CharField(_("Phone"), max_length=20, blank=True)
|
||||||
|
email = models.EmailField(_("Email"), blank=True)
|
||||||
|
currency_symbol = models.CharField(_("Currency Symbol"), max_length=10, default="$")
|
||||||
|
tax_rate = models.DecimalField(_("Tax Rate (%)"), max_digits=5, decimal_places=2, default=0)
|
||||||
|
logo_url = models.URLField(_("Logo URL"), blank=True, null=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.business_name
|
||||||
|
|||||||
@ -1,25 +1,148 @@
|
|||||||
<!DOCTYPE html>
|
{% load static i18n %}{% get_current_language as LANGUAGE_CODE %}<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="{{ LANGUAGE_CODE }}" dir="{% if LANGUAGE_CODE == 'ar' %}rtl{% else %}ltr{% endif %}">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
{% if project_description %}
|
<title>{% block title %}{{ site_settings.business_name }}{% endblock %}</title>
|
||||||
<meta name="description" content="{{ project_description }}">
|
|
||||||
<meta property="og:description" content="{{ project_description }}">
|
{% if project_description %}
|
||||||
<meta property="twitter:description" content="{{ project_description }}">
|
<meta name="description" content="{{ project_description }}">
|
||||||
{% endif %}
|
<meta property="og:description" content="{{ project_description }}">
|
||||||
{% if project_image_url %}
|
{% endif %}
|
||||||
<meta property="og:image" content="{{ project_image_url }}">
|
|
||||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
{% endif %}
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
{% load static %}
|
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;700&family=Plus+Jakarta+Sans:wght@400;600;700&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
|
||||||
{% block head %}{% endblock %}
|
{% if LANGUAGE_CODE == 'ar' %}
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.rtl.min.css">
|
||||||
|
{% else %}
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||||
|
|
||||||
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{% block content %}{% endblock %}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
<div id="wrapper">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<nav id="sidebar">
|
||||||
|
<div class="sidebar-header d-flex align-items-center">
|
||||||
|
<a class="navbar-brand fw-bold text-primary fs-4" href="{% url 'index' %}">
|
||||||
|
{% if site_settings.logo_url %}
|
||||||
|
<img src="{{ site_settings.logo_url }}" alt="Logo" height="30" class="me-2">
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-calculator-fill me-2"></i>
|
||||||
|
{% endif %}
|
||||||
|
{{ site_settings.business_name|truncatechars:12 }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="list-unstyled components">
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'index' %}" class="{% if request.resolver_match.url_name == 'index' %}active{% endif %}">
|
||||||
|
<i class="bi bi-speedometer2"></i> {% trans "Dashboard" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'pos' %}" class="{% if request.resolver_match.url_name == 'pos' %}active{% endif %}">
|
||||||
|
<i class="bi bi-shop"></i> {% trans "POS System" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'reports' %}" class="{% if request.resolver_match.url_name == 'reports' %}active{% endif %}">
|
||||||
|
<i class="bi bi-graph-up-arrow"></i> {% trans "Reports" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="mt-3 px-4 small text-muted text-uppercase fw-bold">{% trans "Inventory" %}</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'inventory' %}" class="{% if request.resolver_match.url_name == 'inventory' %}active{% endif %}">
|
||||||
|
<i class="bi bi-box-seam"></i> {% trans "Products" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'purchases' %}" class="{% if request.resolver_match.url_name == 'purchases' %}active{% endif %}">
|
||||||
|
<i class="bi bi-cart-check"></i> {% trans "Purchases" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="mt-3 px-4 small text-muted text-uppercase fw-bold">{% trans "Contacts" %}</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'customers' %}" class="{% if request.resolver_match.url_name == 'customers' %}active{% endif %}">
|
||||||
|
<i class="bi bi-people"></i> {% trans "Customers" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'suppliers' %}" class="{% if request.resolver_match.url_name == 'suppliers' %}active{% endif %}">
|
||||||
|
<i class="bi bi-truck"></i> {% trans "Suppliers" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="mt-3 px-4 small text-muted text-uppercase fw-bold">{% trans "System" %}</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'settings' %}" class="{% if request.resolver_match.url_name == 'settings' %}active{% endif %}">
|
||||||
|
<i class="bi bi-gear"></i> {% trans "Settings" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="mt-auto p-4 border-top">
|
||||||
|
<form action="{% url 'set_language' %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||||
|
<select name="language" onchange="this.form.submit()" class="form-select form-select-sm border-0 bg-light">
|
||||||
|
{% get_available_languages as LANGUAGES %}
|
||||||
|
{% get_language_info_list for LANGUAGES as languages %}
|
||||||
|
{% for language in languages %}
|
||||||
|
<option value="{{ language.code }}" {% if language.code == LANGUAGE_CODE %}selected{% endif %}>
|
||||||
|
{{ language.name_local }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Page Content -->
|
||||||
|
<div id="content">
|
||||||
|
<nav class="navbar navbar-expand-lg top-navbar sticky-top">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<button type="button" id="sidebarCollapse" class="btn btn-light me-3">
|
||||||
|
<i class="bi bi-list"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<span>{% trans "Admin" %}</span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end shadow border-0 mt-2">
|
||||||
|
<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><a class="dropdown-item text-danger" href="/admin/logout/"><i class="bi bi-box-arrow-right me-2"></i> {% trans "Logout" %}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="p-4">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
document.getElementById('sidebarCollapse').addEventListener('click', function () {
|
||||||
|
document.getElementById('sidebar').classList.toggle('active');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
103
core/templates/core/customers.html
Normal file
103
core/templates/core/customers.html
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Customers" %} | Meezan Accounting{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="fw-bold mb-0">{% trans "Customers" %}</h2>
|
||||||
|
<button class="btn btn-primary rounded-3" data-bs-toggle="modal" data-bs-target="#addCustomerModal">
|
||||||
|
<i class="bi bi-person-plus me-2"></i>{% trans "Add Customer" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if messages %}
|
||||||
|
<div class="mb-4">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags }} alert-dismissible fade show rounded-3" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<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 "Name" %}</th>
|
||||||
|
<th>{% trans "Phone" %}</th>
|
||||||
|
<th>{% trans "Email" %}</th>
|
||||||
|
<th>{% trans "Address" %}</th>
|
||||||
|
<th>{% trans "Total Sales" %}</th>
|
||||||
|
<th class="text-end pe-4">{% trans "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for customer in customers %}
|
||||||
|
<tr>
|
||||||
|
<td class="ps-4 fw-bold">{{ customer.name }}</td>
|
||||||
|
<td>{{ customer.phone }}</td>
|
||||||
|
<td>{{ customer.email }}</td>
|
||||||
|
<td>{{ customer.address|truncatechars:30 }}</td>
|
||||||
|
<td><span class="badge bg-primary bg-opacity-10 text-primary">{{ site_settings.currency_symbol }}{{ customer.total_sales|default:"0.00" }}</span></td>
|
||||||
|
<td class="text-end pe-4">
|
||||||
|
<button class="btn btn-sm btn-light rounded-circle"><i class="bi bi-pencil"></i></button>
|
||||||
|
<button class="btn btn-sm btn-light rounded-circle text-danger"><i class="bi bi-trash"></i></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="text-center py-5 text-muted">
|
||||||
|
{% trans "No customers found." %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Customer Modal -->
|
||||||
|
<div class="modal fade" id="addCustomerModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content border-0 shadow rounded-4">
|
||||||
|
<div class="modal-header border-0">
|
||||||
|
<h5 class="fw-bold">{% trans "Add New Customer" %}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<form action="{% url 'add_customer' %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Full Name" %}</label>
|
||||||
|
<input type="text" name="name" class="form-control rounded-3" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Phone Number" %}</label>
|
||||||
|
<input type="text" name="phone" class="form-control rounded-3">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Email Address" %}</label>
|
||||||
|
<input type="email" name="email" class="form-control rounded-3">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Address" %}</label>
|
||||||
|
<textarea name="address" class="form-control rounded-3" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer border-0">
|
||||||
|
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||||
|
<button type="submit" class="btn btn-primary rounded-3 px-4">{% trans "Save Customer" %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@ -1,145 +1,220 @@
|
|||||||
{% extends "base.html" %}
|
{% extends 'base.html' %}
|
||||||
|
{% load static i18n %}
|
||||||
|
|
||||||
{% block title %}{{ project_name }}{% endblock %}
|
{% block title %}{% trans "Smart Dashboard" %} - {{ site_settings.business_name }}{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--bg-color-start: #6a11cb;
|
|
||||||
--bg-color-end: #2575fc;
|
|
||||||
--text-color: #ffffff;
|
|
||||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
|
||||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
|
||||||
color: var(--text-color);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
text-align: center;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
body::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
|
||||||
animation: bg-pan 20s linear infinite;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bg-pan {
|
|
||||||
0% {
|
|
||||||
background-position: 0% 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
background-position: 100% 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: var(--card-bg-color);
|
|
||||||
border: 1px solid var(--card-border-color);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 2.5rem 2rem;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0 0 1.2rem;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
opacity: 0.92;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader {
|
|
||||||
margin: 1.5rem auto;
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
|
||||||
border-top-color: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.runtime code {
|
|
||||||
background: rgba(0, 0, 0, 0.25);
|
|
||||||
padding: 0.15rem 0.45rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sr-only {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
padding: 0;
|
|
||||||
margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main>
|
<div class="container-fluid">
|
||||||
<div class="card">
|
<!-- Header -->
|
||||||
<h1>Analyzing your requirements and generating your app…</h1>
|
<div class="row mb-4 align-items-center">
|
||||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
<div class="col">
|
||||||
<span class="sr-only">Loading…</span>
|
<h1 class="h3 fw-bold mb-0">{% trans "Overview" %}</h1>
|
||||||
|
<p class="text-muted small mb-0">{% trans "Welcome back! Here's what's happening with your business today." %}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<a href="{% url 'pos' %}" class="btn btn-primary shadow-sm">
|
||||||
|
<i class="bi bi-plus-lg me-2"></i> {% trans "New Sale" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
|
|
||||||
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
|
<!-- Stats Cards -->
|
||||||
<p class="runtime">
|
<div class="row g-3 mb-4">
|
||||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
<div class="col-md-3">
|
||||||
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
<div class="card glass-card border-0 p-3 stat-card">
|
||||||
</p>
|
<div class="d-flex align-items-center">
|
||||||
</div>
|
<div class="stat-icon bg-primary text-white bg-opacity-10 text-primary rounded-3 p-3 me-3">
|
||||||
</main>
|
<i class="bi bi-cash-stack fs-4 text-primary"></i>
|
||||||
<footer>
|
</div>
|
||||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
<div>
|
||||||
</footer>
|
<h6 class="text-muted small mb-1">{% trans "Total Revenue" %}</h6>
|
||||||
|
<h4 class="fw-bold mb-0">{{ site_settings.currency_symbol }}{{ total_sales_amount|floatformat:2 }}</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card glass-card border-0 p-3 stat-card">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="stat-icon bg-success bg-opacity-10 rounded-3 p-3 me-3">
|
||||||
|
<i class="bi bi-cart-check fs-4 text-success"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6 class="text-muted small mb-1">{% trans "Total Sales" %}</h6>
|
||||||
|
<h4 class="fw-bold mb-0">{{ total_sales_count }}</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card glass-card border-0 p-3 stat-card">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="stat-icon bg-info bg-opacity-10 rounded-3 p-3 me-3">
|
||||||
|
<i class="bi bi-box-seam fs-4 text-info"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6 class="text-muted small mb-1">{% trans "Total Products" %}</h6>
|
||||||
|
<h4 class="fw-bold mb-0">{{ total_products }}</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card glass-card border-0 p-3 stat-card">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="stat-icon bg-warning bg-opacity-10 rounded-3 p-3 me-3">
|
||||||
|
<i class="bi bi-people fs-4 text-warning"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6 class="text-muted small mb-1">{% trans "Total Customers" %}</h6>
|
||||||
|
<h4 class="fw-bold mb-0">{{ total_customers }}</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<!-- Sales Chart -->
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card border-0 shadow-sm p-4 h-100">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h5 class="fw-bold mb-0">{% trans "Sales Revenue" %}</h5>
|
||||||
|
<span class="badge bg-primary bg-opacity-10 text-primary">{% trans "Last 7 Days" %}</span>
|
||||||
|
</div>
|
||||||
|
<canvas id="salesChart" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Inventory Status -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card border-0 shadow-sm p-4 h-100">
|
||||||
|
<h5 class="fw-bold mb-4">{% trans "Low Stock Alerts" %}</h5>
|
||||||
|
{% if low_stock_products %}
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
{% for product in low_stock_products %}
|
||||||
|
<li class="list-group-item px-0 border-0 d-flex justify-content-between align-items-center">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="bg-light rounded p-2 me-3">
|
||||||
|
<i class="bi bi-box"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="mb-0 fw-semibold">
|
||||||
|
{% if LANGUAGE_CODE == 'ar' %}{{ product.name_ar }}{% else %}{{ product.name_en }}{% endif %}
|
||||||
|
</p>
|
||||||
|
<small class="text-muted">
|
||||||
|
{% if LANGUAGE_CODE == 'ar' %}{{ product.category.name_ar }}{% else %}{{ product.category.name_en }}{% endif %}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="badge bg-danger rounded-pill">{{ product.stock_quantity }}</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-check-circle text-success display-4"></i>
|
||||||
|
<p class="mt-3 text-muted">{% trans "All stock levels are healthy!" %}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="mt-auto pt-3 border-top">
|
||||||
|
<a href="{% url 'inventory' %}" class="btn btn-light btn-sm w-100 fw-bold">{% trans "View Full Inventory" %}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Transactions -->
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card border-0 shadow-sm p-4">
|
||||||
|
<h5 class="fw-bold mb-4">{% trans "Recent Sales" %}</h5>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Sale ID" %}</th>
|
||||||
|
<th>{% trans "Customer" %}</th>
|
||||||
|
<th>{% trans "Date" %}</th>
|
||||||
|
<th>{% trans "Total Amount" %}</th>
|
||||||
|
<th>{% trans "Status" %}</th>
|
||||||
|
<th>{% trans "Action" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for sale in recent_sales %}
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">#{{ sale.id }}</td>
|
||||||
|
<td>{{ sale.customer.name|default:"Guest" }}</td>
|
||||||
|
<td>{{ sale.created_at|date:"M d, Y H:i" }}</td>
|
||||||
|
<td class="fw-bold">{{ site_settings.currency_symbol }}{{ sale.total_amount }}</td>
|
||||||
|
<td><span class="badge bg-success bg-opacity-10 text-success">{% trans "Completed" %}</span></td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-light">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="text-center py-4 text-muted">{% trans "No recent sales found." %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script>
|
||||||
|
const ctx = document.getElementById('salesChart').getContext('2d');
|
||||||
|
const salesChart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: {{ chart_labels|safe }},
|
||||||
|
datasets: [{
|
||||||
|
label: '{% trans "Revenue" %} ({{ site_settings.currency_symbol }})',
|
||||||
|
data: {{ chart_data|safe }},
|
||||||
|
borderColor: '#2E5BFF',
|
||||||
|
backgroundColor: 'rgba(46, 91, 255, 0.1)',
|
||||||
|
borderWidth: 3,
|
||||||
|
tension: 0.4,
|
||||||
|
fill: true,
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
pointBorderColor: '#2E5BFF',
|
||||||
|
pointBorderWidth: 2,
|
||||||
|
pointRadius: 4,
|
||||||
|
pointHoverRadius: 6
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: {
|
||||||
|
drawBorder: false,
|
||||||
|
color: 'rgba(0, 0, 0, 0.05)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
162
core/templates/core/inventory.html
Normal file
162
core/templates/core/inventory.html
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n static %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Inventory Management" %} | Meezan Accounting{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="fw-bold mb-1">{% trans "Inventory Management" %}</h2>
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'index' %}">{% trans "Dashboard" %}</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">{% trans "Inventory" %}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary shadow-sm" data-bs-toggle="modal" data-bs-target="#addProductModal">
|
||||||
|
<i class="bi bi-plus-lg me-2"></i>{% trans "Add Product" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if messages %}
|
||||||
|
<div class="mb-4">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags }} alert-dismissible fade show rounded-3" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Search & Filter Bar -->
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
||||||
|
<div class="card-body p-3">
|
||||||
|
<form class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text bg-white border-end-0"><i class="bi bi-search text-muted"></i></span>
|
||||||
|
<input type="text" class="form-control border-start-0" placeholder="{% trans 'Search by name or SKU...' %}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<select class="form-select">
|
||||||
|
<option value="">{% trans "All Categories" %}</option>
|
||||||
|
{% for category in categories %}
|
||||||
|
<option value="{{ category.id }}">{% if LANGUAGE_CODE == 'ar' %}{{ category.name_ar }}{% else %}{{ category.name_en }}{% endif %}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<button type="button" class="btn btn-light w-100">{% trans "Filter" %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Product Grid -->
|
||||||
|
<div class="row g-4">
|
||||||
|
{% for product in products %}
|
||||||
|
<div class="col-md-6 col-lg-4 col-xl-3">
|
||||||
|
<div class="card product-card h-100 shadow-sm border-0">
|
||||||
|
<div class="position-relative">
|
||||||
|
{% if product.image %}
|
||||||
|
<img src="{{ product.image.url }}" class="card-img-top" alt="{{ product.name_en }}" style="height: 200px; object-fit: cover;">
|
||||||
|
{% else %}
|
||||||
|
<div class="bg-secondary bg-opacity-10 d-flex align-items-center justify-content-center" style="height: 200px;">
|
||||||
|
<i class="bi bi-box-seam text-secondary opacity-25" style="font-size: 4rem;"></i>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<span class="position-absolute top-0 end-0 m-3 badge {% if product.stock_quantity < 5 %}bg-danger{% else %}bg-success{% endif %}">
|
||||||
|
{% trans "In Stock" %}: {{ product.stock_quantity }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="text-muted small mb-1">
|
||||||
|
{% if LANGUAGE_CODE == 'ar' %}{{ product.category.name_ar }}{% else %}{{ product.category.name_en }}{% endif %}
|
||||||
|
</div>
|
||||||
|
<h5 class="card-title fw-bold mb-2">
|
||||||
|
{% if LANGUAGE_CODE == 'ar' %}{{ product.name_ar }}{% else %}{{ product.name_en }}{% endif %}
|
||||||
|
</h5>
|
||||||
|
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||||
|
<h4 class="text-primary fw-bold mb-0">{{ site_settings.currency_symbol }}{{ product.price }}</h4>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-sm btn-outline-light text-dark"><i class="bi bi-pencil"></i></button>
|
||||||
|
<button class="btn btn-sm btn-outline-light text-dark"><i class="bi bi-eye"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="col-12 text-center py-5">
|
||||||
|
<div class="bg-white rounded-4 p-5 shadow-sm">
|
||||||
|
<i class="bi bi-box-seam text-muted opacity-25" style="font-size: 5rem;"></i>
|
||||||
|
<h4 class="mt-3 text-muted">{% trans "Your inventory is empty" %}</h4>
|
||||||
|
<p class="text-muted">{% trans "Start by adding your first product to the system." %}</p>
|
||||||
|
<button class="btn btn-primary mt-3" data-bs-toggle="modal" data-bs-target="#addProductModal">
|
||||||
|
{% trans "Add First Product" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Product Modal -->
|
||||||
|
<div class="modal fade" id="addProductModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content rounded-4 border-0 shadow">
|
||||||
|
<div class="modal-header border-0 pb-0">
|
||||||
|
<h5 class="modal-title fw-bold">{% trans "Add New Product" %}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<form action="{% url 'add_product' %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Name (English)" %}</label>
|
||||||
|
<input type="text" name="name_en" class="form-control rounded-3" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Name (Arabic)" %}</label>
|
||||||
|
<input type="text" name="name_ar" class="form-control rounded-3" dir="rtl" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Category" %}</label>
|
||||||
|
<select name="category" class="form-select rounded-3" required>
|
||||||
|
<option value="">{% trans "Select Category" %}</option>
|
||||||
|
{% for category in categories %}
|
||||||
|
<option value="{{ category.id }}">{% if LANGUAGE_CODE == 'ar' %}{{ category.name_ar }}{% else %}{{ category.name_en }}{% endif %}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold">{% trans "SKU" %}</label>
|
||||||
|
<input type="text" name="sku" class="form-control rounded-3" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Price" %}</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text bg-light border-end-0">{{ site_settings.currency_symbol }}</span>
|
||||||
|
<input type="number" step="0.01" name="price" class="form-control rounded-3 border-start-0" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Initial Stock" %}</label>
|
||||||
|
<input type="number" name="stock" class="form-control rounded-3" value="0" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer border-0">
|
||||||
|
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||||
|
<button type="submit" class="btn btn-primary rounded-3 px-4">{% trans "Save Product" %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
428
core/templates/core/pos.html
Normal file
428
core/templates/core/pos.html
Normal file
@ -0,0 +1,428 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n static %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "POS" %} | {{ site_settings.business_name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<style>
|
||||||
|
.product-card {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
border: none;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
.product-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 10px 20px rgba(0,0,0,0.1) !important;
|
||||||
|
}
|
||||||
|
.cart-container {
|
||||||
|
position: sticky;
|
||||||
|
top: 80px;
|
||||||
|
height: calc(100vh - 120px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.cart-items {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.category-badge {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-right: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #6c757d;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.category-badge.active {
|
||||||
|
background: var(--bs-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Invoice Print Styles */
|
||||||
|
@media print {
|
||||||
|
body * {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
#invoice-print, #invoice-print * {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
#invoice-print {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 80mm;
|
||||||
|
padding: 5mm;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.no-print {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#invoice-print {
|
||||||
|
display: none;
|
||||||
|
width: 80mm;
|
||||||
|
margin: auto;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
padding: 5mm;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
.invoice-header { text-align: center; margin-bottom: 10px; border-bottom: 1px dashed #000; padding-bottom: 10px; }
|
||||||
|
.invoice-title { font-size: 16px; font-weight: bold; margin-bottom: 5px; }
|
||||||
|
.invoice-details { margin-bottom: 10px; font-size: 11px; }
|
||||||
|
.invoice-table { width: 100%; border-collapse: collapse; margin-bottom: 10px; }
|
||||||
|
.invoice-table th { border-bottom: 1px solid #000; text-align: left; padding: 2px 0; font-size: 11px; }
|
||||||
|
.invoice-table td { padding: 4px 0; font-size: 11px; vertical-align: top; }
|
||||||
|
.invoice-total { border-top: 1px dashed #000; padding-top: 5px; }
|
||||||
|
.bilingual { display: flex; justify-content: space-between; font-size: 10px; color: #555; }
|
||||||
|
.rtl { direction: rtl; text-align: right; }
|
||||||
|
.ltr { direction: ltr; text-align: left; }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid px-4">
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Products Section -->
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h4 class="fw-bold mb-0">{% trans "Point of Sale" %}</h4>
|
||||||
|
<div class="input-group w-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" placeholder="{% trans 'Search products...' %}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="category-badge active" data-category="all">{% trans "All" %}</div>
|
||||||
|
{% for category in categories %}
|
||||||
|
<div class="category-badge" data-category="{{ category.id }}">
|
||||||
|
{% if LANGUAGE_CODE == 'ar' %}{{ category.name_ar }}{% else %}{{ category.name_en }}{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row row-cols-1 row-cols-md-3 g-3" id="productGrid">
|
||||||
|
{% for product in products %}
|
||||||
|
<div class="col product-item" data-category="{{ product.category.id }}" data-name-en="{{ product.name_en|lower }}" data-name-ar="{{ product.name_ar }}">
|
||||||
|
<div class="card h-100 shadow-sm product-card p-2" onclick="addToCart({{ product.id }}, '{{ product.name_en|escapejs }}', '{{ product.name_ar|escapejs }}', {{ product.price }})">
|
||||||
|
{% if product.image %}
|
||||||
|
<img src="{{ product.image }}" class="card-img-top rounded-3" alt="{{ product.name_en }}" style="height: 150px; object-fit: cover;">
|
||||||
|
{% else %}
|
||||||
|
<div class="bg-light rounded-3 d-flex align-items-center justify-content-center" style="height: 150px;">
|
||||||
|
<i class="bi bi-image text-muted opacity-25" style="font-size: 3rem;"></i>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="card-body p-2 text-center">
|
||||||
|
<h6 class="fw-bold mb-1">
|
||||||
|
{% if LANGUAGE_CODE == 'ar' %}{{ product.name_ar }}{% else %}{{ product.name_en }}{% endif %}
|
||||||
|
</h6>
|
||||||
|
<p class="text-primary fw-bold mb-0">{{ site_settings.currency_symbol }}{{ product.price }}</p>
|
||||||
|
<small class="text-muted">{% trans "Stock" %}: {{ product.stock_quantity }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cart Section -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 cart-container">
|
||||||
|
<div class="card-header bg-white border-0 pt-4 px-4">
|
||||||
|
<h5 class="fw-bold">{% trans "Current Order" %}</h5>
|
||||||
|
<select id="customerSelect" class="form-select form-select-sm mt-3">
|
||||||
|
<option value="">{% trans "Walking Customer" %}</option>
|
||||||
|
{% for customer in customers %}
|
||||||
|
<option value="{{ customer.id }}">{{ customer.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body px-4 py-2 cart-items">
|
||||||
|
<div id="cartItemsList">
|
||||||
|
<!-- Cart items will be injected here -->
|
||||||
|
</div>
|
||||||
|
<div class="text-center py-5 text-muted opacity-50" id="emptyCartMsg">
|
||||||
|
<i class="bi bi-cart3 display-1 d-block mb-3"></i>
|
||||||
|
{% trans "Your cart is empty" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-footer bg-light border-0 p-4 rounded-bottom-4">
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>{% trans "Subtotal" %}</span>
|
||||||
|
<span id="subtotalAmount">{{ site_settings.currency_symbol }}0.00</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between mb-3">
|
||||||
|
<span class="fw-bold fs-5">{% trans "Total" %}</span>
|
||||||
|
<span class="fw-bold fs-5 text-primary" id="totalAmount">{{ site_settings.currency_symbol }}0.00</span>
|
||||||
|
</div>
|
||||||
|
<button id="payNowBtn" class="btn btn-primary w-100 py-3 fw-bold rounded-3" onclick="checkout()" disabled>
|
||||||
|
{% trans "PAY NOW" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Invoice Template (Hidden) -->
|
||||||
|
<div id="invoice-print">
|
||||||
|
<div class="invoice-header">
|
||||||
|
<div class="invoice-title" id="inv-business-name"></div>
|
||||||
|
<div class="bilingual"><span class="ltr">TAX INVOICE</span><span class="rtl">فاتورة ضريبية</span></div>
|
||||||
|
<div id="inv-business-address" style="font-size: 10px;"></div>
|
||||||
|
<div id="inv-business-phone" style="font-size: 10px;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="invoice-details">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span>Inv #: <span id="inv-id"></span></span>
|
||||||
|
<span class="rtl">رقم الفاتورة: <span id="inv-id-ar"></span></span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span>Date: <span id="inv-date"></span></span>
|
||||||
|
<span class="rtl">التاريخ: <span id="inv-date-ar"></span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table class="invoice-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Item / الصنف</th>
|
||||||
|
<th style="text-align: center;">Qty</th>
|
||||||
|
<th style="text-align: right;">Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="inv-items">
|
||||||
|
<!-- Items injected here -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="invoice-total">
|
||||||
|
<div class="d-flex justify-content-between fw-bold">
|
||||||
|
<span>TOTAL / المجموع</span>
|
||||||
|
<span id="inv-total"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mt-3" style="font-size: 10px; border-top: 1px dashed #000; padding-top: 5px;">
|
||||||
|
THANK YOU / شكراً لزيارتكم
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Receipt Modal -->
|
||||||
|
<div class="modal fade" id="receiptModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-sm">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-body text-center p-4">
|
||||||
|
<i class="bi bi-check-circle-fill text-success display-1 mb-3"></i>
|
||||||
|
<h4 class="fw-bold">{% trans "Success!" %}</h4>
|
||||||
|
<p class="text-muted">{% trans "Transaction completed." %}</p>
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="button" class="btn btn-primary" onclick="printInvoice()">
|
||||||
|
<i class="bi bi-printer me-2"></i> {% trans "Print Invoice" %}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
let cart = [];
|
||||||
|
let lastSaleData = null;
|
||||||
|
const lang = '{{ LANGUAGE_CODE }}';
|
||||||
|
const currency = '{{ site_settings.currency_symbol }}';
|
||||||
|
|
||||||
|
function addToCart(id, nameEn, nameAr, price) {
|
||||||
|
const existing = cart.find(item => item.id === id);
|
||||||
|
if (existing) {
|
||||||
|
existing.quantity += 1;
|
||||||
|
existing.line_total = existing.quantity * price;
|
||||||
|
} else {
|
||||||
|
cart.push({
|
||||||
|
id,
|
||||||
|
name: lang === 'ar' ? nameAr : nameEn,
|
||||||
|
name_en: nameEn,
|
||||||
|
name_ar: nameAr,
|
||||||
|
price,
|
||||||
|
quantity: 1,
|
||||||
|
line_total: price
|
||||||
|
});
|
||||||
|
}
|
||||||
|
renderCart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateQuantity(id, delta) {
|
||||||
|
const item = cart.find(item => item.id === id);
|
||||||
|
if (item) {
|
||||||
|
item.quantity += delta;
|
||||||
|
if (item.quantity <= 0) {
|
||||||
|
cart = cart.filter(i => i.id !== id);
|
||||||
|
} else {
|
||||||
|
item.line_total = item.quantity * item.price;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderCart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCart() {
|
||||||
|
const listContainer = document.getElementById('cartItemsList');
|
||||||
|
const emptyMsg = document.getElementById('emptyCartMsg');
|
||||||
|
const payBtn = document.getElementById('payNowBtn');
|
||||||
|
|
||||||
|
if (cart.length === 0) {
|
||||||
|
emptyMsg.classList.remove('d-none');
|
||||||
|
listContainer.innerHTML = '';
|
||||||
|
document.getElementById('subtotalAmount').innerText = `${currency}0.00`;
|
||||||
|
document.getElementById('totalAmount').innerText = `${currency}0.00`;
|
||||||
|
payBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyMsg.classList.add('d-none');
|
||||||
|
payBtn.disabled = false;
|
||||||
|
let html = '';
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
cart.forEach(item => {
|
||||||
|
total += item.line_total;
|
||||||
|
html += `
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<div>
|
||||||
|
<div class="fw-bold small">${item.name}</div>
|
||||||
|
<div class="text-muted small">${currency}${item.price} x ${item.quantity}</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary rounded-circle" onclick="updateQuantity(${item.id}, -1)">-</button>
|
||||||
|
<span class="fw-bold">${item.quantity}</span>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary rounded-circle" onclick="updateQuantity(${item.id}, 1)">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
listContainer.innerHTML = html;
|
||||||
|
document.getElementById('subtotalAmount').innerText = `${currency}${total.toFixed(2)}`;
|
||||||
|
document.getElementById('totalAmount').innerText = `${currency}${total.toFixed(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkout() {
|
||||||
|
if (cart.length === 0) return;
|
||||||
|
|
||||||
|
const payBtn = document.getElementById('payNowBtn');
|
||||||
|
const originalText = payBtn.innerText;
|
||||||
|
payBtn.disabled = true;
|
||||||
|
payBtn.innerText = '{% trans "Processing..." %}';
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
customer_id: document.getElementById('customerSelect').value,
|
||||||
|
items: cart,
|
||||||
|
total_amount: cart.reduce((acc, item) => acc + item.line_total, 0),
|
||||||
|
discount: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch('{% url "create_sale_api" %}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token }}'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
return response.json().then(err => { throw err; });
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
lastSaleData = data;
|
||||||
|
prepareInvoice(data);
|
||||||
|
cart = [];
|
||||||
|
renderCart();
|
||||||
|
const receiptModal = new bootstrap.Modal(document.getElementById('receiptModal'));
|
||||||
|
receiptModal.show();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + data.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Checkout error:', error);
|
||||||
|
alert('Checkout failed: ' + (error.error || error.message || 'Unknown error'));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
payBtn.disabled = cart.length === 0;
|
||||||
|
payBtn.innerText = originalText;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareInvoice(data) {
|
||||||
|
document.getElementById('inv-business-name').innerText = data.business.name;
|
||||||
|
document.getElementById('inv-business-address').innerText = data.business.address;
|
||||||
|
document.getElementById('inv-business-phone').innerText = 'Tel: ' + data.business.phone;
|
||||||
|
document.getElementById('inv-id').innerText = data.sale.id;
|
||||||
|
document.getElementById('inv-id-ar').innerText = data.sale.id;
|
||||||
|
document.getElementById('inv-date').innerText = data.sale.created_at;
|
||||||
|
document.getElementById('inv-date-ar').innerText = data.sale.created_at;
|
||||||
|
|
||||||
|
let itemsHtml = '';
|
||||||
|
data.sale.items.forEach(item => {
|
||||||
|
itemsHtml += `
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div>${item.name_en}</div>
|
||||||
|
<div class="rtl text-muted" style="font-size: 9px;">${item.name_ar}</div>
|
||||||
|
</td>
|
||||||
|
<td style="text-align: center;">${item.qty}</td>
|
||||||
|
<td style="text-align: right;">${data.business.currency}${item.total.toFixed(2)}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
document.getElementById('inv-items').innerHTML = itemsHtml;
|
||||||
|
document.getElementById('inv-total').innerText = data.business.currency + data.sale.total.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function printInvoice() {
|
||||||
|
window.print();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search and Category Filtering
|
||||||
|
document.getElementById('productSearch').addEventListener('input', filterProducts);
|
||||||
|
document.querySelectorAll('.category-badge').forEach(badge => {
|
||||||
|
badge.addEventListener('click', function() {
|
||||||
|
document.querySelectorAll('.category-badge').forEach(b => b.classList.remove('active'));
|
||||||
|
this.classList.add('active');
|
||||||
|
filterProducts();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function filterProducts() {
|
||||||
|
const searchTerm = document.getElementById('productSearch').value.toLowerCase();
|
||||||
|
const activeCategory = document.querySelector('.category-badge.active').dataset.category;
|
||||||
|
|
||||||
|
document.querySelectorAll('.product-item').forEach(item => {
|
||||||
|
const nameEn = item.dataset.nameEn;
|
||||||
|
const nameAr = item.dataset.nameAr;
|
||||||
|
const category = item.dataset.category;
|
||||||
|
|
||||||
|
const matchesSearch = nameEn.includes(searchTerm) || nameAr.includes(searchTerm);
|
||||||
|
const matchesCategory = activeCategory === 'all' || category === activeCategory;
|
||||||
|
|
||||||
|
if (matchesSearch && matchesCategory) {
|
||||||
|
item.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
item.classList.add('d-none');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
98
core/templates/core/purchases.html
Normal file
98
core/templates/core/purchases.html
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Purchases" %} | Meezan Accounting{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="fw-bold mb-0">{% trans "Purchase History" %}</h2>
|
||||||
|
<button class="btn btn-primary rounded-3" data-bs-toggle="modal" data-bs-target="#addPurchaseModal">
|
||||||
|
<i class="bi bi-cart-plus me-2"></i>{% trans "New Purchase" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if messages %}
|
||||||
|
<div class="mb-4">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags }} alert-dismissible fade show rounded-3" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<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 "Date" %}</th>
|
||||||
|
<th>{% trans "Supplier" %}</th>
|
||||||
|
<th>{% trans "Total Amount" %}</th>
|
||||||
|
<th class="text-end pe-4">{% trans "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for purchase in purchases %}
|
||||||
|
<tr>
|
||||||
|
<td class="ps-4">{{ purchase.created_at|date:"Y-m-d H:i" }}</td>
|
||||||
|
<td>{{ purchase.supplier.name|default:"-" }}</td>
|
||||||
|
<td class="fw-bold text-primary">{{ site_settings.currency_symbol }}{{ purchase.total_amount }}</td>
|
||||||
|
<td class="text-end pe-4">
|
||||||
|
<button class="btn btn-sm btn-light rounded-circle"><i class="bi bi-eye"></i></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center py-5 text-muted">
|
||||||
|
{% trans "No purchases found." %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Purchase Modal -->
|
||||||
|
<div class="modal fade" id="addPurchaseModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content border-0 shadow rounded-4">
|
||||||
|
<div class="modal-header border-0">
|
||||||
|
<h5 class="fw-bold">{% trans "Record New Purchase" %}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<form action="{% url 'add_purchase' %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Supplier" %}</label>
|
||||||
|
<select name="supplier" class="form-select rounded-3" required>
|
||||||
|
<option value="">{% trans "Select Supplier" %}</option>
|
||||||
|
{% for supplier in suppliers %}
|
||||||
|
<option value="{{ supplier.id }}">{{ supplier.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Total Amount" %}</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text bg-light border-end-0">{{ site_settings.currency_symbol }}</span>
|
||||||
|
<input type="number" step="0.01" name="total_amount" class="form-control rounded-3 border-start-0" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer border-0">
|
||||||
|
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||||
|
<button type="submit" class="btn btn-primary rounded-3 px-4">{% trans "Record Purchase" %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
70
core/templates/core/reports.html
Normal file
70
core/templates/core/reports.html
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Smart Reports" %} | Meezan Accounting{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col">
|
||||||
|
<h1 class="h3 fw-bold">{% trans "Analytics & Reports" %}</h1>
|
||||||
|
<p class="text-muted">{% trans "Deep dive into your business performance." %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Monthly Revenue Table -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card border-0 shadow-sm p-4 h-100">
|
||||||
|
<h5 class="fw-bold mb-4">{% trans "Monthly Revenue" %}</h5>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Month" %}</th>
|
||||||
|
<th class="text-end">{% trans "Revenue" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for sale in monthly_sales %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ sale.month|date:"F Y" }}</td>
|
||||||
|
<td class="text-end fw-bold">{{ site_settings.currency_symbol }}{{ sale.total|floatformat:2 }}</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="text-center text-muted">{% trans "No data available." %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top Products -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card border-0 shadow-sm p-4 h-100">
|
||||||
|
<h5 class="fw-bold mb-4">{% trans "Top Selling Products" %}</h5>
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
{% for item in top_products %}
|
||||||
|
<li class="list-group-item px-0 py-3 border-0 d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<p class="mb-0 fw-bold">
|
||||||
|
{% if LANGUAGE_CODE == 'ar' %}{{ item.product__name_ar }}{% else %}{{ item.product__name_en }}{% endif %}
|
||||||
|
</p>
|
||||||
|
<small class="text-muted">{{ item.total_qty }} {% trans "units sold" %}</small>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<span class="d-block fw-bold text-primary">{{ site_settings.currency_symbol }}{{ item.revenue|floatformat:2 }}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<li class="list-group-item px-0 border-0 text-center text-muted">{% trans "No sales data." %}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
102
core/templates/core/settings.html
Normal file
102
core/templates/core/settings.html
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% 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 "System Settings" %}</h1>
|
||||||
|
<p class="text-muted">{% trans "Manage your business profile and preferences." %}</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-8">
|
||||||
|
<div class="card shadow-sm border-0 glassmorphism mb-4">
|
||||||
|
<div class="card-header bg-transparent border-0 py-3">
|
||||||
|
<h5 class="card-title mb-0 fw-bold">{% trans "Business Profile" %}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label class="form-label fw-semibold">{% trans "Business Name" %}</label>
|
||||||
|
<input type="text" name="business_name" class="form-control" value="{{ settings.business_name }}" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold">{% trans "Email Address" %}</label>
|
||||||
|
<input type="email" name="email" class="form-control" value="{{ settings.email }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold">{% trans "Phone Number" %}</label>
|
||||||
|
<input type="text" name="phone" class="form-control" value="{{ settings.phone }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label fw-semibold">{% trans "Address" %}</label>
|
||||||
|
<textarea name="address" class="form-control" rows="3">{{ settings.address }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<h5 class="fw-bold mb-3">{% trans "Financial Preferences" %}</h5>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold">{% trans "Currency Symbol" %}</label>
|
||||||
|
<input type="text" name="currency_symbol" class="form-control" value="{{ settings.currency_symbol }}" required>
|
||||||
|
<div class="form-text">{% trans "e.g., $, £, SAR, AED" %}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold">{% trans "Default Tax Rate (%)" %}</label>
|
||||||
|
<input type="number" step="0.01" name="tax_rate" class="form-control" value="{{ settings.tax_rate }}" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 pt-3 border-top d-flex justify-content-end">
|
||||||
|
<button type="submit" class="btn btn-primary px-4 py-2">
|
||||||
|
<i class="bi bi-check-circle me-2"></i> {% trans "Save Changes" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card shadow-sm border-0 glassmorphism mb-4">
|
||||||
|
<div class="card-header bg-transparent border-0 py-3">
|
||||||
|
<h5 class="card-title mb-0 fw-bold">{% trans "Help & Support" %}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-muted small">
|
||||||
|
{% trans "Need help configuring your smart admin? Check our documentation or contact support." %}
|
||||||
|
</p>
|
||||||
|
<a href="#" class="btn btn-outline-secondary w-100 btn-sm">
|
||||||
|
<i class="bi bi-question-circle me-1"></i> {% trans "Documentation" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm border-0 glassmorphism bg-light">
|
||||||
|
<div class="card-body text-center py-4">
|
||||||
|
<i class="bi bi-info-circle fs-1 text-primary mb-3"></i>
|
||||||
|
<h6 class="fw-bold">{% trans "Smart Admin Version" %}</h6>
|
||||||
|
<p class="text-muted mb-0">v2.1.0-Meezan</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
95
core/templates/core/suppliers.html
Normal file
95
core/templates/core/suppliers.html
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Suppliers" %} | Meezan Accounting{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="fw-bold mb-0">{% trans "Suppliers" %}</h2>
|
||||||
|
<button class="btn btn-primary rounded-3" data-bs-toggle="modal" data-bs-target="#addSupplierModal">
|
||||||
|
<i class="bi bi-truck me-2"></i>{% trans "Add Supplier" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if messages %}
|
||||||
|
<div class="mb-4">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags }} alert-dismissible fade show rounded-3" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<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 "Name" %}</th>
|
||||||
|
<th>{% trans "Contact Person" %}</th>
|
||||||
|
<th>{% trans "Phone" %}</th>
|
||||||
|
<th class="text-end pe-4">{% trans "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for supplier in suppliers %}
|
||||||
|
<tr>
|
||||||
|
<td class="ps-4 fw-bold">{{ supplier.name }}</td>
|
||||||
|
<td>{{ supplier.contact_person }}</td>
|
||||||
|
<td>{{ supplier.phone }}</td>
|
||||||
|
<td class="text-end pe-4">
|
||||||
|
<button class="btn btn-sm btn-light rounded-circle"><i class="bi bi-pencil"></i></button>
|
||||||
|
<button class="btn btn-sm btn-light rounded-circle text-danger"><i class="bi bi-trash"></i></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center py-5 text-muted">
|
||||||
|
{% trans "No suppliers found." %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Supplier Modal -->
|
||||||
|
<div class="modal fade" id="addSupplierModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content border-0 shadow rounded-4">
|
||||||
|
<div class="modal-header border-0">
|
||||||
|
<h5 class="fw-bold">{% trans "Add New Supplier" %}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<form action="{% url 'add_supplier' %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Supplier Name" %}</label>
|
||||||
|
<input type="text" name="name" class="form-control rounded-3" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Contact Person" %}</label>
|
||||||
|
<input type="text" name="contact_person" class="form-control rounded-3">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Phone Number" %}</label>
|
||||||
|
<input type="text" name="phone" class="form-control rounded-3">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer border-0">
|
||||||
|
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||||
|
<button type="submit" class="btn btn-primary rounded-3 px-4">{% trans "Save Supplier" %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
21
core/urls.py
21
core/urls.py
@ -1,7 +1,20 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
from .views import home
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", home, name="home"),
|
path('', views.index, name='index'),
|
||||||
]
|
path('inventory/', views.inventory, name='inventory'),
|
||||||
|
path('pos/', views.pos, name='pos'),
|
||||||
|
path('customers/', views.customers, name='customers'),
|
||||||
|
path('suppliers/', views.suppliers, name='suppliers'),
|
||||||
|
path('purchases/', views.purchases, name='purchases'),
|
||||||
|
path('reports/', views.reports, name='reports'),
|
||||||
|
path('settings/', views.settings_view, name='settings'),
|
||||||
|
|
||||||
|
# API / Actions
|
||||||
|
path('api/create-sale/', views.create_sale_api, name='create_sale_api'),
|
||||||
|
path('customers/add/', views.add_customer, name='add_customer'),
|
||||||
|
path('suppliers/add/', views.add_supplier, name='add_supplier'),
|
||||||
|
path('purchases/add/', views.add_purchase, name='add_purchase'),
|
||||||
|
path('inventory/add/', views.add_product, name='add_product'),
|
||||||
|
]
|
||||||
251
core/views.py
251
core/views.py
@ -1,25 +1,236 @@
|
|||||||
import os
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
import platform
|
from django.db.models import Sum, Count, F
|
||||||
|
from django.db.models.functions import TruncDate, TruncMonth
|
||||||
from django import get_version as django_version
|
from django.http import JsonResponse
|
||||||
from django.shortcuts import render
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from .models import Product, Sale, Category, Customer, Supplier, Purchase, SaleItem, SystemSetting
|
||||||
|
import json
|
||||||
|
from datetime import timedelta
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.contrib import messages
|
||||||
|
|
||||||
|
def index(request):
|
||||||
def home(request):
|
"""
|
||||||
"""Render the landing screen with loader and environment details."""
|
Enhanced Meezan Dashboard View
|
||||||
host_name = request.get_host().lower()
|
"""
|
||||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
# Summary Stats
|
||||||
now = timezone.now()
|
total_products = Product.objects.count()
|
||||||
|
total_sales_count = Sale.objects.count()
|
||||||
|
total_sales_amount = Sale.objects.aggregate(total=Sum('total_amount'))['total'] or 0
|
||||||
|
total_customers = Customer.objects.count()
|
||||||
|
|
||||||
|
# Stock Alert (Low stock < 5)
|
||||||
|
low_stock_products = Product.objects.filter(stock_quantity__lt=5)
|
||||||
|
|
||||||
|
# Recent Transactions
|
||||||
|
recent_sales = Sale.objects.order_by('-created_at')[:5]
|
||||||
|
|
||||||
|
# Chart Data: Sales for the last 7 days
|
||||||
|
seven_days_ago = timezone.now().date() - timedelta(days=6)
|
||||||
|
sales_over_time = Sale.objects.filter(created_at__date__gte=seven_days_ago) \
|
||||||
|
.annotate(date=TruncDate('created_at')) \
|
||||||
|
.values('date') \
|
||||||
|
.annotate(total=Sum('total_amount')) \
|
||||||
|
.order_by('date')
|
||||||
|
|
||||||
|
# Prepare data for Chart.js
|
||||||
|
chart_labels = []
|
||||||
|
chart_data = []
|
||||||
|
|
||||||
|
date_dict = {s['date']: float(s['total']) for s in sales_over_time}
|
||||||
|
for i in range(7):
|
||||||
|
date = seven_days_ago + timedelta(days=i)
|
||||||
|
chart_labels.append(date.strftime('%b %d'))
|
||||||
|
chart_data.append(date_dict.get(date, 0))
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"project_name": "New Style",
|
'total_products': total_products,
|
||||||
"agent_brand": agent_brand,
|
'total_sales_count': total_sales_count,
|
||||||
"django_version": django_version(),
|
'total_sales_amount': total_sales_amount,
|
||||||
"python_version": platform.python_version(),
|
'total_customers': total_customers,
|
||||||
"current_time": now,
|
'low_stock_products': low_stock_products,
|
||||||
"host_name": host_name,
|
'recent_sales': recent_sales,
|
||||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
'chart_labels': json.dumps(chart_labels),
|
||||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
'chart_data': json.dumps(chart_data),
|
||||||
}
|
}
|
||||||
return render(request, "core/index.html", context)
|
return render(request, 'core/index.html', context)
|
||||||
|
|
||||||
|
def inventory(request):
|
||||||
|
products = Product.objects.all().select_related('category')
|
||||||
|
categories = Category.objects.all()
|
||||||
|
context = {'products': products, 'categories': categories}
|
||||||
|
return render(request, 'core/inventory.html', context)
|
||||||
|
|
||||||
|
def pos(request):
|
||||||
|
products = Product.objects.all().filter(stock_quantity__gt=0)
|
||||||
|
customers = Customer.objects.all()
|
||||||
|
categories = Category.objects.all()
|
||||||
|
context = {'products': products, 'customers': customers, 'categories': categories}
|
||||||
|
return render(request, 'core/pos.html', context)
|
||||||
|
|
||||||
|
def customers(request):
|
||||||
|
customers_list = Customer.objects.all().annotate(total_sales=Sum('sale__total_amount'))
|
||||||
|
context = {'customers': customers_list}
|
||||||
|
return render(request, 'core/customers.html', context)
|
||||||
|
|
||||||
|
def suppliers(request):
|
||||||
|
suppliers_list = Supplier.objects.all()
|
||||||
|
context = {'suppliers': suppliers_list}
|
||||||
|
return render(request, 'core/suppliers.html', context)
|
||||||
|
|
||||||
|
def purchases(request):
|
||||||
|
purchases_list = Purchase.objects.all().select_related('supplier')
|
||||||
|
suppliers_list = Supplier.objects.all()
|
||||||
|
context = {'purchases': purchases_list, 'suppliers': suppliers_list}
|
||||||
|
return render(request, 'core/purchases.html', context)
|
||||||
|
|
||||||
|
def reports(request):
|
||||||
|
"""
|
||||||
|
Smart Reports View
|
||||||
|
"""
|
||||||
|
# Monthly Revenue
|
||||||
|
monthly_sales = Sale.objects.annotate(month=TruncMonth('created_at')) \
|
||||||
|
.values('month') \
|
||||||
|
.annotate(total=Sum('total_amount')) \
|
||||||
|
.order_by('-month')[:12]
|
||||||
|
|
||||||
|
# Top Selling Products
|
||||||
|
top_products = SaleItem.objects.values('product__name_en', 'product__name_ar') \
|
||||||
|
.annotate(total_qty=Sum('quantity'), revenue=Sum('line_total')) \
|
||||||
|
.order_by('-total_qty')[:5]
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'monthly_sales': monthly_sales,
|
||||||
|
'top_products': top_products,
|
||||||
|
}
|
||||||
|
return render(request, 'core/reports.html', context)
|
||||||
|
|
||||||
|
def settings_view(request):
|
||||||
|
"""
|
||||||
|
Smart Admin Settings View
|
||||||
|
"""
|
||||||
|
settings = SystemSetting.objects.first()
|
||||||
|
if not settings:
|
||||||
|
settings = SystemSetting.objects.create()
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
settings.business_name = request.POST.get('business_name')
|
||||||
|
settings.address = request.POST.get('address')
|
||||||
|
settings.phone = request.POST.get('phone')
|
||||||
|
settings.email = request.POST.get('email')
|
||||||
|
settings.currency_symbol = request.POST.get('currency_symbol')
|
||||||
|
settings.tax_rate = request.POST.get('tax_rate')
|
||||||
|
settings.save()
|
||||||
|
messages.success(request, "Settings updated successfully!")
|
||||||
|
return redirect('settings')
|
||||||
|
|
||||||
|
return render(request, 'core/settings.html', {'settings': settings})
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def create_sale_api(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
try:
|
||||||
|
data = json.loads(request.body)
|
||||||
|
customer_id = data.get('customer_id')
|
||||||
|
items = data.get('items', [])
|
||||||
|
total_amount = data.get('total_amount', 0)
|
||||||
|
discount = data.get('discount', 0)
|
||||||
|
|
||||||
|
customer = None
|
||||||
|
if customer_id:
|
||||||
|
customer = Customer.objects.get(id=customer_id)
|
||||||
|
|
||||||
|
sale = Sale.objects.create(
|
||||||
|
customer=customer,
|
||||||
|
total_amount=total_amount,
|
||||||
|
discount=discount
|
||||||
|
)
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
product = Product.objects.get(id=item['id'])
|
||||||
|
SaleItem.objects.create(
|
||||||
|
sale=sale,
|
||||||
|
product=product,
|
||||||
|
quantity=item['quantity'],
|
||||||
|
unit_price=item['price'],
|
||||||
|
line_total=item['line_total']
|
||||||
|
)
|
||||||
|
product.stock_quantity -= item['quantity']
|
||||||
|
product.save()
|
||||||
|
|
||||||
|
settings = SystemSetting.objects.first()
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'sale_id': sale.id,
|
||||||
|
'business': {
|
||||||
|
'name': settings.business_name,
|
||||||
|
'address': settings.address,
|
||||||
|
'phone': settings.phone,
|
||||||
|
'currency': settings.currency_symbol
|
||||||
|
},
|
||||||
|
'sale': {
|
||||||
|
'id': sale.id,
|
||||||
|
'created_at': sale.created_at.strftime("%Y-%m-%d %H:%M"),
|
||||||
|
'total': float(sale.total_amount),
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'name_en': item.product.name_en,
|
||||||
|
'name_ar': item.product.name_ar,
|
||||||
|
'qty': item.quantity,
|
||||||
|
'price': float(item.unit_price),
|
||||||
|
'total': float(item.line_total)
|
||||||
|
} for item in sale.items.all()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'success': False, 'error': str(e)}, status=400)
|
||||||
|
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
|
||||||
|
|
||||||
|
def add_customer(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
name = request.POST.get('name')
|
||||||
|
phone = request.POST.get('phone')
|
||||||
|
email = request.POST.get('email')
|
||||||
|
address = request.POST.get('address')
|
||||||
|
Customer.objects.create(name=name, phone=phone, email=email, address=address)
|
||||||
|
messages.success(request, "Customer added successfully!")
|
||||||
|
return redirect('customers')
|
||||||
|
|
||||||
|
def add_supplier(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
name = request.POST.get('name')
|
||||||
|
contact_person = request.POST.get('contact_person')
|
||||||
|
phone = request.POST.get('phone')
|
||||||
|
Supplier.objects.create(name=name, contact_person=contact_person, phone=phone)
|
||||||
|
messages.success(request, "Supplier added successfully!")
|
||||||
|
return redirect('suppliers')
|
||||||
|
|
||||||
|
def add_purchase(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
supplier_id = request.POST.get('supplier')
|
||||||
|
total_amount = request.POST.get('total_amount')
|
||||||
|
supplier = get_object_or_404(Supplier, id=supplier_id)
|
||||||
|
Purchase.objects.create(supplier=supplier, total_amount=total_amount)
|
||||||
|
messages.success(request, "Purchase recorded successfully!")
|
||||||
|
return redirect('purchases')
|
||||||
|
|
||||||
|
def add_product(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
name_en = request.POST.get('name_en')
|
||||||
|
name_ar = request.POST.get('name_ar')
|
||||||
|
category_id = request.POST.get('category')
|
||||||
|
sku = request.POST.get('sku')
|
||||||
|
price = request.POST.get('price')
|
||||||
|
stock = request.POST.get('stock')
|
||||||
|
category = get_object_or_404(Category, id=category_id)
|
||||||
|
Product.objects.create(
|
||||||
|
name_en=name_en,
|
||||||
|
name_ar=name_ar,
|
||||||
|
category=category,
|
||||||
|
sku=sku,
|
||||||
|
price=price,
|
||||||
|
stock_quantity=stock
|
||||||
|
)
|
||||||
|
messages.success(request, "Product added successfully!")
|
||||||
|
return redirect('inventory')
|
||||||
|
|||||||
@ -1,4 +1,159 @@
|
|||||||
/* Custom styles for the application */
|
/* Meezan Accounting Custom Styles */
|
||||||
body {
|
:root {
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
--meezan-primary: #2E5BFF;
|
||||||
|
--meezan-secondary: #8C9EFF;
|
||||||
|
--meezan-accent: #FFAB40;
|
||||||
|
--meezan-bg: #F8FAFF;
|
||||||
|
--meezan-sidebar-bg: #FFFFFF;
|
||||||
|
--meezan-text: #1A202C;
|
||||||
|
--meezan-glass: rgba(255, 255, 255, 0.7);
|
||||||
|
--sidebar-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Plus Jakarta Sans', 'Cairo', sans-serif;
|
||||||
|
background-color: var(--meezan-bg);
|
||||||
|
color: var(--meezan-text);
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RTL Specific Tweaks */
|
||||||
|
[dir="rtl"] {
|
||||||
|
font-family: 'Cairo', 'Plus Jakarta Sans', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-primary {
|
||||||
|
background-color: var(--meezan-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-primary {
|
||||||
|
color: var(--meezan-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glassmorphism Effect */
|
||||||
|
.glass-card {
|
||||||
|
background: var(--meezan-glass);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Styling */
|
||||||
|
#wrapper {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar {
|
||||||
|
min-width: var(--sidebar-width);
|
||||||
|
max-width: var(--sidebar-width);
|
||||||
|
background: var(--meezan-sidebar-bg);
|
||||||
|
color: var(--meezan-text);
|
||||||
|
transition: all 0.3s;
|
||||||
|
border-inline-end: 1px solid rgba(0,0,0,0.05);
|
||||||
|
z-index: 1000;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar.active {
|
||||||
|
margin-inline-start: calc(-1 * var(--sidebar-width));
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar .sidebar-header {
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--meezan-sidebar-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar ul.components {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar ul li a {
|
||||||
|
padding: 12px 25px;
|
||||||
|
font-size: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--meezan-text);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s;
|
||||||
|
border-radius: 0 50px 50px 0;
|
||||||
|
margin-inline-end: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] #sidebar ul li a {
|
||||||
|
border-radius: 50px 0 0 50px;
|
||||||
|
margin-inline-start: 15px;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar ul li a:hover, #sidebar ul li a.active {
|
||||||
|
background: rgba(46, 91, 255, 0.1);
|
||||||
|
color: var(--meezan-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar ul li a i {
|
||||||
|
margin-inline-end: 15px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content Styling */
|
||||||
|
#content {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-navbar {
|
||||||
|
padding: 15px 30px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero Section */
|
||||||
|
.hero-gradient {
|
||||||
|
background: linear-gradient(135deg, var(--meezan-primary) 0%, var(--meezan-secondary) 100%);
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 3rem;
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--meezan-primary);
|
||||||
|
border: none;
|
||||||
|
padding: 0.6rem 1.5rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Stats */
|
||||||
|
.stat-card {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Tweaks */
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
#sidebar {
|
||||||
|
margin-inline-start: calc(-1 * var(--sidebar-width));
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
#sidebar.active {
|
||||||
|
margin-inline-start: 0;
|
||||||
|
}
|
||||||
|
#content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +1,159 @@
|
|||||||
|
/* Meezan Accounting Custom Styles */
|
||||||
:root {
|
:root {
|
||||||
--bg-color-start: #6a11cb;
|
--meezan-primary: #2E5BFF;
|
||||||
--bg-color-end: #2575fc;
|
--meezan-secondary: #8C9EFF;
|
||||||
--text-color: #ffffff;
|
--meezan-accent: #FFAB40;
|
||||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
--meezan-bg: #F8FAFF;
|
||||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
--meezan-sidebar-bg: #FFFFFF;
|
||||||
|
--meezan-text: #1A202C;
|
||||||
|
--meezan-glass: rgba(255, 255, 255, 0.7);
|
||||||
|
--sidebar-width: 260px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
font-family: 'Plus Jakarta Sans', 'Cairo', sans-serif;
|
||||||
font-family: 'Inter', sans-serif;
|
background-color: var(--meezan-bg);
|
||||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
color: var(--meezan-text);
|
||||||
color: var(--text-color);
|
overflow-x: hidden;
|
||||||
display: flex;
|
}
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
/* RTL Specific Tweaks */
|
||||||
min-height: 100vh;
|
[dir="rtl"] {
|
||||||
text-align: center;
|
font-family: 'Cairo', 'Plus Jakarta Sans', sans-serif;
|
||||||
overflow: hidden;
|
}
|
||||||
position: relative;
|
|
||||||
|
.bg-primary {
|
||||||
|
background-color: var(--meezan-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-primary {
|
||||||
|
color: var(--meezan-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glassmorphism Effect */
|
||||||
|
.glass-card {
|
||||||
|
background: var(--meezan-glass);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Styling */
|
||||||
|
#wrapper {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar {
|
||||||
|
min-width: var(--sidebar-width);
|
||||||
|
max-width: var(--sidebar-width);
|
||||||
|
background: var(--meezan-sidebar-bg);
|
||||||
|
color: var(--meezan-text);
|
||||||
|
transition: all 0.3s;
|
||||||
|
border-inline-end: 1px solid rgba(0,0,0,0.05);
|
||||||
|
z-index: 1000;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar.active {
|
||||||
|
margin-inline-start: calc(-1 * var(--sidebar-width));
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar .sidebar-header {
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--meezan-sidebar-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar ul.components {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar ul li a {
|
||||||
|
padding: 12px 25px;
|
||||||
|
font-size: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--meezan-text);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s;
|
||||||
|
border-radius: 0 50px 50px 0;
|
||||||
|
margin-inline-end: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] #sidebar ul li a {
|
||||||
|
border-radius: 50px 0 0 50px;
|
||||||
|
margin-inline-start: 15px;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar ul li a:hover, #sidebar ul li a.active {
|
||||||
|
background: rgba(46, 91, 255, 0.1);
|
||||||
|
color: var(--meezan-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar ul li a i {
|
||||||
|
margin-inline-end: 15px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content Styling */
|
||||||
|
#content {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-navbar {
|
||||||
|
padding: 15px 30px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero Section */
|
||||||
|
.hero-gradient {
|
||||||
|
background: linear-gradient(135deg, var(--meezan-primary) 0%, var(--meezan-secondary) 100%);
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 3rem;
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--meezan-primary);
|
||||||
|
border: none;
|
||||||
|
padding: 0.6rem 1.5rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Stats */
|
||||||
|
.stat-card {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Tweaks */
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
#sidebar {
|
||||||
|
margin-inline-start: calc(-1 * var(--sidebar-width));
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
#sidebar.active {
|
||||||
|
margin-inline-start: 0;
|
||||||
|
}
|
||||||
|
#content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user