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" %} <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">
@ -625,6 +631,50 @@
</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

@ -128,4 +128,8 @@ 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,5 +1,6 @@
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 _
@ -2423,4 +2424,94 @@ 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')

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) @admin.register(Employee)
class EmployeeAdmin(admin.ModelAdmin): 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') 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) @admin.register(Attendance)
class AttendanceAdmin(admin.ModelAdmin): class AttendanceAdmin(admin.ModelAdmin):
@ -31,4 +32,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')

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"> <div class="card-body">
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">{% trans "Save" %}</button> {% if form.errors %}
<a href="{% url 'hr:employee_list' %}" class="btn btn-secondary">{% trans "Cancel" %}</a> <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> </form>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -17,6 +17,7 @@
<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>
@ -33,6 +34,7 @@
{{ 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>
@ -50,7 +52,7 @@
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="7" class="text-center">{% trans "No employees found." %}</td> <td colspan="8" class="text-center">{% trans "No employees found." %}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -59,4 +61,4 @@
</div> </div>
</div> </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.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
@ -27,7 +28,7 @@ class EmployeeListView(LoginRequiredMixin, ListView):
class EmployeeCreateView(LoginRequiredMixin, CreateView): class EmployeeCreateView(LoginRequiredMixin, CreateView):
model = Employee model = Employee
fields = '__all__' form_class = EmployeeForm
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')
@ -38,7 +39,7 @@ class EmployeeCreateView(LoginRequiredMixin, CreateView):
class EmployeeUpdateView(LoginRequiredMixin, UpdateView): class EmployeeUpdateView(LoginRequiredMixin, UpdateView):
model = Employee model = Employee
fields = '__all__' form_class = EmployeeForm
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')
@ -141,4 +142,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')