Replace role name comparisons with slug checks and display role.name to support dynamic role definitions.
445 lines
28 KiB
HTML
445 lines
28 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}Users{% endblock %}
|
||
{% block breadcrumb %}
|
||
<nav class="mb-4" aria-label="Breadcrumb">
|
||
<ol class="flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||
<li><a href="{% url 'dashboard' %}" class="hover:text-gray-700 dark:hover:text-gray-300">Dashboard</a></li>
|
||
<li class="text-gray-300 dark:text-gray-600">/</li>
|
||
<li class="text-gray-900 dark:text-white font-medium" aria-current="page">Users</li>
|
||
</ol>
|
||
</nav>
|
||
{% endblock %}
|
||
{% block content %}
|
||
<div x-data="{ columns: {{ columns|safe }}, openCols: false, selectedIds: [], selectAll: false, viewModal: false, viewUser: null, editModal: false, editUser: {}, userModalMode: 'edit' }">
|
||
<div class="mb-6 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||
<div>
|
||
<h1 class="text-2xl font-bold">Users</h1>
|
||
<p class="text-gray-500 dark:text-gray-400 text-sm mt-1">{{ page_obj.paginator.count }} active users</p>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<button type="button" @click="editUser = { username: '', email: '', first_name: '', last_name: '', role: '', is_active: true }; userModalMode = 'create'; editModal = true" class="dk-btn dk-btn-primary text-sm"><i class="fas fa-plus"></i> New User</button>
|
||
<form class="flex gap-2" method="get">
|
||
<input type="text" name="search" value="{{ search }}" placeholder="Search by name or email..."
|
||
class="text-sm border border-gray-200 dark:border-gray-600 rounded-lg px-3 py-2 bg-white dark:bg-gray-950 focus:outline-none focus:ring-2 focus:ring-blue-500 w-64">
|
||
<button type="submit" class="dk-btn dk-btn-primary text-sm"><i class="fas fa-search"></i> Search</button>
|
||
{% if search %}
|
||
<a href="{% url 'users-list' %}" class="dk-btn dk-btn-ghost text-sm"><i class="fas fa-times"></i> Clear</a>
|
||
{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<form id="users-bulk-delete-form" method="post" action="{% url 'users-bulk-delete' %}">
|
||
{% csrf_token %}
|
||
<template x-for="id in selectedIds" x-key="id">
|
||
<input type="hidden" name="ids" :value="id">
|
||
</template>
|
||
</form>
|
||
<div x-show="selectedIds.length > 0" x-cloak class="mb-4 flex items-center gap-3 px-4 py-2.5 bg-primary-50 dark:bg-primary-900/20 border border-primary-200 dark:border-primary-800 rounded-lg">
|
||
<span class="text-sm font-medium text-primary-700 dark:text-primary-300">
|
||
<span x-text="selectedIds.length"></span> selected
|
||
</span>
|
||
<button type="button" @click="confirmAction({title:'Delete Users', message:'Delete ' + selectedIds.length + ' user(s)?', confirmText:'Delete', danger:true, onConfirm:function(){ document.getElementById('users-bulk-delete-form').submit() }})" class="dk-btn dk-btn-danger text-xs px-3 py-1">
|
||
<i class="fas fa-trash mr-1"></i> Delete Selected
|
||
</button>
|
||
<button type="button" @click="selectedIds = []; selectAll = false" class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 ml-auto">
|
||
<i class="fas fa-times mr-1"></i> Clear selection
|
||
</button>
|
||
</div>
|
||
|
||
<div class="rounded-xl bg-white dark:bg-gray-950 shadow-sm ring-1 ring-gray-950/5 dark:ring-white/10">
|
||
<div class="px-6 py-3 border-b border-gray-100 dark:border-gray-600 flex flex-wrap items-center justify-between gap-3">
|
||
<form id="user-filters" method="get" class="flex flex-wrap items-center gap-3">
|
||
<label class="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400 cursor-pointer">
|
||
<input type="checkbox" name="has_projects" value="1" {% if has_projects %}checked{% endif %} onchange="this.form.submit()" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
|
||
Has projects
|
||
</label>
|
||
<label class="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400 cursor-pointer">
|
||
<input type="checkbox" name="has_assigned" value="1" {% if has_assigned %}checked{% endif %} onchange="this.form.submit()" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
|
||
Has assigned
|
||
</label>
|
||
<label class="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400 cursor-pointer">
|
||
<input type="checkbox" name="has_created" value="1" {% if has_created %}checked{% endif %} onchange="this.form.submit()" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
|
||
Has created
|
||
</label>
|
||
<select name="role" onchange="this.form.submit()" class="text-xs border border-gray-200 dark:border-gray-600 rounded-lg px-2 py-1.5 bg-white dark:bg-gray-950 text-gray-700 dark:text-gray-300">
|
||
<option value="">All roles</option>
|
||
{% for r in roles %}
|
||
<option value="{{ r.slug }}" {% if role == r.slug %}selected{% endif %}>{{ r.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
{% if has_projects or has_assigned or has_created or role %}
|
||
<a href="{% url 'users-list' %}" class="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 font-medium">Clear filters</a>
|
||
{% endif %}
|
||
</form>
|
||
<div class="flex items-center gap-3">
|
||
<div class="relative">
|
||
<button @click="openCols = !openCols" class="text-xs flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300">
|
||
<i class="fas fa-columns text-[10px]"></i> Columns
|
||
</button>
|
||
<div x-show="openCols" @click.outside="openCols = false" x-cloak class="absolute right-0 mt-1 w-44 bg-white dark:bg-gray-950 rounded-lg shadow-lg ring-1 ring-gray-950/5 dark:ring-white/10 z-50 py-1">
|
||
<form method="get" id="columns-form">
|
||
{% for c in all_columns %}
|
||
<label class="flex items-center gap-2 px-3 py-1.5 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer">
|
||
<input type="checkbox" name="columns" value="{{ c }}" {% if c in columns %}checked{% endif %} onchange="document.getElementById('columns-form').submit()" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
|
||
{{ c|title }}
|
||
</label>
|
||
{% endfor %}
|
||
{% for key, val in request.GET.items %}{% if key != 'columns' and key != 'page' %}<input type="hidden" name="{{ key }}" value="{{ val }}">{% endif %}{% endfor %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
<select name="per_page" onchange="this.form.submit()" form="user-filters" class="text-xs border border-gray-200 dark:border-gray-600 rounded-lg px-2 py-1.5 bg-white dark:bg-gray-950 text-gray-700 dark:text-gray-300">
|
||
<option value="5" {% if per_page == 5 %}selected{% endif %}>5 per page</option>
|
||
<option value="10" {% if per_page == 10 %}selected{% endif %}>10 per page</option>
|
||
<option value="25" {% if per_page == 25 %}selected{% endif %}>25 per page</option>
|
||
<option value="50" {% if per_page == 50 %}selected{% endif %}>50 per page</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="overflow-x-auto">
|
||
<table class="w-full text-sm">
|
||
<thead>
|
||
<tr class="border-b border-gray-100 dark:border-gray-600 bg-gray-50 dark:bg-gray-800">
|
||
<th class="px-3 py-3 w-10">
|
||
<input type="checkbox" @change="selectAll = !selectAll; selectedIds = selectAll ? Array.from(document.querySelectorAll('.user-checkbox')).map(cb => cb.value) : []" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
|
||
</th>
|
||
{% if "name" in columns %}<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name</th>{% endif %}
|
||
{% if "email" in columns %}<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Email</th>{% endif %}
|
||
{% if "roles" in columns %}<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Roles</th>{% endif %}
|
||
{% if "projects" in columns %}<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Projects</th>{% endif %}
|
||
{% if "assigned" in columns %}<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Assigned</th>{% endif %}
|
||
{% if "created" in columns %}<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Created</th>{% endif %}
|
||
{% if "joined" in columns %}<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Joined</th>{% endif %}
|
||
<th class="px-6 py-3 text-right text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for u in users %}
|
||
<tr class="border-b border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors" :class="selectedIds.includes('{{ u.id }}') && 'bg-blue-50 dark:bg-blue-900/10'">
|
||
<td class="px-3 py-3">
|
||
<input type="checkbox" class="user-checkbox rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500" value="{{ u.id }}" x-model="selectedIds">
|
||
</td>
|
||
{% if "name" in columns %}
|
||
<td class="px-6 py-3">
|
||
<div class="flex items-center gap-3">
|
||
{% if u.profile_image %}
|
||
<img src="{{ u.profile_image.url }}" alt="{{ u.get_full_name|default:u.username }}" class="w-9 h-9 rounded-full object-cover shrink-0">
|
||
{% else %}
|
||
<div class="w-9 h-9 rounded-full flex items-center justify-center text-white font-bold text-sm shrink-0" style="background-color:{% cycle '#3B82F6' '#10B981' '#8B5CF6' '#F59E0B' '#EF4444' '#06B6D4' '#F97316' '#EC4899' %}">
|
||
{{ u.get_full_name|default:u.username|make_list|first|upper }}
|
||
</div>
|
||
{% endif %}
|
||
<div>
|
||
<p class="font-medium text-sm text-gray-900 dark:text-white">{{ u.get_full_name|default:u.username }}</p>
|
||
<p class="text-xs text-gray-400">@{{ u.username }}</p>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
{% endif %}
|
||
{% if "email" in columns %}
|
||
<td class="px-6 py-3 text-sm text-gray-600 dark:text-gray-400">{{ u.email|default:"-" }}</td>
|
||
{% endif %}
|
||
{% if "roles" in columns %}
|
||
<td class="px-6 py-3">
|
||
{% if u.role %}
|
||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium
|
||
{% if u.role.slug in admin_roles %}bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400
|
||
{% else %}bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400{% endif %}">
|
||
{{ u.role.name }}
|
||
</span>
|
||
{% else %}
|
||
-
|
||
{% endif %}
|
||
</td>
|
||
{% endif %}
|
||
{% if "projects" in columns %}
|
||
<td class="px-6 py-3 text-center text-sm font-medium text-gray-900 dark:text-white">{{ u.project_count }}</td>
|
||
{% endif %}
|
||
{% if "assigned" in columns %}
|
||
<td class="px-6 py-3 text-center text-sm font-medium text-gray-900 dark:text-white">{{ u.assigned_count }}</td>
|
||
{% endif %}
|
||
{% if "created" in columns %}
|
||
<td class="px-6 py-3 text-center text-sm font-medium text-gray-900 dark:text-white">{{ u.ticket_count }}</td>
|
||
{% endif %}
|
||
{% if "joined" in columns %}
|
||
<td class="px-6 py-3 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">{{ u.date_joined|date:"M d, Y" }}</td>
|
||
{% endif %}
|
||
<td class="px-6 py-3 text-right">
|
||
<div class="flex items-center justify-end gap-1">
|
||
<button type="button"
|
||
@click="viewUser = usersData['{{ u.id }}']; viewModal = true"
|
||
class="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors shadow-sm">View</button>
|
||
<button type="button"
|
||
@click="editUser = Object.assign({}, usersData['{{ u.id }}']); userModalMode = 'edit'; editModal = true"
|
||
class="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors shadow-sm">Edit</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% empty %}
|
||
<tr>
|
||
<td colspan="9" class="text-center py-12 text-gray-400">
|
||
<i class="fas fa-users text-4xl block mb-3 text-gray-300 dark:text-gray-600"></i>
|
||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">No users found</p>
|
||
{% if search %}
|
||
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">No users match your search criteria.</p>
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{% if page_obj.has_other_pages %}
|
||
<div class="px-6 py-3 border-t border-gray-100 dark:border-gray-600 flex items-center justify-between">
|
||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||
Showing {{ page_obj.start_index }}–{{ page_obj.end_index }} of {{ page_obj.paginator.count }}
|
||
</p>
|
||
<div class="flex items-center gap-1">
|
||
{% if page_obj.has_previous %}
|
||
<a href="?page={{ page_obj.previous_page_number }}{% if search %}&search={{ search }}{% endif %}{% if has_projects %}&has_projects=1{% endif %}{% if has_assigned %}&has_assigned=1{% endif %}{% if has_created %}&has_created=1{% endif %}{% if role %}&role={{ role }}{% endif %}{% if per_page != 10 %}&per_page={{ per_page }}{% endif %}" class="px-2.5 py-1 rounded text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800">Previous</a>
|
||
{% endif %}
|
||
{% for n in page_obj.paginator.page_range %}
|
||
{% if n == page_obj.number %}
|
||
<span class="px-2.5 py-1 rounded text-xs font-medium bg-blue-600 text-white">{{ n }}</span>
|
||
{% elif n >= page_obj.number|add:"-2" and n <= page_obj.number|add:"2" %}
|
||
<a href="?page={{ n }}{% if search %}&search={{ search }}{% endif %}{% if has_projects %}&has_projects=1{% endif %}{% if has_assigned %}&has_assigned=1{% endif %}{% if has_created %}&has_created=1{% endif %}{% if role %}&role={{ role }}{% endif %}{% if per_page != 10 %}&per_page={{ per_page }}{% endif %}" class="px-2.5 py-1 rounded text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800">{{ n }}</a>
|
||
{% endif %}
|
||
{% endfor %}
|
||
{% if page_obj.has_next %}
|
||
<a href="?page={{ page_obj.next_page_number }}{% if search %}&search={{ search }}{% endif %}{% if has_projects %}&has_projects=1{% endif %}{% if has_assigned %}&has_assigned=1{% endif %}{% if has_created %}&has_created=1{% endif %}{% if role %}&role={{ role }}{% endif %}{% if per_page != 10 %}&per_page={{ per_page }}{% endif %}" class="px-2.5 py-1 rounded text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800">Next</a>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- View User Modal -->
|
||
<div x-show="viewModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center"
|
||
@keydown.escape.window="viewModal = false">
|
||
<div class="fixed inset-0 bg-black/50" @click="viewModal = false"></div>
|
||
<div class="relative bg-white dark:bg-gray-900 rounded-xl shadow-xl border border-gray-200 dark:border-gray-700 w-full max-w-lg mx-4 sm:mx-auto my-auto z-10 max-h-[90vh] overflow-y-auto">
|
||
|
||
<!-- Header -->
|
||
<div class="px-6 py-5 flex items-center justify-between">
|
||
<h3 class="text-base font-bold text-gray-900 dark:text-white">View user</h3>
|
||
<button type="button" @click="viewModal = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Body -->
|
||
<div class="px-6 pb-4 space-y-4" x-show="viewUser">
|
||
|
||
<!-- Row 1: Name | Email -->
|
||
<div class="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1">Name</label>
|
||
<div class="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
||
x-text="viewUser?.full_name || '—'"></div>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1">Email</label>
|
||
<div class="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
||
x-text="viewUser?.email || '—'"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Row 2: Joined | Last Login -->
|
||
<div class="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1">Joined</label>
|
||
<div class="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
||
x-text="viewUser?.date_joined || '—'"></div>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1">Last Login</label>
|
||
<div class="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400"
|
||
x-text="viewUser?.last_login || 'Never'"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Row 3: Roles -->
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1">Roles</label>
|
||
<div class="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 flex items-center gap-2">
|
||
<span class="inline-flex items-center px-2.5 py-0.5 rounded text-xs font-medium"
|
||
:class="{
|
||
'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300': viewUser?.role === 'superadmin',
|
||
'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300': viewUser?.role === 'manager',
|
||
'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300': true
|
||
}"
|
||
x-text="viewUser?.role_label || viewUser?.role || '—'">
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Footer -->
|
||
<div class="px-6 pb-6 pt-2 flex flex-wrap gap-2.5">
|
||
<button type="button" @click="viewModal = false"
|
||
class="px-4 py-1.5 text-sm font-medium text-gray-900 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||
Close
|
||
</button>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Edit/Create User Modal -->
|
||
<div x-show="editModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center"
|
||
@keydown.escape.window="editModal = false">
|
||
<div class="fixed inset-0 bg-black/50" @click="editModal = false"></div>
|
||
<div class="relative bg-white dark:bg-gray-900 rounded-xl shadow-xl border border-gray-200 dark:border-gray-700 w-full max-w-3xl mx-4 sm:mx-auto my-auto z-10 max-h-[90vh] overflow-y-auto">
|
||
|
||
<!-- Header -->
|
||
<div class="px-6 py-5 flex items-center justify-between border-b border-gray-100 dark:border-gray-800">
|
||
<h3 class="text-base font-bold text-gray-900 dark:text-white" x-text="userModalMode === 'create' ? 'Create User' : 'Edit ' + (editUser?.full_name || 'User')">Edit User</h3>
|
||
<button type="button" @click="editModal = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Form -->
|
||
<form method="post" :action="userModalMode === 'create' ? '/users/create/' : '/users/' + editUser?.id + '/edit/'" enctype="multipart/form-data">
|
||
{% csrf_token %}
|
||
|
||
<div class="px-6 py-4 space-y-4" x-show="editUser">
|
||
<!-- Row 1: Username | Email -->
|
||
<div class="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1">Username <span class="text-red-500">*</span></label>
|
||
<input type="text" name="username" x-model="editUser.username" required
|
||
class="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1">Email <span class="text-red-500">*</span></label>
|
||
<input type="email" name="email" x-model="editUser.email" required
|
||
class="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Row 2: First Name | Last Name -->
|
||
<div class="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1">First Name</label>
|
||
<input type="text" name="first_name" x-model="editUser.first_name"
|
||
class="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1">Last Name</label>
|
||
<input type="text" name="last_name" x-model="editUser.last_name"
|
||
class="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Password Fields (Create Mode Only) -->
|
||
<template x-if="userModalMode === 'create'">
|
||
<div class="grid grid-cols-2 gap-4" x-data="{ showPass1: false, showPass2: false }">
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1">Password <span class="text-red-500">*</span></label>
|
||
<div class="relative">
|
||
<input :type="showPass1 ? 'text' : 'password'" name="password1" required placeholder="Password"
|
||
class="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 pr-10">
|
||
<button type="button" @click="showPass1 = !showPass1" class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||
<i class="fas" :class="showPass1 ? 'fa-eye-slash' : 'fa-eye'"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1">Confirm Password <span class="text-red-500">*</span></label>
|
||
<div class="relative">
|
||
<input :type="showPass2 ? 'text' : 'password'" name="password2" required placeholder="Confirm password"
|
||
class="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 pr-10">
|
||
<button type="button" @click="showPass2 = !showPass2" class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||
<i class="fas" :class="showPass2 ? 'fa-eye-slash' : 'fa-eye'"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Row 3: Role -->
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1">Role</label>
|
||
<select name="role" x-model="editUser.role"
|
||
class="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||
{% for role in roles %}
|
||
<option value="{{ role.slug }}">{{ role.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Row 4: Profile Image -->
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1">Profile Image</label>
|
||
<input type="file" name="profile_image"
|
||
class="w-full text-xs text-gray-500 border border-gray-200 dark:border-gray-700 rounded-lg px-2 py-1.5 bg-white dark:bg-gray-800 focus:outline-none file:mr-3 file:py-1 file:px-2 file:rounded file:border-0 file:text-xs file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
|
||
</div>
|
||
|
||
<!-- Row 5: Active Toggle -->
|
||
<div class="mt-4">
|
||
<input type="hidden" name="is_active" :value="editUser.is_active ? '1' : '0'">
|
||
<label class="flex items-start gap-3 cursor-pointer" @click="editUser.is_active = !editUser.is_active">
|
||
<div role="switch" class="mt-0.5 relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
|
||
:style="editUser.is_active ? 'background-color: var(--color-primary, #3b82f6)' : 'background-color: #d1d5db'">
|
||
<span class="pointer-events-none relative inline-block h-4 w-4 rounded-full bg-white shadow-sm ring-0 transition-transform duration-200 ease-in-out"
|
||
:style="editUser.is_active ? 'transform: translateX(16px)' : 'transform: translateX(0)'"></span>
|
||
</div>
|
||
<div>
|
||
<p class="text-sm font-semibold text-gray-900 dark:text-white">Active</p>
|
||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">User is active and can log in</p>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Footer -->
|
||
<div class="px-6 py-5 border-t border-gray-100 dark:border-gray-800 flex justify-end gap-2">
|
||
<button type="button" @click="editModal = false"
|
||
class="px-4 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||
Cancel
|
||
</button>
|
||
<button type="submit"
|
||
class="px-4 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors shadow-sm"
|
||
x-text="userModalMode === 'create' ? 'Create User' : 'Save Changes'">
|
||
Save Changes
|
||
</button>
|
||
</div>
|
||
</form>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const usersData = {
|
||
{% for u in users %}
|
||
"{{ u.id }}": {
|
||
id: {{ u.id }},
|
||
full_name: "{{ u.get_full_name|default:u.username|escapejs }}",
|
||
username: "{{ u.username|escapejs }}",
|
||
email: "{{ u.email|escapejs }}",
|
||
first_name: "{{ u.first_name|escapejs }}",
|
||
last_name: "{{ u.last_name|escapejs }}",
|
||
role: "{{ u.role.slug|default:'' }}",
|
||
role_label: "{{ u.role.name|default:'' }}",
|
||
is_active: {{ u.is_active|yesno:'true,false' }},
|
||
date_joined: "{{ u.date_joined|date:'M d, Y' }}",
|
||
last_login: "{{ u.last_login|date:'M d, Y'|default:'Never' }}",
|
||
project_count: {{ u.project_count }},
|
||
assigned_count: {{ u.assigned_count }},
|
||
ticket_count: {{ u.ticket_count }},
|
||
initials: "{{ u.get_full_name|default:u.username|make_list|first|upper|escapejs }}",
|
||
avatar_color: "{% cycle '#3B82F6' '#10B981' '#8B5CF6' '#F59E0B' '#EF4444' '#06B6D4' '#F97316' '#EC4899' %}"
|
||
}{% if not forloop.last %},{% endif %}
|
||
{% endfor %}
|
||
};
|
||
</script>
|
||
{% endblock %} |