first pass of new UI
This commit is contained in:
@@ -5,7 +5,8 @@ const ur = express.Router();
|
|||||||
import { MemberState } from '@app/shared/types/member';
|
import { MemberState } from '@app/shared/types/member';
|
||||||
import pool from '../db';
|
import pool from '../db';
|
||||||
import { requireLogin, requireMemberState, requireRole } from '../middleware/auth';
|
import { requireLogin, requireMemberState, requireRole } from '../middleware/auth';
|
||||||
import { assignUserGroup, createGroup } from '../services/rolesService';
|
import { assignUserGroup, createGroup, getAllRoles, getRole, getUsersWithRole } from '../services/rolesService';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
r.use(requireLogin)
|
r.use(requireLogin)
|
||||||
ur.use(requireLogin)
|
ur.use(requireLogin)
|
||||||
@@ -44,43 +45,37 @@ ur.delete('/', [requireMemberState(MemberState.Member), requireRole("17th Admini
|
|||||||
//get all roles
|
//get all roles
|
||||||
r.get('/', [requireMemberState(MemberState.Member)], async (req, res) => {
|
r.get('/', [requireMemberState(MemberState.Member)], async (req, res) => {
|
||||||
try {
|
try {
|
||||||
var con = await pool.getConnection();
|
const roles = await getAllRoles();
|
||||||
|
res.status(200).json(roles);
|
||||||
// Get all roles
|
|
||||||
const roles = await con.query('SELECT * FROM roles;');
|
|
||||||
|
|
||||||
// Get all members for each role
|
|
||||||
const membersRoles = await con.query(`
|
|
||||||
SELECT mr.role_id, v.*
|
|
||||||
FROM members_roles mr
|
|
||||||
JOIN view_member_rank_unit_status_latest v ON mr.member_id = v.member_id
|
|
||||||
`);
|
|
||||||
|
|
||||||
|
|
||||||
// Group members by role_id
|
|
||||||
const roleIdToMembers = {};
|
|
||||||
for (const row of membersRoles) {
|
|
||||||
if (!roleIdToMembers[row.role_id]) roleIdToMembers[row.role_id] = [];
|
|
||||||
// Remove role_id from member object
|
|
||||||
const { role_id, ...member } = row;
|
|
||||||
roleIdToMembers[role_id].push(member);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attach members to each role
|
|
||||||
const result = roles.map(role => ({
|
|
||||||
...role,
|
|
||||||
members: roleIdToMembers[role.id] || []
|
|
||||||
}));
|
|
||||||
|
|
||||||
res.json(result);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.sendStatus(500);
|
||||||
} finally {
|
|
||||||
con.release();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
r.get('/:id/members', [requireMemberState(MemberState.Member)], async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const members = await getUsersWithRole(Number(req.params.id));
|
||||||
|
res.status(200).json(members);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.sendStatus(500);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
r.get('/:id', [requireMemberState(MemberState.Member)], async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const role = await getRole(Number(req.params.id));
|
||||||
|
res.status(200).json(role);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.sendStatus(500);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//create a new role
|
//create a new role
|
||||||
r.post('/', [requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req, res) => {
|
r.post('/', [requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { MemberLight } from '@app/shared/types/member';
|
||||||
import pool from '../db';
|
import pool from '../db';
|
||||||
import { Role } from '@app/shared/types/roles'
|
import { Role, RoleSummary } from '@app/shared/types/roles'
|
||||||
|
|
||||||
export async function assignUserGroup(userID: number, roleID: number) {
|
export async function assignUserGroup(userID: number, roleID: number) {
|
||||||
|
|
||||||
@@ -25,3 +26,33 @@ export async function getUserRoles(userID: number): Promise<Role[]> {
|
|||||||
|
|
||||||
return await pool.query(sql, [userID]);
|
return await pool.query(sql, [userID]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getRole(id: number): Promise<Role> {
|
||||||
|
let res = await pool.query(`SELECT * FROM roles WHERE id = ?`, [id])
|
||||||
|
return res[0] as Role;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllRoles(): Promise<RoleSummary> {
|
||||||
|
return await pool.query(`SELECT id, name, color FROM roles`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUsersWithRole(roleId: number): Promise<MemberLight[]> {
|
||||||
|
const out = await pool.query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
m.member_id AS id,
|
||||||
|
m.member_name AS username,
|
||||||
|
m.displayName,
|
||||||
|
u.color
|
||||||
|
FROM members_roles mr
|
||||||
|
JOIN view_member_rank_unit_status_latest m
|
||||||
|
ON m.member_id = mr.member_id
|
||||||
|
LEFT JOIN units u
|
||||||
|
ON u.name = m.unit
|
||||||
|
WHERE mr.role_id = ?
|
||||||
|
`,
|
||||||
|
[roleId]
|
||||||
|
)
|
||||||
|
|
||||||
|
return out as MemberLight[]
|
||||||
|
}
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
|
import { MemberLight } from "./member";
|
||||||
|
|
||||||
export interface Role {
|
export interface Role {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RoleSummary {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
export type Role = {
|
import { Member, MemberLight } from "@shared/types/member";
|
||||||
id: number;
|
import { Role } from "@shared/types/roles";
|
||||||
name: string;
|
|
||||||
color: string;
|
|
||||||
description: string | null;
|
|
||||||
members: any[];
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const addr = import.meta.env.VITE_APIHOST;
|
const addr = import.meta.env.VITE_APIHOST;
|
||||||
@@ -22,6 +17,30 @@ export async function getRoles(): Promise<Role[]> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getRoleDetails(id: number): Promise<Role> {
|
||||||
|
const res = await fetch(`${addr}/roles/${id}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
return res.json() as Promise<Role>;
|
||||||
|
} else {
|
||||||
|
throw new Error("Could not load role");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRoleMembers(id: number): Promise<MemberLight[]> {
|
||||||
|
const res = await fetch(`${addr}/roles/${id}/members`, {
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
return res.json();
|
||||||
|
} else {
|
||||||
|
throw new Error("Could not load members");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function createRole(name: string, color: string, description: string | null): Promise<Role | null> {
|
export async function createRole(name: string, color: string, description: string | null): Promise<Role | null> {
|
||||||
const res = await fetch(`${addr}/roles`, {
|
const res = await fetch(`${addr}/roles`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
111
ui/src/components/roles/roleView.vue
Normal file
111
ui/src/components/roles/roleView.vue
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { getRoleDetails, getRoleMembers } from '@/api/roles'
|
||||||
|
import type { MemberLight } from '@shared/types/member'
|
||||||
|
import type { Role } from '@shared/types/roles'
|
||||||
|
import { onMounted, ref, watch } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
|
import Separator from '@/components/ui/separator/Separator.vue'
|
||||||
|
import { Plus, X } from 'lucide-vue-next'
|
||||||
|
import MemberCard from '../members/MemberCard.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const roleData = ref<Role | null>(null)
|
||||||
|
const roleMembers = ref<MemberLight[]>([])
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
|
async function loadRole() {
|
||||||
|
loading.value = true
|
||||||
|
const id = Number(route.params.id)
|
||||||
|
console.log(id);
|
||||||
|
roleData.value = await getRoleDetails(id)
|
||||||
|
roleMembers.value = await getRoleMembers(id)
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// const availableMembers = computed(() => {
|
||||||
|
// if (!activeRole.value) return [];
|
||||||
|
// return allMembers.value.filter(
|
||||||
|
// member => !activeRole.value!.members.some(
|
||||||
|
// roleMember => roleMember.member_id === member.member_id
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
// })
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(loadRole)
|
||||||
|
watch(() => route.params.id, loadRole)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-full px-6 py-2">
|
||||||
|
<!-- Loading -->
|
||||||
|
<div v-if="loading" class="text-muted-foreground">
|
||||||
|
Loading group…
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No role selected -->
|
||||||
|
<div v-else-if="!roleData" class="text-muted-foreground">
|
||||||
|
Select a group to view details
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Role details -->
|
||||||
|
<div v-else class="space-y-6">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="h-3 w-3 rounded-full" :style="{ backgroundColor: roleData.color }" />
|
||||||
|
<h2 class="text-2xl font-semibold tracking-tight">
|
||||||
|
{{ roleData.name }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{{ roleData.description || 'No description provided.' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <Button variant="ghost" size="sm" class="text-destructive">
|
||||||
|
Delete
|
||||||
|
</Button> -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<!-- Members -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-sm font-medium">
|
||||||
|
Members ({{ roleMembers.length }})
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<Button size="sm" variant="secondary">
|
||||||
|
<Plus class="mr-2 h-4 w-4" />
|
||||||
|
Add Member
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty state -->
|
||||||
|
<div v-if="roleMembers.length === 0" class="text-sm text-muted-foreground py-6 text-center">
|
||||||
|
No members in this group yet.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-y-auto pr-1">
|
||||||
|
<ul class="space-y-1">
|
||||||
|
<li v-for="member in roleMembers" :key="member.id"
|
||||||
|
class="flex items-center justify-between rounded-md px-3 py-2 hover:bg-muted">
|
||||||
|
<MemberCard :member-id="member.id" />
|
||||||
|
|
||||||
|
<Button variant="ghost" size="icon" class="text-muted-foreground hover:text-destructive">
|
||||||
|
<X class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card'
|
} from '@/components/ui/card'
|
||||||
import { onMounted, ref, computed, reactive, watch } from 'vue';
|
import { onMounted, ref, computed, reactive, watch } from 'vue';
|
||||||
import { addMemberToRole, createRole, deleteRole, getRoles, removeMemberFromRole, Role } from '@/api/roles';
|
import { addMemberToRole, createRole, deleteRole, getRoles, removeMemberFromRole } from '@/api/roles';
|
||||||
import Badge from '@/components/ui/badge/Badge.vue';
|
import Badge from '@/components/ui/badge/Badge.vue';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -34,8 +34,11 @@ import { Plus, X } from 'lucide-vue-next';
|
|||||||
import Separator from '@/components/ui/separator/Separator.vue';
|
import Separator from '@/components/ui/separator/Separator.vue';
|
||||||
import Input from '@/components/ui/input/Input.vue';
|
import Input from '@/components/ui/input/Input.vue';
|
||||||
import Label from '@/components/ui/label/Label.vue';
|
import Label from '@/components/ui/label/Label.vue';
|
||||||
import { getMembers } from '@/api/member';
|
import { getAllLightMembers, getMembers } from '@/api/member';
|
||||||
import { Member } from '@shared/types/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 roles = ref<Role[]>([])
|
||||||
const activeRole = ref<Role | null>(null)
|
const activeRole = ref<Role | null>(null)
|
||||||
@@ -43,16 +46,9 @@ const showDialog = ref(false);
|
|||||||
const showCreateGroupDialog = ref(false);
|
const showCreateGroupDialog = ref(false);
|
||||||
const addingMember = ref(false);
|
const addingMember = ref(false);
|
||||||
const memberToAdd = ref<Member | null>(null);
|
const memberToAdd = ref<Member | null>(null);
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
const allMembers = ref<Member[]>([])
|
const allMembers = ref<MemberLight[]>([])
|
||||||
const availableMembers = computed(() => {
|
|
||||||
if (!activeRole.value) return [];
|
|
||||||
return allMembers.value.filter(
|
|
||||||
member => !activeRole.value!.members.some(
|
|
||||||
roleMember => roleMember.member_id === member.member_id
|
|
||||||
)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
|
|
||||||
type RoleDraft = {
|
type RoleDraft = {
|
||||||
name: string
|
name: string
|
||||||
@@ -137,121 +133,36 @@ async function handleDeleteRole() {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
roles.value = await getRoles();
|
roles.value = await getRoles();
|
||||||
allMembers.value = await getMembers();
|
allMembers.value = await getAllLightMembers();
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="max-w-7xl mx-auto w-full my-[0.2dvh] h-[0.8dvh]">
|
||||||
<Dialog v-model:open="showDialog"
|
<div class="flex items-center justify-between">
|
||||||
v-on:update:open="() => { showDialog = false; addingMember = false; memberToAdd = null; }">
|
<p class="scroll-m-20 text-2xl font-semibold tracking-tight my-5">Role Management</p>
|
||||||
<DialogContent>
|
</div>
|
||||||
<DialogHeader>
|
<div class="grid grid-cols-[minmax(400px,280px)_1fr] gap-5">
|
||||||
<DialogTitle class="flex justify-between items-center">{{ activeRole.name }}
|
<div class="w-full pr-5 border-r">
|
||||||
<Badge class="mr-5">
|
<div class="space-y-1">
|
||||||
{{ activeRole.color }}
|
<p class="px-3 py-2 text-xs font-medium text-muted-foreground uppercase">
|
||||||
</Badge>
|
Roles
|
||||||
</DialogTitle>
|
</p>
|
||||||
<DialogDescription>
|
|
||||||
{{ activeRole.description }}
|
<button v-for="r in roles" :key="r.id" @click="$router.push(`/administration/roles/${r.id}`)"
|
||||||
</DialogDescription>
|
:class="[
|
||||||
</DialogHeader>
|
'flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors cursor-pointer',
|
||||||
<div class="mt-5">
|
Number(route.params.id) == r.id
|
||||||
<div class="flex justify-between items-center">
|
? 'bg-muted font-medium'
|
||||||
<p>Members with this role</p>
|
: 'hover:bg-muted/50'
|
||||||
</div>
|
]">
|
||||||
<Separator class="my-2"></Separator>
|
<span class="h-2 w-2 rounded-full shrink-0" :style="{ backgroundColor: r.color }" />
|
||||||
<ul class="flex flex-col gap-2">
|
<span class="truncate">{{ r.name }}</span>
|
||||||
<li v-for="member in activeRole.members" class="flex justify-between items-center">
|
</button>
|
||||||
<p>{{ member.member_name }}</p>
|
|
||||||
<X class="text-muted-foreground" @click="handleRemoveMember(member.member_id)"></X>
|
|
||||||
</li>
|
|
||||||
<div v-if="!addingMember" @click="addingMember = true"
|
|
||||||
class="flex gap-2 text-muted-foreground hover:text-primary hover:cursor-pointer">
|
|
||||||
<Plus :size="20"></Plus>Add Member
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex gap-2">
|
|
||||||
<Combobox v-model="memberToAdd" by="value">
|
|
||||||
<ComboboxAnchor>
|
|
||||||
<div class="relative w-full max-w-sm items-center">
|
|
||||||
<ComboboxInput class="pl-9"
|
|
||||||
:display-value="(member: Member) => member?.member_name"
|
|
||||||
placeholder="Search Members" />
|
|
||||||
</div>
|
|
||||||
</ComboboxAnchor>
|
|
||||||
<ComboboxList>
|
|
||||||
<ComboboxEmpty>
|
|
||||||
No Members.
|
|
||||||
</ComboboxEmpty>
|
|
||||||
<ComboboxGroup>
|
|
||||||
<ComboboxItem v-for="member in availableMembers" :key="member.member_id"
|
|
||||||
:value="member">
|
|
||||||
{{ member.member_name }}
|
|
||||||
</ComboboxItem>
|
|
||||||
</ComboboxGroup>
|
|
||||||
</ComboboxList>
|
|
||||||
</Combobox>
|
|
||||||
<Button variant="secondary" @click="addingMember = false">Cancel</Button>
|
|
||||||
<Button @click="handleAddMember">Save</Button>
|
|
||||||
</div>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
|
||||||
<!-- <Button @click="handleDeleteRole">Delete Group</Button> -->
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
<Dialog v-model:open="showCreateGroupDialog" v-on:update:open="() => { showCreateGroupDialog = false; }">
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle class="flex justify-between items-center">Create Group</DialogTitle>
|
|
||||||
<DialogDescription>Create a new group of members</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<form class="mt-5 space-y-5" @submit.prevent="handleCreateGroup">
|
|
||||||
<div>
|
|
||||||
<Label class="mb-2 block" for="group-name">Group Name</Label>
|
|
||||||
<Input id="group-name" v-model="roleDraft.name" :aria-invalid="!!draftErrors.name" />
|
|
||||||
<p v-if="draftErrors.name" class="text-destructive text-sm mt-1">{{ draftErrors.name }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label class="mb-2 block" for="group-desc">Description</Label>
|
|
||||||
<Input id="group-desc" v-model="roleDraft.description" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label class="mb-2 block" for="group-color">Color</Label>
|
|
||||||
<Input id="group-color" type="color" v-model="roleDraft.color" />
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button type="button" variant="secondary" @click="showCreateGroupDialog = false"
|
|
||||||
:disabled="creating">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" :disabled="creating">
|
|
||||||
{{ creating ? "Saving..." : "Save" }}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
<div class="max-w-5xl mx-auto">
|
|
||||||
<div class="flex items-center justify-between my-4">
|
|
||||||
<p>Groups</p>
|
|
||||||
<!-- <Button @click="showCreateGroupDialog = true">+ Add New Group</Button> -->
|
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-5">
|
<div>
|
||||||
<Card v-for="value in roles" :key="value.id" @click="activeRole = value; showDialog = true"
|
<RoleView :all-members="allMembers"></RoleView>
|
||||||
class="cursor-pointer">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle class="flex justify-between items-center">{{ value.name }}
|
|
||||||
<Badge>
|
|
||||||
{{ value.color }}
|
|
||||||
</Badge>
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>{{ value.description }}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardFooter>
|
|
||||||
<p class="text-muted-foreground">{{ value.members.length }} members</p>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ const router = createRouter({
|
|||||||
{ path: 'applications/:id', component: () => import('@/pages/Application.vue') },
|
{ path: 'applications/:id', component: () => import('@/pages/Application.vue') },
|
||||||
{ path: 'transfer', component: () => import('@/pages/ManageTransfers.vue') },
|
{ path: 'transfer', component: () => import('@/pages/ManageTransfers.vue') },
|
||||||
{ path: 'loa', component: () => import('@/pages/ManageLOA.vue') },
|
{ path: 'loa', component: () => import('@/pages/ManageLOA.vue') },
|
||||||
{ path: 'roles', component: () => import('@/pages/ManageRoles.vue') }
|
{ path: 'roles', component: () => import('@/pages/ManageRoles.vue') },
|
||||||
|
{ path: 'roles/:id', component: () => import('@/pages/ManageRoles.vue') }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user