Autosave: 20260205-145107
This commit is contained in:
parent
2ad0af108e
commit
55438579ee
Binary file not shown.
Binary file not shown.
@ -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">
|
||||
|
||||
@ -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'),
|
||||
]
|
||||
@ -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.
BIN
hr/__pycache__/forms.cpython-311.pyc
Normal file
BIN
hr/__pycache__/forms.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
@ -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
29
hr/forms.py
Normal 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'}),
|
||||
}
|
||||
@ -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 %}
|
||||
@ -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 %}
|
||||
@ -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')
|
||||
Loading…
x
Reference in New Issue
Block a user