Revert to version 2ad0af1

This commit is contained in:
Flatlogic Bot 2026-02-06 03:57:27 +00:00
parent 75d9aea042
commit 77e86e4c7b
22 changed files with 19 additions and 836 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

View File

@ -8,12 +8,9 @@ https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
""" """
import os import os
import pymysql
pymysql.install_as_MySQLdb()
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = get_wsgi_application() application = get_wsgi_application()

View File

@ -1,461 +0,0 @@
{% extends 'base.html' %}
{% load i18n static %}
{% block title %}{% trans "Customer Display" %} | {{ site_settings.business_name }}{% endblock %}
{% block head %}
<style>
/* --- Critical Layout Overrides for Full Screen --- */
#sidebar,
.navbar,
.top-navbar,
#sidebarCollapse,
.mobile-cart-toggle,
footer {
display: none !important;
}
#wrapper {
display: block !important;
width: 100% !important;
height: 100vh !important;
overflow: hidden !important;
}
#content {
margin-left: 0 !important;
width: 100% !important;
height: 100% !important;
padding: 0 !important;
overflow: hidden !important;
}
main {
padding: 0 !important;
height: 100% !important;
margin: 0 !important;
}
body {
overflow: hidden;
background-color: #f8f9fa;
padding: 0 !important;
margin: 0 !important;
}
/* --- Customer Display Specific Styles --- */
.customer-display-container {
height: 100vh;
width: 100vw;
display: flex;
flex-direction: row;
position: relative;
}
.left-panel {
flex: 1; /* Takes 50% or remaining space */
background: white;
padding: 2rem;
display: flex;
flex-direction: column;
box-shadow: 2px 0 10px rgba(0,0,0,0.05);
z-index: 10;
height: 100%;
max-width: 50%; /* limit width to half screen */
}
.right-panel {
flex: 1;
background: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
flex-direction: column;
height: 100%;
}
.cart-items-container {
flex-grow: 1;
overflow-y: auto;
margin-bottom: 2rem;
padding-right: 10px; /* space for scrollbar */
}
/* Custom Scrollbar for items */
.cart-items-container::-webkit-scrollbar {
width: 6px;
}
.cart-items-container::-webkit-scrollbar-thumb {
background-color: #dee2e6;
border-radius: 4px;
}
.cart-item {
border-bottom: 1px solid #eee;
padding: 1rem 0;
display: flex;
justify-content: space-between;
align-items: center;
animation: fadeIn 0.3s ease-in-out;
}
.cart-item:last-child {
border-bottom: none;
}
.item-name {
font-weight: 600;
font-size: 1.2rem;
color: #212529;
}
.item-price {
color: #6c757d;
font-size: 0.9rem;
}
.item-total {
font-weight: 700;
font-size: 1.3rem;
color: var(--bs-primary);
}
.total-section {
background: var(--bs-primary);
color: white;
padding: 2.5rem;
border-radius: 1.5rem;
box-shadow: 0 10px 30px rgba(0,0,0,0.15);
margin-bottom: 2rem;
width: 100%;
max-width: 500px;
}
.total-label {
font-size: 1.4rem;
opacity: 0.9;
margin-bottom: 0.5rem;
}
.total-amount {
font-size: 4.5rem;
font-weight: 800;
line-height: 1;
}
.logo-placeholder {
max-width: 300px;
opacity: 0.5;
margin-bottom: 2rem;
}
.promo-card {
flex: 1;
width: 100%;
max-width: 500px;
background: white;
border-radius: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding: 2rem;
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Connection Status Dot */
.status-dot {
position: absolute;
top: 10px;
right: 10px;
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #dc3545; /* Red initially */
z-index: 100;
transition: background-color 0.3s;
border: 2px solid white;
box-shadow: 0 0 5px rgba(0,0,0,0.2);
}
.status-dot.connected {
background-color: #198754; /* Green */
}
/* Debug Panel */
#debugPanel {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background: rgba(0,0,0,0.8);
color: #0f0;
font-family: monospace;
font-size: 10px;
padding: 10px;
max-height: 150px;
overflow-y: auto;
display: none; /* Hidden by default, toggleable */
z-index: 2000;
}
.debug-toggle {
position: absolute;
bottom: 10px;
left: 10px;
font-size: 10px;
opacity: 0.3;
z-index: 2001;
cursor: pointer;
}
</style>
{% endblock %}
{% block content %}
<div class="customer-display-container">
<div id="connectionStatus" class="status-dot" title="Connection Status"></div>
<div class="debug-toggle" onclick="toggleDebug()">Debug</div>
<!-- Left Panel: Cart Items -->
<div class="left-panel">
<div class="d-flex align-items-center mb-4 border-bottom pb-3">
{% if site_settings.logo %}
<img src="{{ site_settings.logo.url }}" alt="Logo" height="50" class="me-3">
{% endif %}
<div>
<h2 class="fw-bold mb-0 text-dark">{{ site_settings.business_name|default:"POS Display" }}</h2>
<div class="text-muted small">{% trans "Your Order Details" %}</div>
</div>
</div>
<div class="cart-items-container" id="cartItems">
<!-- Default Empty State -->
<div class="h-100 d-flex flex-column align-items-center justify-content-center text-muted opacity-50">
<i class="bi bi-basket display-1 mb-3"></i>
<h3>{% trans "Welcome!" %}</h3>
<p>{% trans "Items will appear here as scanned." %}</p>
</div>
</div>
<div class="mt-auto border-top pt-3">
<div class="d-flex justify-content-between mb-2 fs-5">
<span class="text-muted">{% trans "Subtotal" %}</span>
<span id="subtotalDisplay" class="fw-bold">---</span>
</div>
<div class="d-flex justify-content-between mb-2 fs-5">
<span class="text-muted">{% trans "VAT" %}</span>
<span id="vatDisplay" class="fw-bold">---</span>
</div>
<div class="d-flex justify-content-between mb-2 fs-5 text-danger" style="visibility: hidden;">
<span class="text-muted">{% trans "Discount" %}</span>
<span id="discountDisplay" class="fw-bold">---</span>
</div>
</div>
</div>
<!-- Right Panel: Big Total / Ads -->
<div class="right-panel">
<div class="total-section text-center">
<div class="total-label">{% trans "Total to Pay" %}</div>
<div class="total-amount" id="totalDisplay">0.00</div>
</div>
<div class="promo-card">
<div class="d-flex flex-column justify-content-center h-100">
<div class="display-1 text-primary mb-3"><i class="bi bi-emoji-smile"></i></div>
<h3 class="fw-bold text-dark mb-2">{% trans "Thank You!" %}</h3>
<p class="text-muted fs-5">{% trans "We appreciate your business." %}</p>
<div class="d-flex gap-2 justify-content-center mt-3">
<button class="btn btn-sm btn-outline-secondary" onclick="requestSync()">{% trans "Refresh" %}</button>
<button class="btn btn-sm btn-outline-danger" onclick="hardReset()">{% trans "Reset" %}</button>
</div>
</div>
</div>
</div>
<div id="debugPanel">Waiting for data...</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const channel = new BroadcastChannel('pos_channel');
let currencySymbol = '{{ site_settings.currency_symbol|escapejs }}';
if (!currencySymbol) currencySymbol = '{{ settings.currency_symbol|default:"$"|escapejs }}';
const statusDot = document.getElementById('connectionStatus');
const debugPanel = document.getElementById('debugPanel');
function log(msg) {
const time = new Date().toLocaleTimeString();
debugPanel.innerHTML = `<div>[${time}] ${msg}</div>` + debugPanel.innerHTML;
// console.log(msg); // Optional: keep console clean or enable for dev
}
function toggleDebug() {
debugPanel.style.display = debugPanel.style.display === 'none' ? 'block' : 'none';
}
function setConnected(isConnected) {
if (isConnected) {
statusDot.classList.add('connected');
statusDot.title = "Connected";
} else {
statusDot.classList.remove('connected');
statusDot.title = "Disconnected";
}
}
function requestSync() {
log("Requesting sync...");
channel.postMessage({ type: 'request_state' });
if (window.opener) {
window.opener.postMessage({ type: 'request_state' }, '*');
}
}
function hardReset() {
localStorage.removeItem('pos_cart_state');
location.reload();
}
// --- State Management ---
let lastTimestamp = 0;
function handleUpdate(data) {
if (!data || !data.items) return;
// Avoid re-rendering if timestamp hasn't changed
if (data.timestamp && data.timestamp <= lastTimestamp) {
return;
}
lastTimestamp = data.timestamp || Date.now();
setConnected(true);
renderCart(data);
}
// 1. Initial Load
try {
const savedState = localStorage.getItem('pos_cart_state');
if (savedState) {
log("Loaded LocalStorage");
handleUpdate(JSON.parse(savedState));
}
} catch (e) {
log("LS Error: " + e.message);
}
// 2. Broadcast Channel
channel.onmessage = (event) => {
log("Broadcast: " + event.data.type);
if (event.data.type === 'update') {
handleUpdate(event.data);
localStorage.setItem('pos_cart_state', JSON.stringify(event.data));
} else if (event.data.type === 'clear') {
clearDisplay();
localStorage.removeItem('pos_cart_state');
}
};
// 3. Polling LocalStorage (Robust Fallback)
setInterval(() => {
try {
const savedState = localStorage.getItem('pos_cart_state');
if (savedState) {
const data = JSON.parse(savedState);
if (data.timestamp > lastTimestamp) {
log("Polling found update");
handleUpdate(data);
}
}
} catch(e) {}
}, 500);
// 4. PostMessage (Iframe/Opener)
window.addEventListener('message', (event) => {
if (event.data && event.data.type === 'update') {
log("PostMessage received");
handleUpdate(event.data);
localStorage.setItem('pos_cart_state', JSON.stringify(event.data));
}
});
// Request initial state
setTimeout(requestSync, 500);
function renderCart(data) {
const container = document.getElementById('cartItems');
const items = data.items || [];
if (items.length === 0) {
clearDisplay();
return;
}
let html = '';
items.forEach(item => {
const nameAr = item.name_ar || item.name_en || 'Unknown';
const nameEn = item.name_en || '';
const price = parseFloat(item.price || 0).toFixed(2);
const qty = parseFloat(item.quantity || 1).toFixed(2);
const lineTotal = parseFloat(item.line_total || 0).toFixed(2);
html += `
<div class="cart-item">
<div>
<div class="item-name">${nameAr}</div>
<div class="small text-muted mb-1">${nameEn}</div>
<div class="item-price">${currencySymbol} ${price} x ${qty}</div>
</div>
<div class="item-total">
${currencySymbol} ${lineTotal}
</div>
</div>
`;
});
container.innerHTML = html;
// Auto scroll
setTimeout(() => container.scrollTop = container.scrollHeight, 50);
const subtotal = parseFloat(data.subtotal || 0).toFixed(2);
const vat = parseFloat(data.vat || 0).toFixed(2);
const total = parseFloat(data.total || 0).toFixed(2);
document.getElementById('subtotalDisplay').innerText = `${currencySymbol} ${subtotal}`;
document.getElementById('vatDisplay').innerText = `${currencySymbol} ${vat}`;
const discountVal = parseFloat(data.discount || 0);
const discountEl = document.getElementById('discountDisplay');
if (discountEl) {
const discountRow = discountEl.parentElement;
if (discountVal > 0) {
discountEl.innerText = `-${currencySymbol} ${discountVal.toFixed(2)}`;
discountRow.style.visibility = 'visible';
} else {
discountRow.style.visibility = 'hidden';
}
}
document.getElementById('totalDisplay').innerText = `${currencySymbol} ${total}`;
}
function clearDisplay() {
document.getElementById('cartItems').innerHTML = `
<div class="h-100 d-flex flex-column align-items-center justify-content-center text-muted opacity-50">
<i class="bi bi-basket display-1 mb-3"></i>
<h3>{% trans "Welcome!" %}</h3>
<p>{% trans "Items will appear here as scanned." %}</p>
</div>
`;
document.getElementById('subtotalDisplay').innerText = '---';
document.getElementById('vatDisplay').innerText = '---';
const discountEl = document.getElementById('discountDisplay');
if (discountEl) {
discountEl.innerText = '---';
discountEl.parentElement.style.visibility = 'hidden';
}
document.getElementById('totalDisplay').innerText = `${currencySymbol} 0.00`;
}
</script>
{% endblock %}

