Added the support for generating docs from template doc
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
<script setup>
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import { useForm, Link } from "@inertiajs/vue3";
|
||||
import { ref, computed } from "vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faMagnifyingGlass, faFloppyDisk } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
const props = defineProps({
|
||||
users: Array,
|
||||
roles: Array,
|
||||
permissions: Array,
|
||||
});
|
||||
|
||||
const query = ref("");
|
||||
const roleFilter = ref(null);
|
||||
|
||||
const forms = Object.fromEntries(
|
||||
props.users.map((u) => [
|
||||
u.id,
|
||||
useForm({ roles: u.roles.map((r) => r.id), dirty: false }),
|
||||
])
|
||||
);
|
||||
|
||||
function toggle(userId, roleId) {
|
||||
const form = forms[userId];
|
||||
const exists = form.roles.includes(roleId);
|
||||
form.roles = exists
|
||||
? form.roles.filter((id) => id !== roleId)
|
||||
: [...form.roles, roleId];
|
||||
form.dirty = true;
|
||||
}
|
||||
|
||||
function submit(userId) {
|
||||
const form = forms[userId];
|
||||
form.put(route("admin.users.update", { user: userId }), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
form.dirty = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function submitAll() {
|
||||
// sequential save of only dirty forms
|
||||
Object.entries(forms).forEach(([id, f]) => {
|
||||
if (f.dirty) {
|
||||
f.put(route("admin.users.update", { user: id }), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
f.dirty = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const filteredUsers = computed(() => {
|
||||
return props.users.filter((u) => {
|
||||
const q = query.value.toLowerCase().trim();
|
||||
const matchesQuery =
|
||||
!q || u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q);
|
||||
const matchesRole = !roleFilter.value || forms[u.id].roles.includes(roleFilter.value);
|
||||
return matchesQuery && matchesRole;
|
||||
});
|
||||
});
|
||||
|
||||
const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminLayout title="Upravljanje vlog uporabnikov">
|
||||
<div class="max-w-7xl mx-auto space-y-8">
|
||||
<div class="bg-white border rounded-xl shadow-sm p-6 space-y-7">
|
||||
<header class="space-y-1">
|
||||
<h1 class="text-xl font-semibold leading-tight tracking-tight">
|
||||
Uporabniki & Vloge
|
||||
</h1>
|
||||
<p class="text-sm text-gray-500">
|
||||
Dodeli ali odstrani vloge. Uporabi iskanje ali filter po vlogah za hitrejše
|
||||
upravljanje.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div
|
||||
class="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between"
|
||||
>
|
||||
<div class="flex flex-wrap gap-3 items-center">
|
||||
<div class="relative">
|
||||
<span class="absolute left-2 top-1.5 text-gray-400">
|
||||
<FontAwesomeIcon :icon="faMagnifyingGlass" class="w-4 h-4" />
|
||||
</span>
|
||||
<input
|
||||
v-model="query"
|
||||
type="text"
|
||||
placeholder="Išči uporabnika..."
|
||||
class="pl-8 pr-3 py-1.5 text-sm rounded-md border border-gray-300 focus:ring-indigo-500 focus:border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@click="roleFilter = null"
|
||||
:class="[
|
||||
'px-2.5 py-1 rounded-full text-xs border transition',
|
||||
roleFilter === null
|
||||
? 'bg-indigo-600 text-white border-indigo-600'
|
||||
: 'bg-white text-gray-600 border-gray-300 hover:bg-gray-50',
|
||||
]"
|
||||
>
|
||||
Vse
|
||||
</button>
|
||||
<button
|
||||
v-for="r in props.roles"
|
||||
:key="'rf-' + r.id"
|
||||
type="button"
|
||||
@click="roleFilter = r.id"
|
||||
:class="[
|
||||
'px-2.5 py-1 rounded-full text-xs border transition',
|
||||
roleFilter === r.id
|
||||
? 'bg-indigo-600 text-white border-indigo-600'
|
||||
: 'bg-white text-gray-600 border-gray-300 hover:bg-gray-50',
|
||||
]"
|
||||
>
|
||||
{{ r.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="submitAll"
|
||||
:disabled="!anyDirty"
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-medium border disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
:class="
|
||||
anyDirty
|
||||
? 'bg-indigo-600 border-indigo-600 text-white hover:bg-indigo-500'
|
||||
: 'bg-white border-gray-300 text-gray-400'
|
||||
"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faFloppyDisk" class="w-4 h-4" />
|
||||
Shrani vse
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto rounded-lg border border-slate-200">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-slate-50 text-slate-600 sticky top-0 z-10">
|
||||
<tr>
|
||||
<th class="p-2 text-left font-medium text-[11px] uppercase tracking-wide">
|
||||
Uporabnik
|
||||
</th>
|
||||
<th
|
||||
v-for="role in props.roles"
|
||||
:key="role.id"
|
||||
class="p-2 font-medium text-[11px] uppercase tracking-wide text-center"
|
||||
>
|
||||
{{ role.name }}
|
||||
</th>
|
||||
<th
|
||||
class="p-2 font-medium text-[11px] uppercase tracking-wide text-center"
|
||||
>
|
||||
Akcije
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(user, idx) in filteredUsers"
|
||||
:key="user.id"
|
||||
:class="[
|
||||
'border-t border-slate-100',
|
||||
idx % 2 === 1 ? 'bg-slate-50/40' : 'bg-white',
|
||||
]"
|
||||
>
|
||||
<td class="p-2 whitespace-nowrap align-top">
|
||||
<div class="font-medium text-sm flex items-center gap-2">
|
||||
<span
|
||||
class="inline-flex items-center justify-center h-7 w-7 rounded-full bg-indigo-50 text-indigo-600 text-xs font-semibold"
|
||||
>{{ user.name.substring(0, 2).toUpperCase() }}</span
|
||||
>
|
||||
<span>{{ user.name }}</span>
|
||||
<span
|
||||
v-if="forms[user.id].dirty"
|
||||
class="ml-1 inline-block px-1.5 py-0.5 rounded bg-amber-100 text-amber-700 text-[10px] font-medium"
|
||||
>Spremembe</span
|
||||
>
|
||||
</div>
|
||||
<div class="text-[11px] text-slate-500 mt-0.5 font-mono">
|
||||
{{ user.email }}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
v-for="role in props.roles"
|
||||
:key="role.id"
|
||||
class="p-2 text-center align-top"
|
||||
>
|
||||
<label class="inline-flex items-center gap-1 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded-md border-2 border-slate-400 bg-white text-indigo-600 accent-indigo-600 hover:border-slate-500 focus:ring-indigo-500 focus:ring-offset-0 focus:outline-none transition"
|
||||
:checked="forms[user.id].roles.includes(role.id)"
|
||||
@change="toggle(user.id, role.id)"
|
||||
/>
|
||||
</label>
|
||||
</td>
|
||||
<td class="p-2 text-center align-top">
|
||||
<button
|
||||
@click="submit(user.id)"
|
||||
:disabled="forms[user.id].processing || !forms[user.id].dirty"
|
||||
class="inline-flex items-center px-3 py-1.5 text-xs font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
:class="
|
||||
forms[user.id].dirty
|
||||
? 'bg-indigo-600 text-white hover:bg-indigo-500'
|
||||
: 'bg-gray-100 text-gray-400'
|
||||
"
|
||||
>
|
||||
<span v-if="forms[user.id].processing">...</span>
|
||||
<span v-else>Shrani</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!filteredUsers.length">
|
||||
<td
|
||||
:colspan="props.roles.length + 2"
|
||||
class="p-6 text-center text-sm text-gray-500"
|
||||
>
|
||||
Ni rezultatov
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2
|
||||
class="text-[11px] font-semibold tracking-wide uppercase text-slate-500 mb-3"
|
||||
>
|
||||
Referenca vlog in dovoljenj
|
||||
</h2>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<div
|
||||
v-for="role in props.roles"
|
||||
:key="'ref-' + role.id"
|
||||
class="px-3 py-2 rounded-lg border border-slate-200 bg-white shadow-sm"
|
||||
>
|
||||
<div class="font-medium text-sm flex items-center gap-2">
|
||||
<span
|
||||
class="inline-flex items-center justify-center h-6 w-6 rounded-md bg-indigo-50 text-indigo-600 text-[11px] font-semibold"
|
||||
>{{ role.name.substring(0, 1).toUpperCase() }}</span
|
||||
>
|
||||
{{ role.name }}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1 mt-2">
|
||||
<span
|
||||
v-for="perm in role.permissions"
|
||||
:key="perm.id"
|
||||
class="text-[10px] uppercase tracking-wide bg-slate-100 text-slate-600 px-1.5 py-0.5 rounded"
|
||||
>{{ perm.slug }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
Reference in New Issue
Block a user