445 lines
14 KiB
Vue
445 lines
14 KiB
Vue
<script setup>
|
|
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
|
import { useForm, Link, router } from "@inertiajs/vue3";
|
|
import { ref, computed } from "vue";
|
|
import { SearchIcon, SaveIcon, UserPlusIcon } from "lucide-vue-next";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/Components/ui/card";
|
|
import { Button } from "@/Components/ui/button";
|
|
import { Input } from "@/Components/ui/input";
|
|
import { Label } from "@/Components/ui/label";
|
|
import { Badge } from "@/Components/ui/badge";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/Components/ui/table";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/Components/ui/dialog";
|
|
import { Checkbox } from "@/Components/ui/checkbox";
|
|
|
|
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;
|
|
console.log("Toggle checkbox");
|
|
}
|
|
|
|
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));
|
|
|
|
// Create user modal
|
|
const showCreateModal = ref(false);
|
|
const createForm = useForm({
|
|
name: "",
|
|
email: "",
|
|
password: "",
|
|
password_confirmation: "",
|
|
roles: [],
|
|
});
|
|
|
|
function openCreateModal() {
|
|
createForm.reset();
|
|
createForm.clearErrors();
|
|
showCreateModal.value = true;
|
|
}
|
|
|
|
function closeCreateModal() {
|
|
showCreateModal.value = false;
|
|
createForm.reset();
|
|
}
|
|
|
|
function submitCreateUser() {
|
|
createForm.post(route("admin.users.store"), {
|
|
preserveScroll: true,
|
|
onSuccess: () => {
|
|
closeCreateModal();
|
|
},
|
|
});
|
|
}
|
|
|
|
function toggleCreateRole(roleId) {
|
|
const exists = createForm.roles.includes(roleId);
|
|
createForm.roles = exists
|
|
? createForm.roles.filter((id) => id !== roleId)
|
|
: [...createForm.roles, roleId];
|
|
}
|
|
|
|
function toggleUserActive(userId) {
|
|
router.patch(
|
|
route("admin.users.toggle-active", { user: userId }),
|
|
{},
|
|
{
|
|
preserveScroll: true,
|
|
}
|
|
);
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<AdminLayout title="Upravljanje vlog uporabnikov">
|
|
<div class="max-w-7xl mx-auto space-y-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Uporabniki & Vloge</CardTitle>
|
|
<CardDescription>
|
|
Dodeli ali odstrani vloge. Uporabi iskanje ali filter po vlogah za hitrejše
|
|
upravljanje.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent class="space-y-6">
|
|
<!-- 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">
|
|
<SearchIcon
|
|
class="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground"
|
|
/>
|
|
<Input
|
|
v-model="query"
|
|
type="text"
|
|
placeholder="Išči uporabnika..."
|
|
class="pl-9 w-64"
|
|
/>
|
|
</div>
|
|
<div class="flex flex-wrap gap-2">
|
|
<Button
|
|
type="button"
|
|
@click="roleFilter = null"
|
|
:variant="roleFilter === null ? 'default' : 'outline'"
|
|
size="sm"
|
|
class="rounded-full"
|
|
>
|
|
Vse
|
|
</Button>
|
|
<Button
|
|
v-for="r in props.roles"
|
|
:key="'rf-' + r.id"
|
|
type="button"
|
|
@click="roleFilter = r.id"
|
|
:variant="roleFilter === r.id ? 'default' : 'outline'"
|
|
size="sm"
|
|
class="rounded-full"
|
|
>
|
|
{{ r.name }}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<Button type="button" @click="openCreateModal" size="sm">
|
|
<UserPlusIcon class="h-4 w-4 mr-2" />
|
|
Ustvari uporabnika
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
@click="submitAll"
|
|
:disabled="!anyDirty"
|
|
size="sm"
|
|
:variant="anyDirty ? 'default' : 'outline'"
|
|
>
|
|
<SaveIcon class="h-4 w-4 mr-2" />
|
|
Shrani vse
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="rounded-lg border">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead class="text-left">Uporabnik</TableHead>
|
|
<TableHead class="text-center">Status</TableHead>
|
|
<TableHead
|
|
v-for="role in props.roles"
|
|
:key="role.id"
|
|
class="text-center"
|
|
>
|
|
{{ role.name }}
|
|
</TableHead>
|
|
<TableHead class="text-center">Akcije</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
<TableRow
|
|
v-for="user in filteredUsers"
|
|
:key="user.id"
|
|
:class="!user.active && 'opacity-60'"
|
|
>
|
|
<TableCell class="whitespace-nowrap">
|
|
<div class="flex items-center gap-2">
|
|
<div
|
|
class="inline-flex items-center justify-center h-8 w-8 rounded-full bg-primary/10 text-primary text-xs font-semibold"
|
|
>
|
|
{{ user.name.substring(0, 2).toUpperCase() }}
|
|
</div>
|
|
<div>
|
|
<div class="font-medium text-sm flex items-center gap-2">
|
|
{{ user.name }}
|
|
<Badge
|
|
v-if="forms[user.id].dirty"
|
|
variant="secondary"
|
|
class="text-xs"
|
|
>
|
|
Spremembe
|
|
</Badge>
|
|
</div>
|
|
<div class="text-xs text-muted-foreground font-mono">
|
|
{{ user.email }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell class="text-center">
|
|
<Badge
|
|
@click="toggleUserActive(user.id)"
|
|
:variant="user.active ? 'default' : 'secondary'"
|
|
class="cursor-pointer"
|
|
>
|
|
{{ user.active ? "Aktiven" : "Neaktiven" }}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell
|
|
v-for="role in props.roles"
|
|
:key="role.id"
|
|
class="text-center"
|
|
>
|
|
<div class="flex items-center justify-center">
|
|
<Checkbox
|
|
:default-value="forms[user.id].roles.includes(role.id)"
|
|
@update:model-value="toggle(user.id, role.id)"
|
|
/>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell class="text-center">
|
|
<Button
|
|
@click="submit(user.id)"
|
|
:disabled="forms[user.id].processing || !forms[user.id].dirty"
|
|
size="sm"
|
|
:variant="forms[user.id].dirty ? 'default' : 'ghost'"
|
|
>
|
|
<span v-if="forms[user.id].processing">...</span>
|
|
<span v-else>Shrani</span>
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
<TableRow v-if="!filteredUsers.length">
|
|
<TableCell
|
|
:colspan="props.roles.length + 3"
|
|
class="text-center text-sm text-muted-foreground py-8"
|
|
>
|
|
Ni rezultatov
|
|
</TableCell>
|
|
</TableRow>
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle class="text-base">Referenca vlog in dovoljenj</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div class="flex flex-wrap gap-3">
|
|
<Card
|
|
v-for="role in props.roles"
|
|
:key="'ref-' + role.id"
|
|
class="border-muted"
|
|
>
|
|
<CardHeader class="pb-3">
|
|
<CardTitle class="text-sm flex items-center gap-2">
|
|
<div
|
|
class="inline-flex items-center justify-center h-6 w-6 rounded-md bg-primary/10 text-primary text-xs font-semibold"
|
|
>
|
|
{{ role.name.substring(0, 1).toUpperCase() }}
|
|
</div>
|
|
{{ role.name }}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div class="flex flex-wrap gap-1.5">
|
|
<Badge
|
|
v-for="perm in role.permissions"
|
|
:key="perm.id"
|
|
variant="secondary"
|
|
class="text-xs"
|
|
>
|
|
{{ perm.slug }}
|
|
</Badge>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<!-- Create User Modal -->
|
|
<Dialog :open="showCreateModal" @update:open="(val) => (showCreateModal = val)">
|
|
<DialogContent class="sm:max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle>Ustvari novega uporabnika</DialogTitle>
|
|
<DialogDescription>
|
|
Dodaj novega uporabnika s privzetimi vlogami
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div class="space-y-4 py-4">
|
|
<div class="space-y-2">
|
|
<Label for="name">Ime</Label>
|
|
<Input
|
|
id="name"
|
|
v-model="createForm.name"
|
|
type="text"
|
|
placeholder="Ime uporabnika"
|
|
/>
|
|
<p v-if="createForm.errors.name" class="text-sm text-destructive">
|
|
{{ createForm.errors.name }}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<Label for="email">E-pošta</Label>
|
|
<Input
|
|
id="email"
|
|
v-model="createForm.email"
|
|
type="email"
|
|
placeholder="uporabnik@example.com"
|
|
/>
|
|
<p v-if="createForm.errors.email" class="text-sm text-destructive">
|
|
{{ createForm.errors.email }}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<Label for="password">Geslo</Label>
|
|
<Input
|
|
id="password"
|
|
v-model="createForm.password"
|
|
type="password"
|
|
placeholder="********"
|
|
/>
|
|
<p v-if="createForm.errors.password" class="text-sm text-destructive">
|
|
{{ createForm.errors.password }}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<Label for="password_confirmation">Potrdi geslo</Label>
|
|
<Input
|
|
id="password_confirmation"
|
|
v-model="createForm.password_confirmation"
|
|
type="password"
|
|
placeholder="********"
|
|
/>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<Label>Vloge</Label>
|
|
<div class="flex flex-wrap gap-2">
|
|
<label
|
|
v-for="role in props.roles"
|
|
:key="'create-role-' + role.id"
|
|
class="flex items-center gap-2 px-3 py-2 rounded-md border cursor-pointer transition hover:bg-accent"
|
|
:class="
|
|
createForm.roles.includes(role.id) && 'bg-primary/10 border-primary'
|
|
"
|
|
>
|
|
<Checkbox
|
|
:default-value="createForm.roles.includes(role.id)"
|
|
@update:model-value="toggleCreateRole(role.id)"
|
|
/>
|
|
<span class="text-sm font-medium">{{ role.name }}</span>
|
|
</label>
|
|
</div>
|
|
<p v-if="createForm.errors.roles" class="text-sm text-destructive">
|
|
{{ createForm.errors.roles }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button type="button" variant="outline" @click="closeCreateModal">
|
|
Prekliči
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
@click="submitCreateUser"
|
|
:disabled="createForm.processing"
|
|
>
|
|
<span v-if="createForm.processing">Ustvarjanje...</span>
|
|
<span v-else>Ustvari</span>
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</AdminLayout>
|
|
</template>
|