Versiunea funcțională EUMI
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.urls import reverse
|
||||
from simple_history.admin import SimpleHistoryAdmin
|
||||
from .models import Directie, Serviciu, Compartiment, Employee
|
||||
|
||||
@@ -33,6 +35,18 @@ class EmployeeAdmin(SimpleHistoryAdmin):
|
||||
"directie",
|
||||
"serviciu",
|
||||
"compartiment",
|
||||
"print_inventory_button",
|
||||
)
|
||||
list_filter = ("directie", "serviciu", "compartiment")
|
||||
search_fields = ("name", "username", "email")
|
||||
|
||||
def print_inventory_button(self, obj):
|
||||
if obj.pk:
|
||||
url = reverse("equipment:print_inventory", args=[obj.pk])
|
||||
return format_html(
|
||||
'<a class="button" href="{}" target="_blank" style="background-color: #417690; color: white; padding: 5px 10px; border-radius: 4px; text-decoration: none;">🖨️ Printează Fișa</a>',
|
||||
url,
|
||||
)
|
||||
return "-"
|
||||
|
||||
print_inventory_button.short_description = "Acțiune"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
from django import forms
|
||||
from .models import Employee
|
||||
|
||||
|
||||
class EmployeeForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Employee
|
||||
fields = [
|
||||
"name",
|
||||
"username",
|
||||
"role",
|
||||
"office_number",
|
||||
"email",
|
||||
"directie",
|
||||
"serviciu",
|
||||
"compartiment",
|
||||
"observations",
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"username": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"role": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"office_number": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"email": forms.EmailInput(attrs={"class": "form-control"}),
|
||||
"directie": forms.Select(attrs={"class": "form-select"}),
|
||||
"serviciu": forms.Select(attrs={"class": "form-select"}),
|
||||
"compartiment": forms.Select(attrs={"class": "form-select"}),
|
||||
"observations": forms.Textarea(attrs={"class": "form-control", "rows": 2}),
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 6.1 on 2026-08-26 11:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0002_historicalcompartiment_historicaldirectie_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='employee',
|
||||
name='observations',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Observații'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='historicalemployee',
|
||||
name='observations',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Observații'),
|
||||
),
|
||||
]
|
||||
+14
-1
@@ -89,7 +89,9 @@ class Employee(models.Model):
|
||||
role = models.CharField(max_length=255, verbose_name="Function/role")
|
||||
office_number = models.CharField(max_length=50, verbose_name="Office number")
|
||||
email = models.EmailField(verbose_name="Email address")
|
||||
observations = models.TextField(null=True, blank=True, verbose_name="Observations")
|
||||
|
||||
# Keep only this single observations field
|
||||
observations = models.TextField(blank=True, null=True, verbose_name="Observații")
|
||||
|
||||
# Organizational Placement - Employees can be placed at any of the three levels
|
||||
directie = models.ForeignKey(
|
||||
@@ -116,6 +118,17 @@ class Employee(models.Model):
|
||||
related_name="employees",
|
||||
verbose_name="Compartiment",
|
||||
)
|
||||
|
||||
@property
|
||||
def get_active_inventory(self):
|
||||
"""Returns a dictionary of all active equipment and certificates assigned to the user."""
|
||||
return {
|
||||
"computers": self.computers.filter(status="activ"),
|
||||
"monitors": self.monitors.filter(status="activ"),
|
||||
"peripherals": self.peripherals.filter(status="activ"),
|
||||
"certificates": self.digital_certificates.filter(is_canceled=False),
|
||||
}
|
||||
|
||||
history = HistoricalRecords()
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<div class="modal-header bg-dark text-white">
|
||||
<h5 class="modal-title">{% if employee %}Editează Angajat{% else %}Adaugă Angajat Nou{% endif %}</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form hx-post="{% if employee %}{% url 'users:employee_edit' employee.pk %}{% else %}{% url 'users:employee_create' %}{% endif %}"
|
||||
hx-target="#modal-content-area">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Nume complet</label>
|
||||
{{ form.name }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Username</label>
|
||||
{{ form.username }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Funcție</label>
|
||||
{{ form.role }}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-bold">Nr. Birou</label>
|
||||
{{ form.office_number }}
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-bold">Email</label>
|
||||
{{ form.email }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold">Direcție</label>
|
||||
{{ form.directie }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold">Serviciu</label>
|
||||
{{ form.serviciu }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold">Compartiment</label>
|
||||
{{ form.compartiment }}
|
||||
</div>
|
||||
<div class="col-md-12 mt-3">
|
||||
<label class="form-label fw-bold">Observații</label>
|
||||
{{ form.observations }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anulează</button>
|
||||
<button type="submit" class="btn btn-primary">Salvează</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,31 @@
|
||||
{% for emp in employees %}
|
||||
<tr>
|
||||
<td class="align-middle fw-medium">{{ emp.name }}</td>
|
||||
<td class="align-middle">{{ emp.username }}</td>
|
||||
<td class="align-middle">{{ emp.role }}</td>
|
||||
<td class="align-middle">
|
||||
{% if emp.directie %}<strong>{{ emp.directie.name }}</strong><br>{% endif %}
|
||||
{% if emp.serviciu %}<span class="text-muted">{{ emp.serviciu.name }}</span><br>{% endif %}
|
||||
{% if emp.compartiment %}<small class="text-secondary">{{ emp.compartiment.name }}</small>{% endif %}
|
||||
</td>
|
||||
<td class="align-middle text-center">
|
||||
<!-- Edit Button -->
|
||||
{% if user.is_staff %}
|
||||
<button class="btn btn-sm btn-outline-secondary me-1"
|
||||
hx-get="{% url 'users:employee_edit' emp.id %}"
|
||||
hx-target="#modal-content-area"
|
||||
data-bs-toggle="modal" data-bs-target="#form-modal">
|
||||
✏️ Editează
|
||||
</button>
|
||||
{% endif %}
|
||||
<!-- Print Button -->
|
||||
<a href="{% url 'equipment:print_inventory' emp.id %}" target="_blank" class="btn btn-sm btn-outline-primary">
|
||||
🖨️ Fișă
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-muted py-4">Nu au fost găsiți angajați conform criteriilor de căutare.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -0,0 +1,64 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold">Angajați</h2>
|
||||
|
||||
<div class="d-flex gap-3 w-50 justify-content-end">
|
||||
<!-- HTMX Live Search Input -->
|
||||
<input class="form-control w-50" type="search"
|
||||
name="search" placeholder="Caută angajat..."
|
||||
hx-get="{% url 'users:user_list' %}"
|
||||
hx-trigger="input changed delay:400ms, search"
|
||||
hx-target="#user-table-body">
|
||||
|
||||
<!-- Add Button (Triggers Modal) -->
|
||||
{% if user.is_staff %}
|
||||
<button class="btn btn-success fw-bold"
|
||||
hx-get="{% url 'users:employee_create' %}"
|
||||
hx-target="#modal-content-area"
|
||||
data-bs-toggle="modal" data-bs-target="#form-modal">
|
||||
+ Adaugă Angajat
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<table class="table table-striped table-hover mb-0">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Nume</th>
|
||||
<th>Username</th>
|
||||
<th>Funcție</th>
|
||||
<th>Structură (Dir / Serv / Comp)</th>
|
||||
<th class="text-center">Acțiuni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<!-- Added hx-trigger="employeeListChanged from:body" so it refreshes on save -->
|
||||
<tbody id="user-table-body" hx-get="{% url 'users:user_list' %}" hx-trigger="employeeListChanged from:body">
|
||||
{% include 'users/partials/user_table.html' %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Empty Bootstrap Modal Container -->
|
||||
<div class="modal fade" id="form-modal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content" id="modal-content-area">
|
||||
<!-- Form will be injected here by HTMX -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Script to automatically close the modal when HTMX triggers 'closeModal' -->
|
||||
<script>
|
||||
document.body.addEventListener('closeModal', function() {
|
||||
var modalEl = document.getElementById('form-modal');
|
||||
var modal = bootstrap.Modal.getInstance(modalEl);
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
+43
-1
@@ -1,3 +1,45 @@
|
||||
from django.test import TestCase
|
||||
from django.core.exceptions import ValidationError
|
||||
from .models import Directie, Serviciu, Compartiment, Employee
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
class UsersModelTests(TestCase):
|
||||
def setUp(self):
|
||||
self.directie = Directie.objects.create(name="Direcția Economică")
|
||||
self.serviciu = Serviciu.objects.create(
|
||||
name="Serviciul Contabilitate", directie=self.directie
|
||||
)
|
||||
|
||||
def test_compartiment_validation_valid(self):
|
||||
# A Compartiment assigned to a Serviciu should pass validation
|
||||
comp = Compartiment(name="Compartiment Salarizare", serviciu=self.serviciu)
|
||||
comp.full_clean() # Validates the model without saving to DB
|
||||
self.assertEqual(comp.serviciu, self.serviciu)
|
||||
|
||||
def test_compartiment_validation_missing_parent(self):
|
||||
# A Compartiment with no parent should raise a ValidationError
|
||||
comp = Compartiment(name="Compartiment Orfan")
|
||||
with self.assertRaises(ValidationError):
|
||||
comp.full_clean()
|
||||
|
||||
def test_compartiment_validation_multiple_parents(self):
|
||||
# A Compartiment assigned to BOTH a Serviciu and a Directie should fail
|
||||
comp = Compartiment(
|
||||
name="Compartiment Dublu Subordonat",
|
||||
directie=self.directie,
|
||||
serviciu=self.serviciu,
|
||||
)
|
||||
with self.assertRaises(ValidationError):
|
||||
comp.full_clean()
|
||||
|
||||
def test_employee_creation(self):
|
||||
emp = Employee.objects.create(
|
||||
name="Test User",
|
||||
username="testuser",
|
||||
role="Inspector",
|
||||
office_number="12",
|
||||
email="test@primariaroman.ro",
|
||||
directie=self.directie,
|
||||
)
|
||||
self.assertEqual(emp.name, "Test User")
|
||||
self.assertEqual(str(emp), "Test User (testuser)")
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = "users"
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.user_list, name="user_list"),
|
||||
path("new/", views.employee_form, name="employee_create"),
|
||||
path("<int:pk>/edit/", views.employee_form, name="employee_edit"),
|
||||
]
|
||||
+53
-2
@@ -1,3 +1,54 @@
|
||||
from django.shortcuts import render
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.http import HttpResponse
|
||||
from django.db.models import Q
|
||||
from .models import Employee
|
||||
from .forms import EmployeeForm
|
||||
|
||||
# Create your views here.
|
||||
|
||||
@login_required
|
||||
def user_list(request):
|
||||
query = request.GET.get("search", "")
|
||||
employees = Employee.objects.all().order_by("name")
|
||||
|
||||
if query:
|
||||
# Filter by name, username, or role (case-insensitive)
|
||||
employees = employees.filter(
|
||||
Q(name__icontains=query)
|
||||
| Q(username__icontains=query)
|
||||
| Q(role__icontains=query)
|
||||
)
|
||||
|
||||
context = {"employees": employees}
|
||||
|
||||
# If the request comes from HTMX, only return the table rows
|
||||
if request.headers.get("HX-Request"):
|
||||
return render(request, "users/partials/user_table.html", context)
|
||||
|
||||
# Otherwise, return the full page
|
||||
return render(request, "users/user_list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def employee_form(request, pk=None):
|
||||
if pk:
|
||||
employee = get_object_or_404(Employee, pk=pk)
|
||||
else:
|
||||
employee = None
|
||||
|
||||
if request.method == "POST":
|
||||
form = EmployeeForm(request.POST, instance=employee)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
# Return a 204 No Content response, but trigger custom HTMX events
|
||||
response = HttpResponse(status=204)
|
||||
response["HX-Trigger"] = "employeeListChanged, closeModal"
|
||||
return response
|
||||
else:
|
||||
form = EmployeeForm(instance=employee)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"users/partials/employee_form.html",
|
||||
{"form": form, "employee": employee},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user