feat: add admin user management page with CRUD and i18n
- New admin blueprint with CRUD proxy endpoints for users - Admin user management template with search, create, edit, toggle active - Navbar: add admin link for is_admin users (desktop + mobile) - Register admin blueprint in app factory - Add IT/EN translations for all admin UI strings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -37,11 +37,13 @@ def create_app() -> Flask:
|
|||||||
from blueprints.measure import measure_bp
|
from blueprints.measure import measure_bp
|
||||||
from blueprints.maker import maker_bp
|
from blueprints.maker import maker_bp
|
||||||
from blueprints.statistics import statistics_bp
|
from blueprints.statistics import statistics_bp
|
||||||
|
from blueprints.admin import admin_bp
|
||||||
|
|
||||||
app.register_blueprint(auth_bp)
|
app.register_blueprint(auth_bp)
|
||||||
app.register_blueprint(measure_bp, url_prefix="/measure")
|
app.register_blueprint(measure_bp, url_prefix="/measure")
|
||||||
app.register_blueprint(maker_bp, url_prefix="/maker")
|
app.register_blueprint(maker_bp, url_prefix="/maker")
|
||||||
app.register_blueprint(statistics_bp, url_prefix="/statistics")
|
app.register_blueprint(statistics_bp, url_prefix="/statistics")
|
||||||
|
app.register_blueprint(admin_bp, url_prefix="/admin")
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
"""Admin blueprint - user management."""
|
||||||
|
from flask import Blueprint, flash, jsonify, redirect, render_template, request, session, url_for
|
||||||
|
from flask_babel import gettext as _
|
||||||
|
|
||||||
|
from blueprints.auth import login_required
|
||||||
|
from services.api_client import api_client
|
||||||
|
|
||||||
|
admin_bp = Blueprint("admin", __name__, url_prefix="/admin")
|
||||||
|
|
||||||
|
|
||||||
|
def admin_required(f):
|
||||||
|
"""Decorator to require admin privileges."""
|
||||||
|
from functools import wraps
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
user = session.get("user", {})
|
||||||
|
if not user.get("is_admin"):
|
||||||
|
flash(_("Accesso non autorizzato"), "error")
|
||||||
|
return redirect(url_for("measure.select_recipe"))
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# PAGINE (GET)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@admin_bp.route("/users")
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def user_list():
|
||||||
|
"""User management page."""
|
||||||
|
resp = api_client.get("/api/users")
|
||||||
|
|
||||||
|
if isinstance(resp, dict) and resp.get("error"):
|
||||||
|
flash(_("Errore nel caricamento degli utenti: %(error)s", error=resp.get("detail", "")), "error")
|
||||||
|
users = []
|
||||||
|
elif isinstance(resp, list):
|
||||||
|
users = resp
|
||||||
|
else:
|
||||||
|
users = []
|
||||||
|
|
||||||
|
return render_template("admin/users.html", users=users)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# API PROXY AJAX (JSON)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@admin_bp.route("/api/users", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def api_create_user():
|
||||||
|
"""Proxy: Create new user."""
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
resp = api_client.post("/api/users", data=data)
|
||||||
|
|
||||||
|
if isinstance(resp, dict) and resp.get("error"):
|
||||||
|
return jsonify(resp), resp.get("status_code", 500)
|
||||||
|
|
||||||
|
return jsonify(resp), 201
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.route("/api/users/<int:user_id>", methods=["PUT"])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def api_update_user(user_id: int):
|
||||||
|
"""Proxy: Update user."""
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
resp = api_client.put(f"/api/users/{user_id}", data=data)
|
||||||
|
|
||||||
|
if isinstance(resp, dict) and resp.get("error"):
|
||||||
|
return jsonify(resp), resp.get("status_code", 500)
|
||||||
|
|
||||||
|
return jsonify(resp), 200
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.route("/api/users/<int:user_id>/password", methods=["PUT"])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def api_change_password(user_id: int):
|
||||||
|
"""Proxy: Change user password."""
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
resp = api_client.put(f"/api/users/{user_id}/password", data=data)
|
||||||
|
|
||||||
|
if isinstance(resp, dict) and resp.get("error"):
|
||||||
|
return jsonify(resp), resp.get("status_code", 500)
|
||||||
|
|
||||||
|
return jsonify(resp), 200
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.route("/api/users/<int:user_id>/toggle-active", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def api_toggle_active(user_id: int):
|
||||||
|
"""Proxy: Toggle user active status."""
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
active = data.get("active", True)
|
||||||
|
resp = api_client.put(f"/api/users/{user_id}", data={"active": active})
|
||||||
|
|
||||||
|
if isinstance(resp, dict) and resp.get("error"):
|
||||||
|
return jsonify(resp), resp.get("status_code", 500)
|
||||||
|
|
||||||
|
return jsonify(resp), 200
|
||||||
@@ -0,0 +1,535 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ _('Gestione Utenti') }} - TieMeasureFlow{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<script>window.__users = {{ users|tojson }};</script>
|
||||||
|
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"
|
||||||
|
x-data="userManagement(window.__users)">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-2xl font-bold text-[var(--text-primary)]">{{ _('Gestione Utenti') }}</h1>
|
||||||
|
<p class="mt-1 text-sm text-[var(--text-secondary)]">{{ _('Crea, modifica e gestisci gli utenti del sistema') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 mb-6">
|
||||||
|
<!-- Search -->
|
||||||
|
<div class="relative w-full sm:w-80">
|
||||||
|
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-secondary)]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||||
|
</svg>
|
||||||
|
<input type="text" x-model="search"
|
||||||
|
placeholder="{{ _('Cerca utente...') }}"
|
||||||
|
class="w-full pl-10 pr-4 py-2 rounded-lg border border-[var(--border-color)] bg-[var(--bg-card)]
|
||||||
|
text-sm text-[var(--text-primary)] placeholder-[var(--text-secondary)]
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-colors">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- New User Button -->
|
||||||
|
<button @click="openCreateModal()"
|
||||||
|
class="flex items-center gap-2 px-4 py-2 bg-primary text-white text-sm font-medium rounded-lg
|
||||||
|
hover:bg-primary-700 transition-colors shadow-sm">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/>
|
||||||
|
</svg>
|
||||||
|
{{ _('Nuovo Utente') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Users Table -->
|
||||||
|
<div class="bg-[var(--bg-card)] rounded-xl border border-[var(--border-color)] shadow-sm overflow-hidden">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-[var(--border-color)] bg-[var(--bg-secondary)]">
|
||||||
|
<th class="text-left px-4 py-3 text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">{{ _('Username') }}</th>
|
||||||
|
<th class="text-left px-4 py-3 text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider hidden sm:table-cell">{{ _('Nome Visualizzato') }}</th>
|
||||||
|
<th class="text-left px-4 py-3 text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider hidden md:table-cell">{{ _('Email') }}</th>
|
||||||
|
<th class="text-left px-4 py-3 text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">{{ _('Ruoli') }}</th>
|
||||||
|
<th class="text-center px-4 py-3 text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">{{ _('Stato') }}</th>
|
||||||
|
<th class="text-right px-4 py-3 text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">{{ _('Azioni') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-[var(--border-color)]">
|
||||||
|
<template x-for="user in filteredUsers" :key="user.id">
|
||||||
|
<tr class="hover:bg-[var(--bg-secondary)] transition-colors cursor-pointer"
|
||||||
|
@click="openEditModal(user)">
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-primary/10 border border-primary/20 flex items-center justify-center text-primary font-semibold text-xs"
|
||||||
|
x-text="user.display_name ? user.display_name[0].toUpperCase() : user.username[0].toUpperCase()"></div>
|
||||||
|
<div>
|
||||||
|
<span class="text-sm font-medium text-[var(--text-primary)]" x-text="user.username"></span>
|
||||||
|
<template x-if="user.is_admin">
|
||||||
|
<span class="ml-1.5 inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-semibold bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300">Admin</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-[var(--text-secondary)] hidden sm:table-cell" x-text="user.display_name"></td>
|
||||||
|
<td class="px-4 py-3 text-sm text-[var(--text-secondary)] hidden md:table-cell" x-text="user.email || '-'"></td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
<template x-for="role in user.roles" :key="role">
|
||||||
|
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] font-medium"
|
||||||
|
:class="{
|
||||||
|
'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300': role === 'Maker',
|
||||||
|
'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300': role === 'MeasurementTec',
|
||||||
|
'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300': role === 'Metrologist'
|
||||||
|
}"
|
||||||
|
x-text="role"></span>
|
||||||
|
</template>
|
||||||
|
<template x-if="!user.roles || user.roles.length === 0">
|
||||||
|
<span class="text-xs text-[var(--text-secondary)]">-</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-center">
|
||||||
|
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] font-medium"
|
||||||
|
:class="user.active
|
||||||
|
? 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300'
|
||||||
|
: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'"
|
||||||
|
x-text="user.active ? '{{ _('Attivo') }}' : '{{ _('Disattivato') }}'"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-right" @click.stop>
|
||||||
|
<div class="flex items-center justify-end gap-1">
|
||||||
|
<button @click="openEditModal(user)" class="p-1.5 rounded-lg text-[var(--text-secondary)] hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/20 transition-colors" :title="'{{ _('Modifica') }}'">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button @click="toggleActive(user)" class="p-1.5 rounded-lg transition-colors"
|
||||||
|
:class="user.active
|
||||||
|
? 'text-[var(--text-secondary)] hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20'
|
||||||
|
: 'text-[var(--text-secondary)] hover:text-emerald-600 hover:bg-emerald-50 dark:hover:bg-emerald-900/20'"
|
||||||
|
:title="user.active ? '{{ _('Disattiva') }}' : '{{ _('Riattiva') }}'">
|
||||||
|
<svg x-show="user.active" class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/>
|
||||||
|
</svg>
|
||||||
|
<svg x-show="!user.active" x-cloak class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<template x-if="filteredUsers.length === 0">
|
||||||
|
<div class="px-6 py-12 text-center">
|
||||||
|
<svg class="w-12 h-12 mx-auto text-[var(--text-secondary)] opacity-40" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"/>
|
||||||
|
</svg>
|
||||||
|
<p class="mt-3 text-sm text-[var(--text-secondary)]">{{ _('Nessun utente trovato') }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Count -->
|
||||||
|
<div class="mt-3 text-xs text-[var(--text-secondary)]">
|
||||||
|
<span x-text="filteredUsers.length"></span> {{ _('utenti') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal: Create / Edit User -->
|
||||||
|
<div x-show="showModal" x-cloak
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||||
|
@keydown.escape.window="closeModal()">
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<div class="absolute inset-0 bg-black/50" @click="closeModal()"></div>
|
||||||
|
|
||||||
|
<!-- Modal Content -->
|
||||||
|
<div x-show="showModal"
|
||||||
|
x-transition:enter="transition ease-out duration-200"
|
||||||
|
x-transition:enter-start="opacity-0 scale-95"
|
||||||
|
x-transition:enter-end="opacity-100 scale-100"
|
||||||
|
x-transition:leave="transition ease-in duration-150"
|
||||||
|
x-transition:leave-start="opacity-100 scale-100"
|
||||||
|
x-transition:leave-end="opacity-0 scale-95"
|
||||||
|
class="relative bg-[var(--bg-card)] rounded-xl border border-[var(--border-color)] shadow-xl w-full max-w-lg max-h-[90vh] overflow-y-auto">
|
||||||
|
|
||||||
|
<!-- Modal Header -->
|
||||||
|
<div class="flex items-center justify-between px-6 py-4 border-b border-[var(--border-color)]">
|
||||||
|
<h2 class="text-lg font-semibold text-[var(--text-primary)]"
|
||||||
|
x-text="isEditing ? '{{ _('Modifica Utente') }}' : '{{ _('Nuovo Utente') }}'"></h2>
|
||||||
|
<button @click="closeModal()" class="p-1 rounded-lg text-[var(--text-secondary)] hover:bg-[var(--bg-secondary)] transition-colors">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Body -->
|
||||||
|
<div class="px-6 py-4 space-y-4">
|
||||||
|
<!-- Username -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-[var(--text-primary)] mb-1">{{ _('Username') }}</label>
|
||||||
|
<input type="text" x-model="form.username"
|
||||||
|
:disabled="isEditing"
|
||||||
|
:class="isEditing ? 'opacity-50 cursor-not-allowed' : ''"
|
||||||
|
class="w-full px-3 py-2 rounded-lg border border-[var(--border-color)] bg-[var(--bg-card)]
|
||||||
|
text-sm text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-colors"
|
||||||
|
placeholder="{{ _('Username') }}">
|
||||||
|
<template x-if="isEditing">
|
||||||
|
<p class="mt-1 text-xs text-[var(--text-secondary)]">{{ _('Il nome utente non può essere modificato') }}</p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Display Name -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-[var(--text-primary)] mb-1">{{ _('Nome Visualizzato') }}</label>
|
||||||
|
<input type="text" x-model="form.display_name"
|
||||||
|
class="w-full px-3 py-2 rounded-lg border border-[var(--border-color)] bg-[var(--bg-card)]
|
||||||
|
text-sm text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-colors"
|
||||||
|
placeholder="{{ _('Nome Visualizzato') }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-[var(--text-primary)] mb-1">{{ _('Email') }}</label>
|
||||||
|
<input type="email" x-model="form.email"
|
||||||
|
class="w-full px-3 py-2 rounded-lg border border-[var(--border-color)] bg-[var(--bg-card)]
|
||||||
|
text-sm text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-colors"
|
||||||
|
placeholder="email@example.com">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
{{ _('Password') }}
|
||||||
|
<template x-if="isEditing">
|
||||||
|
<span class="font-normal text-[var(--text-secondary)]"> ({{ _('lascia vuoto per non modificare') }})</span>
|
||||||
|
</template>
|
||||||
|
</label>
|
||||||
|
<input type="password" x-model="form.password"
|
||||||
|
class="w-full px-3 py-2 rounded-lg border border-[var(--border-color)] bg-[var(--bg-card)]
|
||||||
|
text-sm text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-colors"
|
||||||
|
:placeholder="isEditing ? '{{ _('Nuova password (opzionale)') }}' : '{{ _('Password') }}'"
|
||||||
|
minlength="6">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Roles -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-[var(--text-primary)] mb-2">{{ _('Ruoli') }}</label>
|
||||||
|
<div class="flex flex-wrap gap-3">
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" x-model="form.roles" value="Maker"
|
||||||
|
class="rounded border-[var(--border-color)] text-primary focus:ring-primary/30">
|
||||||
|
<span class="text-sm text-[var(--text-primary)]">Maker</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" x-model="form.roles" value="MeasurementTec"
|
||||||
|
class="rounded border-[var(--border-color)] text-primary focus:ring-primary/30">
|
||||||
|
<span class="text-sm text-[var(--text-primary)]">MeasurementTec</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" x-model="form.roles" value="Metrologist"
|
||||||
|
class="rounded border-[var(--border-color)] text-primary focus:ring-primary/30">
|
||||||
|
<span class="text-sm text-[var(--text-primary)]">Metrologist</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Admin + Preferences Row -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
<!-- Is Admin -->
|
||||||
|
<div>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" x-model="form.is_admin"
|
||||||
|
class="rounded border-[var(--border-color)] text-primary focus:ring-primary/30">
|
||||||
|
<span class="text-sm font-medium text-[var(--text-primary)]">{{ _('Amministratore') }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Language -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-[var(--text-primary)] mb-1">{{ _('Lingua') }}</label>
|
||||||
|
<select x-model="form.language_pref"
|
||||||
|
class="w-full px-3 py-2 rounded-lg border border-[var(--border-color)] bg-[var(--bg-card)]
|
||||||
|
text-sm text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-colors">
|
||||||
|
<option value="it">{{ _('Italiano') }}</option>
|
||||||
|
<option value="en">{{ _('English') }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Theme -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-[var(--text-primary)] mb-1">{{ _('Tema') }}</label>
|
||||||
|
<select x-model="form.theme_pref"
|
||||||
|
class="w-full px-3 py-2 rounded-lg border border-[var(--border-color)] bg-[var(--bg-card)]
|
||||||
|
text-sm text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-colors">
|
||||||
|
<option value="light">{{ _('Chiaro') }}</option>
|
||||||
|
<option value="dark">{{ _('Scuro') }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Message -->
|
||||||
|
<template x-if="errorMsg">
|
||||||
|
<div class="px-3 py-2 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800">
|
||||||
|
<p class="text-sm text-red-700 dark:text-red-300" x-text="errorMsg"></p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Footer -->
|
||||||
|
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-[var(--border-color)]">
|
||||||
|
<button @click="closeModal()"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-[var(--text-secondary)] rounded-lg
|
||||||
|
hover:bg-[var(--bg-secondary)] transition-colors">
|
||||||
|
{{ _('Annulla') }}
|
||||||
|
</button>
|
||||||
|
<button @click="saveUser()"
|
||||||
|
:disabled="saving"
|
||||||
|
class="px-4 py-2 bg-primary text-white text-sm font-medium rounded-lg
|
||||||
|
hover:bg-primary-700 transition-colors shadow-sm disabled:opacity-50">
|
||||||
|
<span x-show="!saving" x-text="isEditing ? '{{ _('Salva Modifiche') }}' : '{{ _('Crea Utente') }}'"></span>
|
||||||
|
<span x-show="saving" x-cloak>{{ _('Salvataggio...') }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirm Toggle Modal -->
|
||||||
|
<div x-show="showToggleConfirm" x-cloak
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<div class="absolute inset-0 bg-black/50" @click="showToggleConfirm = false"></div>
|
||||||
|
<div x-show="showToggleConfirm"
|
||||||
|
x-transition
|
||||||
|
class="relative bg-[var(--bg-card)] rounded-xl border border-[var(--border-color)] shadow-xl w-full max-w-sm p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-[var(--text-primary)] mb-2"
|
||||||
|
x-text="toggleUser?.active ? '{{ _('Conferma Disattivazione') }}' : '{{ _('Conferma Riattivazione') }}'"></h3>
|
||||||
|
<p class="text-sm text-[var(--text-secondary)] mb-4">
|
||||||
|
<span x-text="toggleUser?.active
|
||||||
|
? '{{ _('Sei sicuro di voler disattivare l\'utente') }}'
|
||||||
|
: '{{ _('Sei sicuro di voler riattivare l\'utente') }}'"></span>
|
||||||
|
<strong x-text="toggleUser?.username"></strong>?
|
||||||
|
</p>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<button @click="showToggleConfirm = false"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-[var(--text-secondary)] rounded-lg hover:bg-[var(--bg-secondary)] transition-colors">
|
||||||
|
{{ _('Annulla') }}
|
||||||
|
</button>
|
||||||
|
<button @click="confirmToggle()"
|
||||||
|
:disabled="saving"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-white rounded-lg shadow-sm disabled:opacity-50 transition-colors"
|
||||||
|
:class="toggleUser?.active ? 'bg-red-600 hover:bg-red-700' : 'bg-emerald-600 hover:bg-emerald-700'">
|
||||||
|
<span x-text="toggleUser?.active ? '{{ _('Disattiva') }}' : '{{ _('Riattiva') }}'"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
function userManagement(initialUsers) {
|
||||||
|
return {
|
||||||
|
users: initialUsers || [],
|
||||||
|
search: '',
|
||||||
|
showModal: false,
|
||||||
|
showToggleConfirm: false,
|
||||||
|
isEditing: false,
|
||||||
|
editingUserId: null,
|
||||||
|
toggleUser: null,
|
||||||
|
saving: false,
|
||||||
|
errorMsg: '',
|
||||||
|
form: {
|
||||||
|
username: '',
|
||||||
|
display_name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
roles: [],
|
||||||
|
is_admin: false,
|
||||||
|
language_pref: 'it',
|
||||||
|
theme_pref: 'light'
|
||||||
|
},
|
||||||
|
|
||||||
|
get csrfToken() {
|
||||||
|
const meta = document.querySelector('meta[name=csrf-token]');
|
||||||
|
return meta ? meta.content : '';
|
||||||
|
},
|
||||||
|
|
||||||
|
get filteredUsers() {
|
||||||
|
if (!this.search.trim()) return this.users;
|
||||||
|
const q = this.search.toLowerCase();
|
||||||
|
return this.users.filter(u =>
|
||||||
|
u.username.toLowerCase().includes(q) ||
|
||||||
|
(u.display_name && u.display_name.toLowerCase().includes(q)) ||
|
||||||
|
(u.email && u.email.toLowerCase().includes(q)) ||
|
||||||
|
(u.roles && u.roles.some(r => r.toLowerCase().includes(q)))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
openCreateModal() {
|
||||||
|
this.isEditing = false;
|
||||||
|
this.editingUserId = null;
|
||||||
|
this.errorMsg = '';
|
||||||
|
this.form = {
|
||||||
|
username: '',
|
||||||
|
display_name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
roles: [],
|
||||||
|
is_admin: false,
|
||||||
|
language_pref: 'it',
|
||||||
|
theme_pref: 'light'
|
||||||
|
};
|
||||||
|
this.showModal = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
openEditModal(user) {
|
||||||
|
this.isEditing = true;
|
||||||
|
this.editingUserId = user.id;
|
||||||
|
this.errorMsg = '';
|
||||||
|
this.form = {
|
||||||
|
username: user.username,
|
||||||
|
display_name: user.display_name || '',
|
||||||
|
email: user.email || '',
|
||||||
|
password: '',
|
||||||
|
roles: [...(user.roles || [])],
|
||||||
|
is_admin: user.is_admin || false,
|
||||||
|
language_pref: user.language_pref || 'it',
|
||||||
|
theme_pref: user.theme_pref || 'light'
|
||||||
|
};
|
||||||
|
this.showModal = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
closeModal() {
|
||||||
|
this.showModal = false;
|
||||||
|
this.errorMsg = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveUser() {
|
||||||
|
this.saving = true;
|
||||||
|
this.errorMsg = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.isEditing) {
|
||||||
|
// Update user
|
||||||
|
const data = {
|
||||||
|
display_name: this.form.display_name,
|
||||||
|
email: this.form.email || null,
|
||||||
|
roles: this.form.roles,
|
||||||
|
is_admin: this.form.is_admin,
|
||||||
|
language_pref: this.form.language_pref,
|
||||||
|
theme_pref: this.form.theme_pref
|
||||||
|
};
|
||||||
|
|
||||||
|
const resp = await fetch(`/admin/api/users/${this.editingUserId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': this.csrfToken },
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await resp.json();
|
||||||
|
if (!resp.ok) {
|
||||||
|
this.errorMsg = result.detail || 'Errore nel salvataggio';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update local array
|
||||||
|
const idx = this.users.findIndex(u => u.id === this.editingUserId);
|
||||||
|
if (idx >= 0) this.users[idx] = result;
|
||||||
|
|
||||||
|
// Change password if provided
|
||||||
|
if (this.form.password) {
|
||||||
|
const pwResp = await fetch(`/admin/api/users/${this.editingUserId}/password`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': this.csrfToken },
|
||||||
|
body: JSON.stringify({ password: this.form.password })
|
||||||
|
});
|
||||||
|
if (!pwResp.ok) {
|
||||||
|
const pwResult = await pwResp.json();
|
||||||
|
this.errorMsg = pwResult.detail || 'Errore nel cambio password';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create user
|
||||||
|
if (!this.form.username || !this.form.display_name || !this.form.password) {
|
||||||
|
this.errorMsg = '{{ _("Username, nome visualizzato e password sono obbligatori") }}';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
username: this.form.username,
|
||||||
|
password: this.form.password,
|
||||||
|
display_name: this.form.display_name,
|
||||||
|
email: this.form.email || null,
|
||||||
|
roles: this.form.roles,
|
||||||
|
is_admin: this.form.is_admin,
|
||||||
|
language_pref: this.form.language_pref,
|
||||||
|
theme_pref: this.form.theme_pref
|
||||||
|
};
|
||||||
|
|
||||||
|
const resp = await fetch('/admin/api/users', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': this.csrfToken },
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await resp.json();
|
||||||
|
if (!resp.ok) {
|
||||||
|
this.errorMsg = result.detail || 'Errore nella creazione';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.users.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.closeModal();
|
||||||
|
} catch (e) {
|
||||||
|
this.errorMsg = '{{ _("Errore di connessione al server") }}';
|
||||||
|
} finally {
|
||||||
|
this.saving = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleActive(user) {
|
||||||
|
this.toggleUser = user;
|
||||||
|
this.showToggleConfirm = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async confirmToggle() {
|
||||||
|
if (!this.toggleUser) return;
|
||||||
|
this.saving = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newActive = !this.toggleUser.active;
|
||||||
|
const resp = await fetch(`/admin/api/users/${this.toggleUser.id}/toggle-active`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': this.csrfToken },
|
||||||
|
body: JSON.stringify({ active: newActive })
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await resp.json();
|
||||||
|
if (!resp.ok) {
|
||||||
|
alert(result.detail || 'Errore');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update local array
|
||||||
|
const idx = this.users.findIndex(u => u.id === this.toggleUser.id);
|
||||||
|
if (idx >= 0) this.users[idx] = result;
|
||||||
|
|
||||||
|
this.showToggleConfirm = false;
|
||||||
|
this.toggleUser = null;
|
||||||
|
} catch (e) {
|
||||||
|
alert('{{ _("Errore di connessione al server") }}');
|
||||||
|
} finally {
|
||||||
|
this.saving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -73,6 +73,22 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{# Admin: Utenti #}
|
||||||
|
{% if current_user.get('is_admin') %}
|
||||||
|
<a href="{{ url_for('admin.user_list') }}"
|
||||||
|
class="nav-link group flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium
|
||||||
|
text-[var(--text-secondary)] hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/20
|
||||||
|
transition-colors duration-200
|
||||||
|
{% if request.endpoint and request.endpoint.startswith('admin.') %}
|
||||||
|
text-primary bg-primary-50 dark:bg-primary-900/20
|
||||||
|
{% endif %}">
|
||||||
|
<!-- Users Icon -->
|
||||||
|
<svg class="w-4.5 h-4.5" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"/>
|
||||||
|
</svg>
|
||||||
|
<span>{{ _('Utenti') }}</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -248,6 +264,18 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{# Admin: Utenti #}
|
||||||
|
{% if current_user.get('is_admin') %}
|
||||||
|
<a href="{{ url_for('admin.user_list') }}"
|
||||||
|
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium
|
||||||
|
text-[var(--text-secondary)] hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/20
|
||||||
|
transition-colors">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"/>
|
||||||
|
</svg>
|
||||||
|
{{ _('Utenti') }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1605,6 +1605,108 @@ msgstr "Select a measurement point for the histogram"
|
|||||||
msgid "Errore nella generazione del report"
|
msgid "Errore nella generazione del report"
|
||||||
msgstr "Error generating report"
|
msgstr "Error generating report"
|
||||||
|
|
||||||
|
# Admin - User Management
|
||||||
|
#: blueprints/admin.py:18
|
||||||
|
msgid "Accesso non autorizzato"
|
||||||
|
msgstr "Unauthorized access"
|
||||||
|
|
||||||
|
#: blueprints/admin.py:36
|
||||||
|
#, python-format
|
||||||
|
msgid "Errore nel caricamento degli utenti: %(error)s"
|
||||||
|
msgstr "Error loading users: %(error)s"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:3 templates/admin/users.html:13
|
||||||
|
msgid "Gestione Utenti"
|
||||||
|
msgstr "User Management"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:14
|
||||||
|
msgid "Crea, modifica e gestisci gli utenti del sistema"
|
||||||
|
msgstr "Create, edit and manage system users"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:25
|
||||||
|
msgid "Cerca utente..."
|
||||||
|
msgstr "Search user..."
|
||||||
|
|
||||||
|
#: templates/admin/users.html:38
|
||||||
|
msgid "Nuovo Utente"
|
||||||
|
msgstr "New User"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:50
|
||||||
|
msgid "Email"
|
||||||
|
msgstr "Email"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:95
|
||||||
|
msgid "Attivo"
|
||||||
|
msgstr "Active"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:95 templates/admin/users.html:323
|
||||||
|
msgid "Disattivato"
|
||||||
|
msgstr "Deactivated"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:108
|
||||||
|
msgid "Disattiva"
|
||||||
|
msgstr "Deactivate"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:108 templates/admin/users.html:323
|
||||||
|
msgid "Riattiva"
|
||||||
|
msgstr "Reactivate"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:130
|
||||||
|
msgid "Nessun utente trovato"
|
||||||
|
msgstr "No users found"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:137
|
||||||
|
msgid "utenti"
|
||||||
|
msgstr "users"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:160
|
||||||
|
msgid "Modifica Utente"
|
||||||
|
msgstr "Edit User"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:292
|
||||||
|
msgid "Crea Utente"
|
||||||
|
msgstr "Create User"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:207
|
||||||
|
msgid "lascia vuoto per non modificare"
|
||||||
|
msgstr "leave empty to keep unchanged"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:213
|
||||||
|
msgid "Nuova password (opzionale)"
|
||||||
|
msgstr "New password (optional)"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:246
|
||||||
|
msgid "Amministratore"
|
||||||
|
msgstr "Administrator"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:252
|
||||||
|
msgid "Lingua"
|
||||||
|
msgstr "Language"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:263
|
||||||
|
msgid "Tema"
|
||||||
|
msgstr "Theme"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:307
|
||||||
|
msgid "Conferma Disattivazione"
|
||||||
|
msgstr "Confirm Deactivation"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:307
|
||||||
|
msgid "Conferma Riattivazione"
|
||||||
|
msgstr "Confirm Reactivation"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:310
|
||||||
|
msgid "Sei sicuro di voler disattivare l'utente"
|
||||||
|
msgstr "Are you sure you want to deactivate user"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:311
|
||||||
|
msgid "Sei sicuro di voler riattivare l'utente"
|
||||||
|
msgstr "Are you sure you want to reactivate user"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:459
|
||||||
|
msgid "Username, nome visualizzato e password sono obbligatori"
|
||||||
|
msgstr "Username, display name and password are required"
|
||||||
|
|
||||||
#~ msgid "Esci"
|
#~ msgid "Esci"
|
||||||
#~ msgstr "Logout"
|
#~ msgstr "Logout"
|
||||||
|
|
||||||
|
|||||||
@@ -1635,6 +1635,108 @@ msgstr "Seleziona un punto di misura per l'istogramma"
|
|||||||
msgid "Errore nella generazione del report"
|
msgid "Errore nella generazione del report"
|
||||||
msgstr "Errore nella generazione del report"
|
msgstr "Errore nella generazione del report"
|
||||||
|
|
||||||
|
# Admin - User Management
|
||||||
|
#: blueprints/admin.py:18
|
||||||
|
msgid "Accesso non autorizzato"
|
||||||
|
msgstr "Accesso non autorizzato"
|
||||||
|
|
||||||
|
#: blueprints/admin.py:36
|
||||||
|
#, python-format
|
||||||
|
msgid "Errore nel caricamento degli utenti: %(error)s"
|
||||||
|
msgstr "Errore nel caricamento degli utenti: %(error)s"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:3 templates/admin/users.html:13
|
||||||
|
msgid "Gestione Utenti"
|
||||||
|
msgstr "Gestione Utenti"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:14
|
||||||
|
msgid "Crea, modifica e gestisci gli utenti del sistema"
|
||||||
|
msgstr "Crea, modifica e gestisci gli utenti del sistema"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:25
|
||||||
|
msgid "Cerca utente..."
|
||||||
|
msgstr "Cerca utente..."
|
||||||
|
|
||||||
|
#: templates/admin/users.html:38
|
||||||
|
msgid "Nuovo Utente"
|
||||||
|
msgstr "Nuovo Utente"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:50
|
||||||
|
msgid "Email"
|
||||||
|
msgstr "Email"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:95
|
||||||
|
msgid "Attivo"
|
||||||
|
msgstr "Attivo"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:95 templates/admin/users.html:323
|
||||||
|
msgid "Disattivato"
|
||||||
|
msgstr "Disattivato"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:108
|
||||||
|
msgid "Disattiva"
|
||||||
|
msgstr "Disattiva"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:108 templates/admin/users.html:323
|
||||||
|
msgid "Riattiva"
|
||||||
|
msgstr "Riattiva"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:130
|
||||||
|
msgid "Nessun utente trovato"
|
||||||
|
msgstr "Nessun utente trovato"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:137
|
||||||
|
msgid "utenti"
|
||||||
|
msgstr "utenti"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:160
|
||||||
|
msgid "Modifica Utente"
|
||||||
|
msgstr "Modifica Utente"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:292
|
||||||
|
msgid "Crea Utente"
|
||||||
|
msgstr "Crea Utente"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:207
|
||||||
|
msgid "lascia vuoto per non modificare"
|
||||||
|
msgstr "lascia vuoto per non modificare"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:213
|
||||||
|
msgid "Nuova password (opzionale)"
|
||||||
|
msgstr "Nuova password (opzionale)"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:246
|
||||||
|
msgid "Amministratore"
|
||||||
|
msgstr "Amministratore"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:252
|
||||||
|
msgid "Lingua"
|
||||||
|
msgstr "Lingua"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:263
|
||||||
|
msgid "Tema"
|
||||||
|
msgstr "Tema"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:307
|
||||||
|
msgid "Conferma Disattivazione"
|
||||||
|
msgstr "Conferma Disattivazione"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:307
|
||||||
|
msgid "Conferma Riattivazione"
|
||||||
|
msgstr "Conferma Riattivazione"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:310
|
||||||
|
msgid "Sei sicuro di voler disattivare l'utente"
|
||||||
|
msgstr "Sei sicuro di voler disattivare l'utente"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:311
|
||||||
|
msgid "Sei sicuro di voler riattivare l'utente"
|
||||||
|
msgstr "Sei sicuro di voler riattivare l'utente"
|
||||||
|
|
||||||
|
#: templates/admin/users.html:459
|
||||||
|
msgid "Username, nome visualizzato e password sono obbligatori"
|
||||||
|
msgstr "Username, nome visualizzato e password sono obbligatori"
|
||||||
|
|
||||||
#~ msgid "Esci"
|
#~ msgid "Esci"
|
||||||
#~ msgstr "Esci"
|
#~ msgstr "Esci"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user