Replace role name comparisons with slug checks and display role.name to support dynamic role definitions.
398 lines
26 KiB
HTML
398 lines
26 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}{% if editing %}Edit User{% else %}Create User{% endif %}{% endblock %}
|
||
{% block breadcrumb %}
|
||
<nav class="mb-4" aria-label="Breadcrumb">
|
||
<ol class="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
|
||
<li><a href="{% url 'users-list' %}" class="hover:text-gray-700 dark:hover:text-gray-300">Users</a></li>
|
||
<li class="text-gray-300 dark:text-gray-600"><i class="fas fa-chevron-right text-[10px]"></i></li>
|
||
{% if editing %}
|
||
<li><a href="{% url 'user-detail' user_obj.id %}" class="hover:text-gray-700 dark:hover:text-gray-300">{{ user_obj.get_full_name|default:user_obj.username }}</a></li>
|
||
<li class="text-gray-300 dark:text-gray-600"><i class="fas fa-chevron-right text-[10px]"></i></li>
|
||
<li class="text-gray-900 dark:text-white font-medium" aria-current="page">Edit</li>
|
||
{% else %}
|
||
<li class="text-gray-900 dark:text-white font-medium" aria-current="page">Create</li>
|
||
{% endif %}
|
||
</ol>
|
||
</nav>
|
||
{% endblock %}
|
||
{% block content %}
|
||
<div class="w-full">
|
||
|
||
<!-- Page Header -->
|
||
<div class="mb-6 flex items-center justify-between">
|
||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white tracking-tight">
|
||
{% if editing %}Edit User{% else %}Create User{% endif %}
|
||
</h1>
|
||
</div>
|
||
|
||
<!-- Form -->
|
||
<form method="post" enctype="multipart/form-data">
|
||
{% csrf_token %}
|
||
|
||
<div class="space-y-6">
|
||
|
||
<!-- Row 1: Name | Email -->
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1.5">
|
||
Name <span class="text-red-500">*</span>
|
||
</label>
|
||
<input type="text" name="username" value="{{ form.username.value|default:'' }}"
|
||
required placeholder="Username"
|
||
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-950 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none transition-colors">
|
||
{% if form.username.errors %}<p class="text-xs text-red-500 mt-1">{{ form.username.errors.0 }}</p>{% endif %}
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1.5">
|
||
Email <span class="text-red-500">*</span>
|
||
</label>
|
||
<input type="email" name="email" value="{{ form.email.value|default:'' }}"
|
||
placeholder="Email address"
|
||
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-950 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none transition-colors">
|
||
{% if form.email.errors %}<p class="text-xs text-red-500 mt-1">{{ form.email.errors.0 }}</p>{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Row 2: First Name | Last Name -->
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1.5">First Name</label>
|
||
<input type="text" name="first_name" value="{{ form.first_name.value|default:'' }}"
|
||
placeholder="First name"
|
||
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-950 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none transition-colors">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1.5">Last Name</label>
|
||
<input type="text" name="last_name" value="{{ form.last_name.value|default:'' }}"
|
||
placeholder="Last name"
|
||
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-950 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none transition-colors">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Row 3: Password fields (create only) -->
|
||
{% if not editing %}
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1.5">
|
||
Password <span class="text-red-500">*</span>
|
||
</label>
|
||
<input type="password" name="password1" required placeholder="Password"
|
||
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-950 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none transition-colors">
|
||
{% if form.password1.errors %}<p class="text-xs text-red-500 mt-1">{{ form.password1.errors.0 }}</p>{% endif %}
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1.5">
|
||
Confirm Password <span class="text-red-500">*</span>
|
||
</label>
|
||
<input type="password" name="password2" required placeholder="Confirm password"
|
||
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-950 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none transition-colors">
|
||
{% if form.password2.errors %}<p class="text-xs text-red-500 mt-1">{{ form.password2.errors.0 }}</p>{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Roles -->
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1.5">Roles</label>
|
||
{% if can_change_role %}
|
||
<select name="role" class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-950 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none transition-colors">
|
||
{% for role in roles %}
|
||
<option value="{{ role.slug }}" {% if editing and current_role == role or not editing and role.slug == 'backend_developer' %}selected{% endif %}>{{ role.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
{% else %}
|
||
<input type="hidden" name="role" value="backend_developer">
|
||
<div class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
|
||
Only admins can change roles.
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Active toggle -->
|
||
<div x-data="{ isActive: {{ user_obj.is_active|yesno:'true,false'|default:'true' }} }">
|
||
<input type="hidden" name="is_active" :value="isActive ? '1' : '0'">
|
||
<label class="flex items-start gap-3 cursor-pointer" @click.prevent="isActive = !isActive">
|
||
<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="isActive ? '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="isActive ? '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-sm text-gray-500 dark:text-gray-400 mt-0.5">User is active and can log in</p>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
|
||
<!-- Profile image (editing only) -->
|
||
{% if editing and user_obj.profile_image %}
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1.5">Profile Image</label>
|
||
<div class="flex items-center gap-3">
|
||
<img src="{{ user_obj.profile_image.url }}" alt="Profile" class="w-12 h-12 rounded-full object-cover">
|
||
{{ form.profile_image }}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
</div>
|
||
|
||
<!-- Action Buttons -->
|
||
<div class="flex items-center gap-3" style="margin-top: 2rem;">
|
||
<button type="submit"
|
||
class="bg-blue-600 hover:bg-blue-700 text-white font-medium text-sm px-4 py-2 rounded-lg transition-colors">
|
||
{% if editing %}Save Changes{% else %}Create User{% endif %}
|
||
</button>
|
||
<a href="{% url 'users-list' %}"
|
||
class="bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-900 dark:text-white font-medium text-sm px-4 py-2 rounded-lg transition-colors">
|
||
Cancel
|
||
</a>
|
||
</div>
|
||
</form>
|
||
|
||
<!-- Projects Section (edit mode only) -->
|
||
{% if editing %}
|
||
<div id="projects" style="margin-top: 4rem;" class="rounded-xl bg-white dark:bg-gray-950 shadow-sm ring-1 ring-gray-950/5 dark:ring-white/10"
|
||
x-data="{
|
||
openCols: false,
|
||
cols: { prefix: true, tickets: true, members: true },
|
||
createMemberOpen: {{ request.GET.open_attach|yesno:'true,false' }},
|
||
createMemberData: { project_id: '' },
|
||
open: false,
|
||
search: '',
|
||
selectedIds: [],
|
||
selectAll: false,
|
||
options: [
|
||
{% for p in available_projects %}
|
||
{ id: {{ p.id }}, name: '{{ p.name|escapejs }}' }{% if not forloop.last %},{% endif %}
|
||
{% endfor %}
|
||
],
|
||
get filteredOptions() {
|
||
if (this.search === '') return this.options;
|
||
return this.options.filter(i => String(i.name).toLowerCase().includes(this.search.toLowerCase()));
|
||
}
|
||
}">
|
||
|
||
<!-- Section header -->
|
||
<div class="px-6 py-4 border-b border-gray-100 dark:border-gray-600 flex flex-wrap items-center justify-between gap-3">
|
||
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">Projects</h3>
|
||
<div class="flex items-center gap-2">
|
||
<!-- Search -->
|
||
<form method="GET" class="flex items-center gap-2">
|
||
<input type="hidden" name="proj_per_page" value="{{ proj_per_page }}">
|
||
<div class="relative">
|
||
<input type="text" name="project_search" value="{{ project_search }}" placeholder="Search projects..."
|
||
class="px-3 py-1.5 text-xs border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-950 text-gray-900 dark:text-gray-100 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500 w-44">
|
||
</div>
|
||
<button type="submit" class="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-200 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">Search</button>
|
||
{% if project_search %}
|
||
<a href="?#projects" class="text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">Clear</a>
|
||
{% endif %}
|
||
</form>
|
||
|
||
<!-- Per page -->
|
||
<form method="GET">
|
||
<input type="hidden" name="project_search" value="{{ project_search }}">
|
||
<select name="proj_per_page" onchange="this.form.submit()"
|
||
class="text-xs border border-gray-200 dark:border-gray-600 rounded-lg py-1.5 px-2 bg-white dark:bg-gray-950 text-gray-700 dark:text-gray-300 focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||
<option value="5" {% if proj_per_page == 5 %}selected{% endif %}>5 / page</option>
|
||
<option value="10" {% if proj_per_page == 10 %}selected{% endif %}>10 / page</option>
|
||
<option value="25" {% if proj_per_page == 25 %}selected{% endif %}>25 / page</option>
|
||
</select>
|
||
</form>
|
||
|
||
<!-- Column toggle -->
|
||
<div class="relative">
|
||
<button @click="openCols = !openCols"
|
||
class="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7"/>
|
||
</svg>
|
||
Columns
|
||
</button>
|
||
<div x-show="openCols" @click.outside="openCols = false" x-cloak
|
||
class="absolute right-0 mt-1 w-40 bg-white dark:bg-gray-950 rounded-lg shadow-lg ring-1 ring-gray-950/5 dark:ring-white/10 z-20 py-1">
|
||
<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" x-model="cols.prefix" class="rounded border-gray-300 text-blue-600"> Ticket Prefix
|
||
</label>
|
||
<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" x-model="cols.tickets" class="rounded border-gray-300 text-blue-600"> Tickets
|
||
</label>
|
||
<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" x-model="cols.members" class="rounded border-gray-300 text-blue-600"> Members
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Attach button -->
|
||
<button type="button" @click="createMemberOpen = true"
|
||
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors shadow-sm">
|
||
<i class="fas fa-plus"></i> Attach Project
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bulk delete form -->
|
||
<form id="projects-bulk-delete-form" method="post" action="{% url 'project-member-bulk-delete' %}">
|
||
{% csrf_token %}
|
||
<input type="hidden" name="user_id" value="{{ user_obj.id }}">
|
||
<template x-for="id in selectedIds" x-key="id">
|
||
<input type="hidden" name="member_ids" :value="id">
|
||
</template>
|
||
</form>
|
||
|
||
<!-- Selection toolbar -->
|
||
<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:'Detach Projects', message:'Detach ' + selectedIds.length + ' project(s)?', confirmText:'Detach', danger:true, onConfirm:function(){ document.getElementById('projects-bulk-delete-form').submit() }})"
|
||
class="dk-btn dk-btn-danger text-xs px-3 py-1">
|
||
<i class="fas fa-trash mr-1"></i> Detach 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="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-4 py-3 w-10">
|
||
<input type="checkbox" @change="selectAll = !selectAll; selectedIds = selectAll ? Array.from(document.querySelectorAll('.member-checkbox')).map(cb => cb.value) : []" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
|
||
</th>
|
||
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Project Name</th>
|
||
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Description</th>
|
||
<th x-show="cols.prefix" class="px-4 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Ticket Prefix</th>
|
||
<th x-show="cols.tickets" class="px-4 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Tickets</th>
|
||
<th x-show="cols.members" class="px-4 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Members</th>
|
||
<th class="px-4 py-3 text-right text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
||
{% for membership in user_memberships %}
|
||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors" :class="selectedIds.includes('{{ membership.id }}') && 'bg-blue-50 dark:bg-blue-900/10'">
|
||
<td class="px-4 py-3">
|
||
<input type="checkbox" class="member-checkbox rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500" :value="'{{ membership.id }}'" x-model="selectedIds">
|
||
</td>
|
||
<td class="px-4 py-3 font-medium text-gray-900 dark:text-white">{{ membership.project.name }}</td>
|
||
<td class="px-4 py-3 text-gray-500 dark:text-gray-400 max-w-xs truncate">{{ membership.project.description|default:"—"|striptags|truncatechars:50 }}</td>
|
||
<td x-show="cols.prefix" class="px-4 py-3 text-center">
|
||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-mono font-medium bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300">{{ membership.project.ticket_prefix }}</span>
|
||
</td>
|
||
<td x-show="cols.tickets" class="px-4 py-3 text-center text-gray-900 dark:text-white font-medium">{{ membership.ticket_count }}</td>
|
||
<td x-show="cols.members" class="px-4 py-3 text-center text-gray-900 dark:text-white font-medium">{{ membership.member_count }}</td>
|
||
<td class="px-4 py-3 text-right">
|
||
<div class="flex items-center justify-end gap-3">
|
||
<a href="{% url 'project-detail' membership.project.id %}"
|
||
class="text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 font-medium">View</a>
|
||
<form method="post" action="{% url 'project-member-delete' membership.id %}" class="inline">
|
||
{% csrf_token %}
|
||
<button type="button"
|
||
@click="confirmAction({title:'Detach Project', message:'Remove this user from {{ membership.project.name|escapejs }}?', confirmText:'Detach', danger:true, onConfirm:function(){ $el.closest('form').submit() }})"
|
||
class="text-xs text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 font-medium">Detach</button>
|
||
</form>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% empty %}
|
||
<tr>
|
||
<td colspan="7" class="px-4 py-10 text-center text-sm text-gray-400 dark:text-gray-500">
|
||
{% if project_search %}No projects match "{{ project_search }}".{% else %}Not assigned to any projects yet.{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Footer: count + pagination -->
|
||
<div class="px-4 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 {{ proj_page_obj.start_index }}–{{ proj_page_obj.end_index }} of {{ proj_page_obj.paginator.count }} result{{ proj_page_obj.paginator.count|pluralize }}
|
||
</p>
|
||
{% if proj_page_obj.has_other_pages %}
|
||
<div class="flex items-center gap-1">
|
||
{% if proj_page_obj.has_previous %}
|
||
<a href="?proj_page={{ proj_page_obj.previous_page_number }}&proj_per_page={{ proj_per_page }}{% if project_search %}&project_search={{ project_search }}{% endif %}#projects"
|
||
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 proj_page_obj.paginator.page_range %}
|
||
{% if n == proj_page_obj.number %}
|
||
<span class="px-2.5 py-1 rounded text-xs font-medium bg-blue-600 text-white">{{ n }}</span>
|
||
{% elif n >= proj_page_obj.number|add:"-2" and n <= proj_page_obj.number|add:"2" %}
|
||
<a href="?proj_page={{ n }}&proj_per_page={{ proj_per_page }}{% if project_search %}&project_search={{ project_search }}{% endif %}#projects"
|
||
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 proj_page_obj.has_next %}
|
||
<a href="?proj_page={{ proj_page_obj.next_page_number }}&proj_per_page={{ proj_per_page }}{% if project_search %}&project_search={{ project_search }}{% endif %}#projects"
|
||
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>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Create Project Attachment Modal -->
|
||
<div x-cloak x-show="createMemberOpen" class="fixed inset-0 z-50 flex items-center justify-center">
|
||
<div class="fixed inset-0 bg-black/50" @click="createMemberOpen = 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-md mx-4 sm:mx-auto my-auto z-10 overflow-visible">
|
||
<!-- Header -->
|
||
<div class="px-6 py-5 flex items-center justify-between">
|
||
<h3 class="text-base font-bold text-gray-900 dark:text-white">Attach Project</h3>
|
||
<button type="button" @click="createMemberOpen = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Body & Form -->
|
||
<form method="post" action="{% url 'user-project-attach' %}">
|
||
{% csrf_token %}
|
||
<input type="hidden" name="user_id" value="{{ user_obj.id }}">
|
||
|
||
<div class="px-6 pb-4">
|
||
<div class="relative w-full">
|
||
<input type="hidden" name="project_id" x-model="createMemberData.project_id">
|
||
|
||
<button type="button" @click="open = !open" @click.away="open = false" class="w-full flex items-center justify-between px-3 py-2.5 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 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||
<span x-text="createMemberData.project_id ? options.find(o => o.id == createMemberData.project_id)?.name || 'Select an option' : 'Select an option'" :class="!createMemberData.project_id && 'text-gray-400'"></span>
|
||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
||
</button>
|
||
|
||
<!-- Dropdown Menu -->
|
||
<div x-cloak x-show="open" class="absolute left-0 right-0 top-full mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50 overflow-hidden">
|
||
<div class="p-2 border-b border-gray-100 dark:border-gray-700">
|
||
<div class="relative">
|
||
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-xs"></i>
|
||
<input type="text" x-model="search" placeholder="Search projects..." class="w-full pl-8 pr-3 py-1.5 text-sm border border-gray-200 dark:border-gray-600 rounded bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-1 focus:ring-blue-500 text-gray-900 dark:text-white" @click.stop>
|
||
</div>
|
||
</div>
|
||
<ul class="max-h-48 overflow-y-auto py-1">
|
||
<template x-for="option in filteredOptions" :key="option.id">
|
||
<li @click="createMemberData.project_id = option.id; open = false; search = ''" class="px-3 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-blue-50 dark:hover:bg-blue-900/30 cursor-pointer" x-text="option.name"></li>
|
||
</template>
|
||
<li x-show="filteredOptions.length === 0" class="px-3 py-4 text-sm text-gray-500 text-center">No projects found</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Footer -->
|
||
<div class="px-6 py-4 border-t border-gray-100 dark:border-gray-700 flex items-center justify-start gap-2">
|
||
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors">Attach</button>
|
||
{% if available_projects.exists %}
|
||
<button type="submit" name="create_another" value="1" class="px-4 py-2 text-sm font-medium text-gray-700 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">Attach & attach another</button>
|
||
{% endif %}
|
||
<button type="button" @click="createMemberOpen = false" class="px-4 py-2 text-sm font-medium text-gray-700 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">Cancel</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
</div>
|
||
{% endblock %} |