mirror of
https://github.com/wizarrrr/wizarr.git
synced 2026-07-31 23:37:07 -04:00
271 lines
18 KiB
HTML
271 lines
18 KiB
HTML
<div class="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all w-full max-w-2xl dark:bg-gray-800"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="modal-title"
|
||
x-data="{ activeTab: 'tab-{{ related_users[0].id }}' }">
|
||
<div class="bg-white dark:bg-gray-800">
|
||
<!-- Header -->
|
||
<div class="flex justify-between items-center px-4 pt-5 sm:px-6 sm:pt-6 pb-3">
|
||
<h2 id="modal-title"
|
||
class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
|
||
{{ _("Edit User") }} – {{ user.username or user.email }}
|
||
</h2>
|
||
<button type="button"
|
||
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||
onclick="document.getElementById('modal').classList.add('hidden')"
|
||
aria-label="{{ _('Close modal') }}">
|
||
<svg class="w-3 h-3"
|
||
aria-hidden="true"
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
fill="none"
|
||
viewBox="0 0 14 14">
|
||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
||
</svg>
|
||
<span class="sr-only">{{ _('Close modal') }}</span>
|
||
</button>
|
||
</div>
|
||
<!-- Tab Navigation -->
|
||
<div class="border-b border-gray-200 dark:border-gray-700 px-4 sm:px-6">
|
||
<nav class="flex -mb-px space-x-2 overflow-x-auto"
|
||
aria-label="{{ _('Tabs') }}"
|
||
role="tablist">
|
||
{% for related_user in related_users %}
|
||
<button type="button"
|
||
role="tab"
|
||
:aria-selected="activeTab === 'tab-{{ related_user.id }}'"
|
||
@click="activeTab = 'tab-{{ related_user.id }}'"
|
||
:class="activeTab === 'tab-{{ related_user.id }}' ? 'border-primary text-primary dark:border-primary dark:text-primary' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'"
|
||
class="whitespace-nowrap py-3 px-4 border-b-2 font-medium text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition-colors">
|
||
{% if related_user.server %}
|
||
{{ related_user.server.name }}
|
||
{% else %}
|
||
{{ _("Local") }}
|
||
{% endif %}
|
||
</button>
|
||
{% endfor %}
|
||
<button type="button"
|
||
role="tab"
|
||
:aria-selected="activeTab === 'tab-notes'"
|
||
@click="activeTab = 'tab-notes'"
|
||
:class="activeTab === 'tab-notes' ? 'border-primary text-primary dark:border-primary dark:text-primary' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'"
|
||
class="whitespace-nowrap py-3 px-4 border-b-2 font-medium text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition-colors">
|
||
{{ _("Notes") }}
|
||
</button>
|
||
</nav>
|
||
</div>
|
||
<!-- Tab Content -->
|
||
<div class="px-4 pb-4 pt-4 sm:p-6 sm:pt-4">
|
||
{% for related_user in related_users %}
|
||
<div x-show="activeTab === 'tab-{{ related_user.id }}'"
|
||
x-transition:enter="transition ease-out duration-200"
|
||
x-transition:enter-start="opacity-0 transform translate-y-2"
|
||
x-transition:enter-end="opacity-100 transform translate-y-0"
|
||
role="tabpanel"
|
||
:aria-hidden="activeTab !== 'tab-{{ related_user.id }}'">
|
||
<!-- Server Badge (for multi-server setups) -->
|
||
{% if related_users|length > 1 %}
|
||
<div class="flex items-center justify-between mb-4 pb-3 border-b border-gray-200 dark:border-gray-700">
|
||
<div class="flex items-center">
|
||
{% if related_user.server %}
|
||
{{ related_user.server.server_type|server_name_tag(related_user.server.name) }}
|
||
{% else %}
|
||
{{ 'local'|server_type_tag }}
|
||
{% endif %}
|
||
</div>
|
||
<span class="text-xs text-gray-500 dark:text-gray-400">ID: {{ related_user.id }}</span>
|
||
</div>
|
||
{% endif %}
|
||
<!-- Permission Management Section (only for supported servers) -->
|
||
{% if related_user.server and related_user.server.server_type in ['plex', 'jellyfin', 'emby', 'audiobookshelf'] %}
|
||
<div class="mb-6">
|
||
<h3 class="text-base font-semibold text-gray-900 dark:text-white mb-4">{{ _("Permission Management") }}</h3>
|
||
<!-- Permission Toggles -->
|
||
<div class="space-y-3 bg-gray-50 dark:bg-gray-900 rounded-lg p-4"
|
||
x-data="{ downloading: {{ (related_user.allow_downloads if related_user.allow_downloads is not none else false)|tojson }}, liveTv: {{ (related_user.allow_live_tv if related_user.allow_live_tv is not none else false)|tojson }}, cameraUpload: {{ (related_user.allow_camera_upload if related_user.allow_camera_upload is not none else false)|tojson }}, updating: { downloads: false, liveTv: false, camera: false }, togglePermission(type, formRef) { const originalValue = this[type]; this.updating[type === 'downloading' ? 'downloads' : (type === 'liveTv' ? 'liveTv' : 'camera')] = true; this[type] = !this[type]; this.$nextTick(() => htmx.trigger(formRef, 'submit')); }, showError(message) { alert('Error: ' + message); } }">
|
||
<!-- Downloads Permission (All servers support this) -->
|
||
<div class="flex items-center justify-between">
|
||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ _("Allow Downloads") }}</label>
|
||
<button type="button"
|
||
@click="togglePermission('downloading', $refs.downloadForm)"
|
||
:disabled="updating.downloads"
|
||
:class="[ downloading ? 'bg-primary' : 'bg-gray-200 dark:bg-gray-700', updating.downloads ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer' ]"
|
||
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-all duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 dark:focus:ring-offset-gray-900"
|
||
aria-label="{{ _('Toggle downloads permission') }}">
|
||
<span :class="downloading ? 'translate-x-5' : 'translate-x-0'"
|
||
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
|
||
</button>
|
||
<form x-ref="downloadForm"
|
||
method="post"
|
||
action="{{ url_for('admin.update_user_permissions', db_id=related_user.id) }}"
|
||
hx-post="{{ url_for('admin.update_user_permissions', db_id=related_user.id) }}"
|
||
hx-swap="none"
|
||
@htmx:after-request="updating.downloads = false"
|
||
@htmx:response-error="downloading = !downloading; updating.downloads = false; showError($event.detail.xhr.responseText || 'Failed to update permission')"
|
||
class="hidden">
|
||
<input type="hidden" name="permission_type" value="allow_downloads">
|
||
<input type="hidden" name="enabled" :value="downloading ? 'true' : 'false'">
|
||
</form>
|
||
</div>
|
||
<!-- Live TV Permission (Plex, Jellyfin, Emby only) -->
|
||
{% if related_user.server and related_user.server.server_type in ['plex', 'jellyfin', 'emby'] %}
|
||
<div class="flex items-center justify-between">
|
||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ _("Allow Live TV") }}</label>
|
||
<button type="button"
|
||
@click="togglePermission('liveTv', $refs.liveTvForm)"
|
||
:disabled="updating.liveTv"
|
||
:class="[ liveTv ? 'bg-primary' : 'bg-gray-200 dark:bg-gray-700', updating.liveTv ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer' ]"
|
||
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-all duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 dark:focus:ring-offset-gray-900"
|
||
aria-label="{{ _('Toggle live TV permission') }}">
|
||
<span :class="liveTv ? 'translate-x-5' : 'translate-x-0'"
|
||
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
|
||
</button>
|
||
<form x-ref="liveTvForm"
|
||
method="post"
|
||
action="{{ url_for('admin.update_user_permissions', db_id=related_user.id) }}"
|
||
hx-post="{{ url_for('admin.update_user_permissions', db_id=related_user.id) }}"
|
||
hx-swap="none"
|
||
@htmx:after-request="updating.liveTv = false"
|
||
@htmx:response-error="liveTv = !liveTv; updating.liveTv = false; showError($event.detail.xhr.responseText || 'Failed to update permission')"
|
||
class="hidden">
|
||
<input type="hidden" name="permission_type" value="allow_live_tv">
|
||
<input type="hidden" name="enabled" :value="liveTv ? 'true' : 'false'">
|
||
</form>
|
||
</div>
|
||
{% endif %}
|
||
<!-- Camera Upload Permission (Plex only) -->
|
||
{% if related_user.server and related_user.server.server_type == 'plex' %}
|
||
<div class="flex items-center justify-between">
|
||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ _("Allow Camera Upload") }}</label>
|
||
<button type="button"
|
||
@click="togglePermission('cameraUpload', $refs.cameraForm)"
|
||
:disabled="updating.camera"
|
||
:class="[ cameraUpload ? 'bg-primary' : 'bg-gray-200 dark:bg-gray-700', updating.camera ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer' ]"
|
||
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-all duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 dark:focus:ring-offset-gray-900"
|
||
aria-label="{{ _('Toggle camera upload permission') }}">
|
||
<span :class="cameraUpload ? 'translate-x-5' : 'translate-x-0'"
|
||
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
|
||
</button>
|
||
<form x-ref="cameraForm"
|
||
method="post"
|
||
action="{{ url_for('admin.update_user_permissions', db_id=related_user.id) }}"
|
||
hx-post="{{ url_for('admin.update_user_permissions', db_id=related_user.id) }}"
|
||
hx-swap="none"
|
||
@htmx:after-request="updating.camera = false"
|
||
@htmx:response-error="cameraUpload = !cameraUpload; updating.camera = false; showError($event.detail.xhr.responseText || 'Failed to update permission')"
|
||
class="hidden">
|
||
<input type="hidden" name="permission_type" value="allow_camera_upload">
|
||
<input type="hidden" name="enabled" :value="cameraUpload ? 'true' : 'false'">
|
||
</form>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
<!-- Library Access Section -->
|
||
{% set lib_data = user_libraries_map.get(related_user.id, {}) %}
|
||
{% set libraries = lib_data.get('libraries', []) %}
|
||
{% set accessible = lib_data.get('accessible_libraries', []) %}
|
||
<div class="mb-6">
|
||
<h3 class="text-base font-semibold text-gray-900 dark:text-white mb-4">{{ _("Library Access") }}</h3>
|
||
{% if libraries|length == 0 %}
|
||
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 text-sm text-gray-500 dark:text-gray-400">
|
||
<p>{{ _("No libraries available") }}</p>
|
||
<p class="text-xs mt-1">{{ _("Scan libraries from the invitation page to enable library restrictions") }}</p>
|
||
</div>
|
||
{% else %}
|
||
<div class="relative" x-data="{ updating: false }">
|
||
<form id="library-form-{{ related_user.id }}"
|
||
@htmx:before-request="updating = true"
|
||
@htmx:after-request="updating = false"
|
||
:class="updating ? 'opacity-60 pointer-events-none' : ''"
|
||
class="transition-opacity duration-200">
|
||
<ul class="h-48 px-3 py-2 overflow-y-auto text-sm text-gray-700 dark:text-gray-200 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-900">
|
||
{% for library in libraries %}
|
||
<li>
|
||
<label class="flex items-center ps-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer py-2"
|
||
:class="updating ? 'cursor-not-allowed' : 'cursor-pointer'">
|
||
<input type="checkbox"
|
||
name="library_ids[]"
|
||
value="{{ library.id }}"
|
||
{% if accessible|length == 0 or library.name in accessible %}checked{% endif %}
|
||
hx-post="{{ url_for('admin.update_user_libraries', db_id=related_user.id) }}"
|
||
hx-trigger="change"
|
||
hx-include="#library-form-{{ related_user.id }}"
|
||
hx-swap="none"
|
||
:disabled="updating"
|
||
class="w-4 h-4 text-primary bg-gray-100 border-gray-300 rounded focus:ring-primary dark:focus:ring-primary dark:ring-offset-gray-900 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||
<span class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-300">{{ library.name }}</span>
|
||
</label>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</form>
|
||
</div>
|
||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||
{{ _("Click to toggle library access. Updates save automatically.") }}
|
||
</p>
|
||
{% endif %}
|
||
</div>
|
||
<!-- Expiry Management Section -->
|
||
<div>
|
||
<h3 class="text-base font-semibold text-gray-900 dark:text-white mb-4">{{ _("Expiry Management") }}</h3>
|
||
<form hx-post="{{ url_for('admin.user_detail', db_id=related_user.id) }}"
|
||
class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
|
||
<div class="flex items-end gap-3">
|
||
<div class="flex-1">
|
||
<label for="expires_{{ related_user.id }}"
|
||
class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||
{{ _("Expiry date") }}
|
||
</label>
|
||
<input id="expires_{{ related_user.id }}"
|
||
name="expires"
|
||
type="datetime-local"
|
||
value="{{ related_user.expires|default('', true) and related_user.expires.strftime('%Y-%m-%dT%H:%M') or '' }}"
|
||
class="bg-white border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-2.5 dark:bg-gray-800 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" />
|
||
</div>
|
||
<button type="submit"
|
||
class="inline-flex items-center px-4 py-2.5 text-sm font-medium text-white bg-primary rounded-lg hover:bg-primary_hover focus:ring-4 focus:outline-none focus:ring-primary_focus dark:bg-primary dark:hover:bg-primary_hover dark:focus:ring-primary_focus">
|
||
{{ _("Update") }}
|
||
</button>
|
||
</div>
|
||
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
|
||
{% if related_user.expires %}
|
||
{{ _("Current expiry") }}: {{ related_user.expires|human_date }}
|
||
{% else %}
|
||
{{ _("No expiry set") }}
|
||
{% endif %}
|
||
</p>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
<!-- Notes Tab Content -->
|
||
<div x-show="activeTab === 'tab-notes'"
|
||
x-transition:enter="transition ease-out duration-200"
|
||
x-transition:enter-start="opacity-0 transform translate-y-2"
|
||
x-transition:enter-end="opacity-100 transform translate-y-0"
|
||
role="tabpanel"
|
||
:aria-hidden="activeTab !== 'tab-notes'">
|
||
<h3 class="text-base font-semibold text-gray-900 dark:text-white mb-4">{{ _("Notes") }}</h3>
|
||
<form hx-post="{{ url_for('admin.user_detail', db_id=user.id) }}"
|
||
class="space-y-4">
|
||
<div>
|
||
<label for="notes" class="sr-only">{{ _("User notes") }}</label>
|
||
<textarea id="notes"
|
||
name="notes"
|
||
rows="6"
|
||
placeholder="{{ _('Add notes about this user...') }}"
|
||
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-3 dark:bg-gray-900 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">{{ user.notes|default('', true) }}</textarea>
|
||
</div>
|
||
<div class="flex justify-end">
|
||
<button type="submit"
|
||
class="inline-flex items-center px-4 py-2.5 text-sm font-medium text-white bg-primary rounded-lg hover:bg-primary_hover focus:ring-4 focus:outline-none focus:ring-primary_focus dark:bg-primary dark:hover:bg-primary_hover dark:focus:ring-primary_focus">
|
||
{{ _("Update Notes") }}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|