Autosave: 20260205-145107

This commit is contained in:
Flatlogic Bot 2026-02-05 14:51:08 +00:00
parent 2ad0af108e
commit 55438579ee
13 changed files with 281 additions and 14 deletions

View File

@ -50,6 +50,12 @@
<i class="bi bi-whatsapp me-2"></i>{% trans "WhatsApp Gateway" %}
</button>
</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>
<div class="tab-content" id="settingsTabsContent">
@ -625,6 +631,50 @@
</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 -->
<div class="modal fade" id="addTierModal" tabindex="-1">
<div class="modal-dialog">

View File

@ -128,4 +128,8 @@ urlpatterns = [
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/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,5 +1,6 @@
import base64
import os
import subprocess
from django.conf import settings as django_settings
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
@ -2423,4 +2424,94 @@ def search_customers_api(request):
).values('id', 'name', 'phone')[:20]
else:
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')

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

29
hr/forms.py Normal file
View File

@ -0,0 +1,29 @@
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,11 +9,100 @@
<div class="card-body">
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
<a href="{% url 'hr:employee_list' %}" class="btn btn-secondary">{% trans "Cancel" %}</a>
{% if form.errors %}
<div class="alert alert-danger">
{% 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>
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

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

View File

@ -3,6 +3,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from .models import Employee, Department, Attendance, LeaveRequest, JobPosition, BiometricDevice
from .forms import EmployeeForm
from django.db.models import Count
from django.shortcuts import get_object_or_404, redirect
from django.contrib import messages
@ -27,7 +28,7 @@ class EmployeeListView(LoginRequiredMixin, ListView):
class EmployeeCreateView(LoginRequiredMixin, CreateView):
model = Employee
fields = '__all__'
form_class = EmployeeForm
template_name = 'hr/employee_form.html'
success_url = reverse_lazy('hr:employee_list')
@ -38,7 +39,7 @@ class EmployeeCreateView(LoginRequiredMixin, CreateView):
class EmployeeUpdateView(LoginRequiredMixin, UpdateView):
model = Employee
fields = '__all__'
form_class = EmployeeForm
template_name = 'hr/employee_form.html'
success_url = reverse_lazy('hr:employee_list')
@ -141,4 +142,4 @@ def sync_device_logs(request, pk):
else:
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')