153 lines
4.7 KiB
Vue
153 lines
4.7 KiB
Vue
<script setup lang="ts">
|
|
import Button from '@/components/ui/button/Button.vue';
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardFooter,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@/components/ui/card'
|
|
import { onMounted, ref, computed, reactive, watch } from 'vue';
|
|
import { addMemberToRole, createRole, deleteRole, getRoles, removeMemberFromRole } from '@/api/roles';
|
|
import Badge from '@/components/ui/badge/Badge.vue';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog'
|
|
|
|
import {
|
|
Combobox,
|
|
ComboboxAnchor,
|
|
ComboboxEmpty,
|
|
ComboboxGroup,
|
|
ComboboxInput,
|
|
ComboboxItem,
|
|
ComboboxItemIndicator,
|
|
ComboboxList,
|
|
} from '@/components/ui/combobox'
|
|
import { Plus, X } from 'lucide-vue-next';
|
|
import Separator from '@/components/ui/separator/Separator.vue';
|
|
import Input from '@/components/ui/input/Input.vue';
|
|
import Label from '@/components/ui/label/Label.vue';
|
|
import { getAllLightMembers, getMembers } from '@/api/member';
|
|
import { Member, MemberLight } from '@shared/types/member';
|
|
import { Role } from '@shared/types/roles';
|
|
import RoleView from '@/components/roles/roleView.vue';
|
|
import { useRoute } from 'vue-router';
|
|
|
|
const roles = ref<Role[]>([])
|
|
const activeRole = ref<Role | null>(null)
|
|
const showDialog = ref(false);
|
|
const showCreateGroupDialog = ref(false);
|
|
const addingMember = ref(false);
|
|
const memberToAdd = ref<Member | null>(null);
|
|
const route = useRoute();
|
|
|
|
const allMembers = ref<MemberLight[]>([])
|
|
|
|
type RoleDraft = {
|
|
name: string
|
|
description: string
|
|
color: string // whatever you store, e.g. a hex string or a semantic tag
|
|
}
|
|
|
|
const roleDraft = reactive<RoleDraft>({
|
|
name: "",
|
|
description: "",
|
|
color: "", // e.g. "#FF8A00" or "orange"
|
|
})
|
|
|
|
const draftErrors = reactive<{ name?: string; color?: string }>({})
|
|
const creating = ref(false)
|
|
|
|
function resetRoleDraft() {
|
|
roleDraft.name = ""
|
|
roleDraft.description = ""
|
|
roleDraft.color = ""
|
|
draftErrors.name = undefined
|
|
draftErrors.color = undefined
|
|
}
|
|
|
|
watch(showCreateGroupDialog, (open) => {
|
|
if (!open) resetRoleDraft()
|
|
})
|
|
|
|
|
|
function validateRoleDraft(): boolean {
|
|
draftErrors.name = !roleDraft.name.trim() ? "Group name is required" : undefined
|
|
// If color is required or must be hex, validate it here:
|
|
if (!roleDraft.color.trim()) {
|
|
draftErrors.color = "Color is required"
|
|
} else if (!/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(roleDraft.color)) {
|
|
draftErrors.color = "Use a valid hex color like #FF8A00"
|
|
} else {
|
|
draftErrors.color = undefined
|
|
}
|
|
|
|
return !draftErrors.name && !draftErrors.color
|
|
}
|
|
|
|
async function handleCreateGroup() {
|
|
if (!validateRoleDraft()) return
|
|
|
|
try {
|
|
creating.value = true
|
|
|
|
// If your createRole API already accepts a payload:
|
|
await createRole(roleDraft.name, roleDraft.color, roleDraft.description)
|
|
|
|
// Refresh list, close, reset
|
|
roles.value = await getRoles()
|
|
showCreateGroupDialog.value = false
|
|
resetRoleDraft()
|
|
} catch (err) {
|
|
console.error("Failed to create role:", err)
|
|
// optionally surface a toast/error UI here
|
|
} finally {
|
|
creating.value = false
|
|
}
|
|
}
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
roles.value = await getRoles();
|
|
allMembers.value = await getAllLightMembers();
|
|
})
|
|
</script>
|
|
<template>
|
|
<div class="max-w-7xl mx-auto w-full my-[0.2dvh] h-[0.8dvh]">
|
|
<div class="flex items-center justify-between">
|
|
<p class="scroll-m-20 text-2xl font-semibold tracking-tight my-5">Role Management</p>
|
|
</div>
|
|
<div class="grid grid-cols-[minmax(400px,280px)_1fr] gap-5">
|
|
<div class="w-full pr-5 border-r">
|
|
<div class="space-y-1">
|
|
<p class="px-3 py-2 text-xs font-medium text-muted-foreground uppercase">
|
|
Roles
|
|
</p>
|
|
|
|
<button v-for="r in roles" :key="r.id" @click="$router.push(`/administration/roles/${r.id}`)"
|
|
:class="[
|
|
'flex w-full items-center gap-3 rounded-md px-3 py-2 transition-colors cursor-pointer',
|
|
Number(route.params.id) == r.id
|
|
? 'bg-muted font-medium'
|
|
: 'hover:bg-muted/50'
|
|
]">
|
|
<span class="h-2 w-2 rounded-full shrink-0" :style="{ backgroundColor: r.color }" />
|
|
<span class="truncate">{{ r.name }}</span>
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
<div>
|
|
<RoleView :all-members="allMembers"></RoleView>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template> |