Versiunea funcțională EUMI

This commit is contained in:
2026-08-31 16:10:46 +03:00
parent 75bcef3095
commit ce5028a27f
58 changed files with 4271 additions and 98 deletions
+14
View File
@@ -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"
+29
View File
@@ -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
View File
@@ -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 %}
+64
View File
@@ -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
View File
@@ -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)")
+10
View File
@@ -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
View File
@@ -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},
)