Teren-app/resources/js/Pages/Admin/Users/Index.vue
2026-01-05 18:27:35 +01:00

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>