View File

@ -128,9 +128,6 @@
<div class="col-lg-8"> <div class="col-lg-8">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-4 gap-3"> <div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-4 gap-3">
<h4 class="fw-bold mb-0">{% trans "Point of Sale" %}</h4> <h4 class="fw-bold mb-0">{% trans "Point of Sale" %}</h4>
<button class="btn btn-sm btn-outline-secondary ms-3" onclick="openCustomerDisplay()" title="{% trans 'Open Customer Screen' %}">
<i class="bi bi-display"></i> <span class="d-none d-md-inline">{% trans "Customer Screen" %}</span>
</button>
<div class="input-group w-100 w-md-50"> <div class="input-group w-100 w-md-50">
<span class="input-group-text bg-white border-end-0"><i class="bi bi-search"></i></span> <span class="input-group-text bg-white border-end-0"><i class="bi bi-search"></i></span>
<input type="text" id="productSearch" class="form-control border-start-0 shadow-none" placeholder="{% trans 'Search products...' %}"> <input type="text" id="productSearch" class="form-control border-start-0 shadow-none" placeholder="{% trans 'Search products...' %}">
@ -518,71 +515,6 @@
{% localize off %} {% localize off %}
<script> <script>
let cart = []; let cart = [];
const posChannel = new BroadcastChannel('pos_channel');
// Listen for requests from the customer display (BroadcastChannel)
posChannel.onmessage = (event) => {
if (event.data.type === 'request_state') {
broadcastCart();
}
};
// Listen for direct messages from the popup (fallback for iframe restrictions)
window.addEventListener('message', (event) => {
if (event.data && event.data.type === 'request_state') {
// Reply directly to the source window
broadcastCart(event.source);
}
});
// Force sync when returning to this tab
window.addEventListener('focus', () => {
broadcastCart();
});
function openCustomerDisplay() {
const win = window.open('{% url "customer_display" %}', 'CustomerDisplay', 'width=1000,height=800,menubar=no,toolbar=no,location=no,status=no');
// Give it a moment to load and then force push data
setTimeout(() => broadcastCart(), 1000);
setTimeout(() => broadcastCart(), 3000);
}
function broadcastCart(targetWindow = null) {
const subtotal = cart.reduce((acc, item) => acc + item.line_total, 0);
const totalVat = cart.reduce((acc, item) => acc + (item.line_total * (item.vat_rate / 100)), 0);
const discount = parseFloat(document.getElementById('discountInput').value) || 0;
const total = Math.max(0, subtotal + totalVat - discount);
const payload = {
type: 'update',
items: cart,
subtotal: subtotal,
vat: totalVat,
discount: discount,
total: total,
timestamp: Date.now()
};
// 1. Broadcast Channel (Primary, Real-time)
posChannel.postMessage(payload);
// 2. Local Storage (Backup/Sync)
localStorage.setItem('pos_cart_state', JSON.stringify(payload));
// 3. Direct PostMessage (Fallback if target window requested it)
if (targetWindow) {
targetWindow.postMessage(payload, '*');
}
}
// Heartbeat: Ensure customer display stays in sync even if events are missed
setInterval(() => {
if (cart.length > 0) {
broadcastCart();
}
}, 3000);
let lastSaleData = null; let lastSaleData = null;
let selectedPaymentMethodId = null; let selectedPaymentMethodId = null;
let customerLoyalty = null; let customerLoyalty = null;
@ -609,8 +541,8 @@
} else { } else {
cart.push({ cart.push({
id, id,
name_en: nameEn || '', name_en: nameEn,
name_ar: nameAr || nameEn || '', name_ar: nameAr,
price, price,
vat_rate: vatRate, vat_rate: vatRate,
quantity: 1, quantity: 1,
@ -651,7 +583,6 @@
document.getElementById('subtotalAmount').innerText = `${currency} ${formatAmount(subtotal)}`; document.getElementById('subtotalAmount').innerText = `${currency} ${formatAmount(subtotal)}`;
document.getElementById('taxAmount').innerText = `${currency} ${formatAmount(totalVat)}`; document.getElementById('taxAmount').innerText = `${currency} ${formatAmount(totalVat)}`;
document.getElementById('totalAmount').innerText = `${currency} ${formatAmount(total)}`; document.getElementById('totalAmount').innerText = `${currency} ${formatAmount(total)}`;
broadcastCart();
} }
function renderCart() { function renderCart() {
@ -670,10 +601,6 @@
updateTotals(); updateTotals();
payBtn.disabled = true; payBtn.disabled = true;
holdBtn.disabled = true; holdBtn.disabled = true;
// Clear customer display as well
posChannel.postMessage({ type: 'clear' });
localStorage.removeItem('pos_cart_state');
return; return;
} }

View File

@ -50,12 +50,6 @@
<i class="bi bi-whatsapp me-2"></i>{% trans "WhatsApp Gateway" %} <i class="bi bi-whatsapp me-2"></i>{% trans "WhatsApp Gateway" %}
</button> </button>
</li> </li>
<li class="nav-item" role="presentation">
<button class="nav-link fw-bold px-4" id="backup-tab" data-bs-toggle="pill" data-bs-target="#backup" type="button" role="tab">
<i class="bi bi-hdd-network me-2"></i>{% trans "System Backup" %}
</button>
</li>
</ul> </ul>
<div class="tab-content" id="settingsTabsContent"> <div class="tab-content" id="settingsTabsContent">
@ -631,50 +625,6 @@
</div> </div>
</div> </div>
<!-- Backup Tab -->
<div class="tab-pane fade" id="backup" role="tabpanel">
<div class="row">
<div class="col-lg-6">
<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 "Backup Database" %}</h5>
</div>
<div class="card-body">
<p class="text-muted">{% trans "Download a complete SQL dump of your database. Keep this file safe." %}</p>
<a href="{% url 'backup_database' %}" class="btn btn-primary w-100">
<i class="bi bi-download me-2"></i> {% trans "Download Backup" %}
</a>
</div>
</div>
</div>
<div class="col-lg-6">
<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 text-danger">{% trans "Restore Database" %}</h5>
</div>
<div class="card-body">
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<strong>{% trans "Warning:" %}</strong> {% trans "This will overwrite all current data. This action cannot be undone." %}
</div>
<form action="{% url 'restore_database' %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="mb-3">
<label class="form-label fw-semibold">{% trans "Select Backup File (.sql)" %}</label>
<input type="file" name="backup_file" class="form-control" accept=".sql" required>
</div>
<button type="submit" class="btn btn-danger w-100" onclick="return confirm('{% trans "Are you sure? This will wipe the current database." %}')">
<i class="bi bi-arrow-counterclockwise me-2"></i> {% trans "Restore Database" %}
</button>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Add Tier Modal --> <!-- Add Tier Modal -->
<div class="modal fade" id="addTierModal" tabindex="-1"> <div class="modal fade" id="addTierModal" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">

View File

@ -5,7 +5,6 @@ urlpatterns = [
path('', views.index, name='index'), path('', views.index, name='index'),
path('inventory/', views.inventory, name='inventory'), path('inventory/', views.inventory, name='inventory'),
path('pos/', views.pos, name='pos'), path('pos/', views.pos, name='pos'),
path('pos/customer-display/', views.customer_display, name='customer_display'),
path('customers/', views.customers, name='customers'), path('customers/', views.customers, name='customers'),
path('suppliers/', views.suppliers, name='suppliers'), path('suppliers/', views.suppliers, name='suppliers'),
path('purchases/', views.purchases, name='purchases'), path('purchases/', views.purchases, name='purchases'),
@ -129,8 +128,4 @@ urlpatterns = [
path('settings/devices/add/', views.add_device, name='add_device'), path('settings/devices/add/', views.add_device, name='add_device'),
path('settings/devices/edit/<int:pk>/', views.edit_device, name='edit_device'), path('settings/devices/edit/<int:pk>/', views.edit_device, name='edit_device'),
path('settings/devices/delete/<int:pk>/', views.delete_device, name='delete_device'), path('settings/devices/delete/<int:pk>/', views.delete_device, name='delete_device'),
]
# System Backup
path('settings/backup/download/', views.backup_database, name='backup_database'),
path('settings/backup/restore/', views.restore_database, name='restore_database'),
]

View File

@ -1,6 +1,5 @@
import base64 import base64
import os import os
import subprocess
from django.conf import settings as django_settings from django.conf import settings as django_settings
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -2424,103 +2423,4 @@ def search_customers_api(request):
).values('id', 'name', 'phone')[:20] ).values('id', 'name', 'phone')[:20]
else: else:
customers = [] customers = []
return JsonResponse({'results': list(customers)}) return JsonResponse({'results': list(customers)})
@login_required
def backup_database(request):
if not request.user.is_superuser:
messages.error(request, _("Access denied."))
return redirect('settings')
db_settings = django_settings.DATABASES['default']
db_name = db_settings['NAME']
db_user = db_settings['USER']
db_password = db_settings['PASSWORD']
db_host = db_settings['HOST']
db_port = db_settings['PORT']
timestamp = timezone.now().strftime('%Y-%m-%d_%H-%M-%S')
filename = f"backup_{db_name}_{timestamp}.sql"
env = os.environ.copy()
env['MYSQL_PWD'] = db_password
command = [
'mysqldump',
'-h', db_host,
'-P', str(db_port),
'-u', db_user,
'--no-tablespaces',
db_name
]
try:
process = subprocess.Popen(command, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, error = process.communicate()
if process.returncode != 0:
messages.error(request, f"Backup failed: {error.decode('utf-8')}")
return redirect('settings')
response = HttpResponse(output, content_type='application/octet-stream')
response['Content-Disposition'] = f'attachment; filename="{filename}"'
return response
except Exception as e:
messages.error(request, f"An error occurred: {str(e)}")
return redirect('settings')
@login_required
def restore_database(request):
if not request.user.is_superuser:
messages.error(request, _("Access denied."))
return redirect('settings')
if request.method == 'POST' and request.FILES.get('backup_file'):
backup_file = request.FILES['backup_file']
if not backup_file.name.endswith('.sql'):
messages.error(request, _("Please upload a valid .sql file."))
return redirect(reverse('settings') + '#backup')
db_settings = django_settings.DATABASES['default']
db_name = db_settings['NAME']
db_user = db_settings['USER']
db_password = db_settings['PASSWORD']
db_host = db_settings['HOST']
db_port = db_settings['PORT']
env = os.environ.copy()
env['MYSQL_PWD'] = db_password
command = [
'mysql',
'-h', db_host,
'-P', str(db_port),
'-u', db_user,
db_name
]
try:
file_content = backup_file.read()
process = subprocess.Popen(command, env=env, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
output, error = process.communicate(input=file_content)
if process.returncode != 0:
messages.error(request, f"Restore failed: {error.decode('utf-8')}")
else:
messages.success(request, _("Database restored successfully!"))
except Exception as e:
messages.error(request, f"An error occurred: {str(e)}")
return redirect(reverse('settings') + '#backup')
@login_required
def customer_display(request):
"""
Customer Facing Display (CFD) View
"""
settings = SystemSetting.objects.first()
return render(request, 'core/customer_display.html', {'settings': settings})

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -12,10 +12,9 @@ class JobPositionAdmin(admin.ModelAdmin):
@admin.register(Employee) @admin.register(Employee)
class EmployeeAdmin(admin.ModelAdmin): class EmployeeAdmin(admin.ModelAdmin):
list_display = ('first_name', 'last_name', 'biometric_id', 'email', 'department', 'job_position', 'status') list_display = ('first_name', 'last_name', 'email', 'department', 'job_position', 'status')
list_filter = ('status', 'department', 'gender') list_filter = ('status', 'department', 'gender')
search_fields = ('first_name', 'last_name', 'email', 'phone', 'biometric_id') search_fields = ('first_name', 'last_name', 'email', 'phone')
ordering = ('first_name', 'last_name')
@admin.register(Attendance) @admin.register(Attendance)
class AttendanceAdmin(admin.ModelAdmin): class AttendanceAdmin(admin.ModelAdmin):
@ -32,4 +31,4 @@ class LeaveRequestAdmin(admin.ModelAdmin):
class BiometricDeviceAdmin(admin.ModelAdmin): class BiometricDeviceAdmin(admin.ModelAdmin):
list_display = ('name', 'ip_address', 'port', 'device_type', 'status', 'last_sync') list_display = ('name', 'ip_address', 'port', 'device_type', 'status', 'last_sync')
list_filter = ('status', 'device_type') list_filter = ('status', 'device_type')
search_fields = ('name', 'ip_address') search_fields = ('name', 'ip_address')

View File

@ -1,29 +0,0 @@
from django import forms
from .models import Employee
from django.utils.translation import gettext_lazy as _
class EmployeeForm(forms.ModelForm):
class Meta:
model = Employee
fields = [
'first_name', 'last_name', 'gender', 'date_of_birth',
'email', 'phone', 'address',
'department', 'job_position', 'hire_date', 'status', 'salary',
'user', 'biometric_id'
]
widgets = {
'date_of_birth': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'hire_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'phone': forms.TextInput(attrs={'class': 'form-control'}),
'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'gender': forms.Select(attrs={'class': 'form-select'}),
'department': forms.Select(attrs={'class': 'form-select'}),
'job_position': forms.Select(attrs={'class': 'form-select'}),
'status': forms.Select(attrs={'class': 'form-select'}),
'salary': forms.NumberInput(attrs={'class': 'form-control'}),
'user': forms.Select(attrs={'class': 'form-select'}),
'biometric_id': forms.NumberInput(attrs={'class': 'form-control'}),
}

View File

@ -9,100 +9,11 @@
<div class="card-body"> <div class="card-body">
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }}
{% if form.errors %} <button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
<div class="alert alert-danger"> <a href="{% url 'hr:employee_list' %}" class="btn btn-secondary">{% trans "Cancel" %}</a>
{% for field in form %}
{% for error in field.errors %}
<strong>{{ field.label }}:</strong> {{ error }}<br>
{% endfor %}
{% endfor %}
{% for error in form.non_field_errors %}
{{ error }}<br>
{% endfor %}
</div>
{% endif %}
<h5 class="text-primary mb-3">{% trans "Personal Information" %}</h5>
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.first_name.id_for_label }}" class="form-label">{{ form.first_name.label }}</label>
{{ form.first_name }}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.last_name.id_for_label }}" class="form-label">{{ form.last_name.label }}</label>
{{ form.last_name }}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.gender.id_for_label }}" class="form-label">{{ form.gender.label }}</label>
{{ form.gender }}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.date_of_birth.id_for_label }}" class="form-label">{{ form.date_of_birth.label }}</label>
{{ form.date_of_birth }}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.email.id_for_label }}" class="form-label">{{ form.email.label }}</label>
{{ form.email }}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.phone.id_for_label }}" class="form-label">{{ form.phone.label }}</label>
{{ form.phone }}
</div>
<div class="col-12 mb-3">
<label for="{{ form.address.id_for_label }}" class="form-label">{{ form.address.label }}</label>
{{ form.address }}
</div>
</div>
<hr class="my-4">
<h5 class="text-primary mb-3">{% trans "Job Details" %}</h5>
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.department.id_for_label }}" class="form-label">{{ form.department.label }}</label>
{{ form.department }}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.job_position.id_for_label }}" class="form-label">{{ form.job_position.label }}</label>
{{ form.job_position }}
</div>
<div class="col-md-4 mb-3">
<label for="{{ form.hire_date.id_for_label }}" class="form-label">{{ form.hire_date.label }}</label>
{{ form.hire_date }}
</div>
<div class="col-md-4 mb-3">
<label for="{{ form.status.id_for_label }}" class="form-label">{{ form.status.label }}</label>
{{ form.status }}
</div>
<div class="col-md-4 mb-3">
<label for="{{ form.salary.id_for_label }}" class="form-label">{{ form.salary.label }}</label>
{{ form.salary }}
</div>
</div>
<hr class="my-4">
<h5 class="text-primary mb-3">{% trans "System Access & Biometrics" %}</h5>
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.user.id_for_label }}" class="form-label">{{ form.user.label }}</label>
{{ form.user }}
<small class="form-text text-muted">{% trans "Link to a system user account for login access." %}</small>
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.biometric_id.id_for_label }}" class="form-label">{{ form.biometric_id.label }}</label>
{{ form.biometric_id }}
<small class="form-text text-muted">{% trans "User ID on the physical biometric device." %}</small>
</div>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
<a href="{% url 'hr:employee_list' %}" class="btn btn-secondary">{% trans "Cancel" %}</a>
</div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -17,7 +17,6 @@
<thead> <thead>
<tr> <tr>
<th>{% trans "Name" %}</th> <th>{% trans "Name" %}</th>
<th>{% trans "Biometric ID" %}</th>
<th>{% trans "Department" %}</th> <th>{% trans "Department" %}</th>
<th>{% trans "Position" %}</th> <th>{% trans "Position" %}</th>
<th>{% trans "Email" %}</th> <th>{% trans "Email" %}</th>
@ -34,7 +33,6 @@
{{ employee.first_name }} {{ employee.last_name }} {{ employee.first_name }} {{ employee.last_name }}
</a> </a>
</td> </td>
<td>{{ employee.biometric_id|default:"-" }}</td>
<td>{{ employee.department.name_en }} / {{ employee.department.name_ar }}</td> <td>{{ employee.department.name_en }} / {{ employee.department.name_ar }}</td>
<td>{{ employee.job_position.title_en }} / {{ employee.job_position.title_ar }}</td> <td>{{ employee.job_position.title_en }} / {{ employee.job_position.title_ar }}</td>
<td>{{ employee.email }}</td> <td>{{ employee.email }}</td>
@ -52,7 +50,7 @@
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="8" class="text-center">{% trans "No employees found." %}</td> <td colspan="7" class="text-center">{% trans "No employees found." %}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -61,4 +59,4 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -3,7 +3,6 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .models import Employee, Department, Attendance, LeaveRequest, JobPosition, BiometricDevice from .models import Employee, Department, Attendance, LeaveRequest, JobPosition, BiometricDevice
from .forms import EmployeeForm
from django.db.models import Count from django.db.models import Count
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.contrib import messages from django.contrib import messages
@ -28,7 +27,7 @@ class EmployeeListView(LoginRequiredMixin, ListView):
class EmployeeCreateView(LoginRequiredMixin, CreateView): class EmployeeCreateView(LoginRequiredMixin, CreateView):
model = Employee model = Employee
form_class = EmployeeForm fields = '__all__'
template_name = 'hr/employee_form.html' template_name = 'hr/employee_form.html'
success_url = reverse_lazy('hr:employee_list') success_url = reverse_lazy('hr:employee_list')
@ -39,7 +38,7 @@ class EmployeeCreateView(LoginRequiredMixin, CreateView):
class EmployeeUpdateView(LoginRequiredMixin, UpdateView): class EmployeeUpdateView(LoginRequiredMixin, UpdateView):
model = Employee model = Employee
form_class = EmployeeForm fields = '__all__'
template_name = 'hr/employee_form.html' template_name = 'hr/employee_form.html'
success_url = reverse_lazy('hr:employee_list') success_url = reverse_lazy('hr:employee_list')
@ -142,4 +141,4 @@ def sync_device_logs(request, pk):
else: else:
messages.success(request, _("Sync Successful! Fetched %(total)s records, %(new)s new.") % {'total': result['total'], 'new': result['new']}) messages.success(request, _("Sync Successful! Fetched %(total)s records, %(new)s new.") % {'total': result['total'], 'new': result['new']})
return redirect('hr:device_list') return redirect('hr:device_list')

View File

@ -2,9 +2,7 @@
"""Django's command-line utility for administrative tasks.""" """Django's command-line utility for administrative tasks."""
import os import os
import sys import sys
import pymysql
pymysql.install_as_MySQLdb()
def main(): def main():
"""Run administrative tasks.""" """Run administrative tasks."""
@ -21,4 +19,4 @@ def main():
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@ -1,5 +1,4 @@
Django==5.2.7 Django==5.2.7
pymysql mysqlclient==2.2.7
python-dotenv==1.1.1 python-dotenv==1.1.1
pyzk==0.9 pyzk==0.9
requests==2.32.3