Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions VueApp/src/Effort/App.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<template>
<main>
<component
:is="$route.meta.layout || 'div'"
nav="viper-effort"
:navarea="true"
highlighted-top-nav="Faculty"
/>
</main>
<GenericError />
</template>

<script lang="ts">
import GenericError from "@/components/GenericError.vue"
export default {
name: "EffortApplication",
components: {
GenericError,
},
}
</script>
174 changes: 174 additions & 0 deletions VueApp/src/Effort/__tests__/use-effort-permissions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import { setActivePinia, createPinia } from "pinia"
import { useEffortPermissions, EffortPermissions } from "../composables/use-effort-permissions"
import { useUserStore } from "@/store/UserStore"

describe("useEffortPermissions", () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
})

describe("Permission Constants", () => {
it("exports correct permission strings", () => {
expect(EffortPermissions.Base).toBe("SVMSecure.Effort")
expect(EffortPermissions.ViewDept).toBe("SVMSecure.Effort.ViewDept")
expect(EffortPermissions.ViewAllDepartments).toBe("SVMSecure.Effort.ViewAllDepartments")
expect(EffortPermissions.EditEffort).toBe("SVMSecure.Effort.EditEffort")
expect(EffortPermissions.ManageTerms).toBe("SVMSecure.Effort.ManageTerms")
expect(EffortPermissions.VerifyEffort).toBe("SVMSecure.Effort.VerifyEffort")
})
})

describe("hasPermission function", () => {
it("returns true when user has the permission", () => {
const userStore = useUserStore()
userStore.setPermissions(["SVMSecure.Effort", "SVMSecure.Effort.ViewDept"] as never[])

const { hasPermission } = useEffortPermissions()

expect(hasPermission("SVMSecure.Effort")).toBeTruthy()
expect(hasPermission("SVMSecure.Effort.ViewDept")).toBeTruthy()
})

it("returns false when user does not have the permission", () => {
const userStore = useUserStore()
userStore.setPermissions(["SVMSecure.Effort"] as never[])

const { hasPermission } = useEffortPermissions()

expect(hasPermission("SVMSecure.Effort.ManageTerms")).toBeFalsy()
})

it("returns false when user has no permissions", () => {
const { hasPermission } = useEffortPermissions()

expect(hasPermission("SVMSecure.Effort")).toBeFalsy()
})
})

describe("computed permission flags", () => {
describe("hasManageTerms", () => {
it("returns true when user has ManageTerms permission", () => {
const userStore = useUserStore()
userStore.setPermissions([EffortPermissions.ManageTerms] as never[])

const { hasManageTerms } = useEffortPermissions()

expect(hasManageTerms.value).toBeTruthy()
})

it("returns false when user lacks ManageTerms permission", () => {
const userStore = useUserStore()
userStore.setPermissions([EffortPermissions.ViewDept] as never[])

const { hasManageTerms } = useEffortPermissions()

expect(hasManageTerms.value).toBeFalsy()
})
})

describe("hasViewAllDepartments", () => {
it("returns true for admin users with ViewAllDepartments", () => {
const userStore = useUserStore()
userStore.setPermissions([EffortPermissions.ViewAllDepartments] as never[])

const { hasViewAllDepartments, isAdmin } = useEffortPermissions()

expect(hasViewAllDepartments.value).toBeTruthy()
expect(isAdmin.value).toBeTruthy()
})

it("returns false for regular department users", () => {
const userStore = useUserStore()
userStore.setPermissions([EffortPermissions.ViewDept] as never[])

const { hasViewAllDepartments, isAdmin } = useEffortPermissions()

expect(hasViewAllDepartments.value).toBeFalsy()
expect(isAdmin.value).toBeFalsy()
})
})

describe("hasViewDept", () => {
it("returns true when user has ViewDept permission", () => {
const userStore = useUserStore()
userStore.setPermissions([EffortPermissions.ViewDept] as never[])

const { hasViewDept } = useEffortPermissions()

expect(hasViewDept.value).toBeTruthy()
})
})

describe("hasEditEffort", () => {
it("returns true when user has EditEffort permission", () => {
const userStore = useUserStore()
userStore.setPermissions([EffortPermissions.EditEffort] as never[])

const { hasEditEffort } = useEffortPermissions()

expect(hasEditEffort.value).toBeTruthy()
})
})

describe("hasVerifyEffort", () => {
it("returns true when user has VerifyEffort permission (self-service)", () => {
const userStore = useUserStore()
userStore.setPermissions([EffortPermissions.VerifyEffort] as never[])

const { hasVerifyEffort } = useEffortPermissions()

expect(hasVerifyEffort.value).toBeTruthy()
})
})
})

describe("permission combinations for term management", () => {
it("term manager can manage terms and view all departments", () => {
const userStore = useUserStore()
userStore.setPermissions([EffortPermissions.ViewAllDepartments, EffortPermissions.ManageTerms] as never[])

const { hasManageTerms, isAdmin } = useEffortPermissions()

expect(hasManageTerms.value).toBeTruthy()
expect(isAdmin.value).toBeTruthy()
})

it("department user without ManageTerms cannot manage terms", () => {
const userStore = useUserStore()
userStore.setPermissions([EffortPermissions.ViewDept, EffortPermissions.EditEffort] as never[])

const { hasManageTerms, hasEditEffort, hasViewDept } = useEffortPermissions()

expect(hasManageTerms.value).toBeFalsy()
expect(hasEditEffort.value).toBeTruthy()
expect(hasViewDept.value).toBeTruthy()
})

it("self-service user can only verify their own effort", () => {
const userStore = useUserStore()
userStore.setPermissions([EffortPermissions.VerifyEffort] as never[])

const { hasVerifyEffort, hasEditEffort, hasManageTerms, isAdmin } = useEffortPermissions()

expect(hasVerifyEffort.value).toBeTruthy()
expect(hasEditEffort.value).toBeFalsy()
expect(hasManageTerms.value).toBeFalsy()
expect(isAdmin.value).toBeFalsy()
})
})

describe("permissions array exposure", () => {
it("exposes raw permissions array for advanced checks", () => {
const userStore = useUserStore()
const testPermissions = [EffortPermissions.Base, EffortPermissions.ViewDept, EffortPermissions.EditEffort]
userStore.setPermissions(testPermissions as never[])

const { permissions } = useEffortPermissions()

expect(permissions.value).toEqual(testPermissions)
expect(permissions.value.length).toBe(3)
})
})
})
166 changes: 166 additions & 0 deletions VueApp/src/Effort/components/EffortLeftNav.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<template>
<q-drawer
v-model="localDrawerOpen"
show-if-above
elevated
side="left"
:mini="!localDrawerOpen"
no-mini-animation
:width="300"
id="mainLeftDrawer"
v-cloak
class="no-print"
>
<div
class="q-pa-sm"
id="leftNavMenu"
>
<q-btn
dense
round
unelevated
color="secondary"
icon="close"
class="float-right lt-md"
@click="localDrawerOpen = false"
/>
<h2>Effort 3.0</h2>

<q-list
dense
separator
>
<!-- Current Term - clickable with pencil icon -->
<q-item
v-if="currentTerm"
clickable
v-ripple
:to="{ name: 'TermSelection' }"
class="leftNavHeader"
>
<q-item-section>
<q-item-label lines="1">
{{ currentTerm.termName }}
<q-icon
name="edit"
size="xs"
class="q-ml-xs"
/>
</q-item-label>
</q-item-section>
</q-item>
<q-item
v-else
clickable
v-ripple
:to="{ name: 'TermSelection' }"
class="leftNavHeader"
>
<q-item-section>
<q-item-label lines="1">Select a Term</q-item-label>
</q-item-section>
</q-item>

<!-- Manage Terms - only for ManageTerms users -->
<q-item
v-if="hasManageTerms"
clickable
v-ripple
:to="{ name: 'TermManagement' }"
class="leftNavLink"
>
<q-item-section>
<q-item-label lines="1">Manage Terms</q-item-label>
</q-item-section>
</q-item>

<!-- Audit - only for ViewAudit users -->
<q-item
v-if="hasViewAudit"
clickable
v-ripple
:to="{ name: 'EffortAudit', query: currentTerm ? { termCode: currentTerm.termCode } : {} }"
class="leftNavLink"
>
<q-item-section>
<q-item-label lines="1">Audit Trail</q-item-label>
</q-item-section>
</q-item>

<!-- Spacer -->
<q-item class="leftNavSpacer">
<q-item-section></q-item-section>
</q-item>

<q-item
clickable
v-ripple
href="https://ucdsvm.knowledgeowl.com/help/effort-system-overview"
target="_blank"
rel="noopener noreferrer"
class="leftNavLink"
>
<q-item-section>
<q-item-label lines="1">
<q-icon
name="help_outline"
size="xs"
class="q-mr-xs"
/>
Help
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
</q-drawer>
</template>

<script setup lang="ts">
import { ref, watch } from "vue"
import { useRoute } from "vue-router"
import { effortService } from "../services/effort-service"
import { useEffortPermissions } from "../composables/use-effort-permissions"
import type { TermDto } from "../types"

const props = defineProps<{
drawerOpen: boolean
onDrawerChange: (value: boolean) => void
}>()

const route = useRoute()
const { hasManageTerms, hasViewAudit } = useEffortPermissions()

const localDrawerOpen = ref(props.drawerOpen)
const currentTerm = ref<TermDto | null>(null)

// Sync local drawer state with parent
watch(
() => props.drawerOpen,
(newValue) => {
localDrawerOpen.value = newValue
},
)

watch(localDrawerOpen, (newValue) => {
props.onDrawerChange(newValue)
})

// Load term when termCode changes in route
async function loadCurrentTerm(termCode: number | null) {
if (termCode) {
currentTerm.value = await effortService.getTerm(termCode)
} else {
currentTerm.value = await effortService.getCurrentTerm()
}
}

watch(
() => route.params.termCode,
(newTermCode) => {
const termCode = newTermCode ? parseInt(newTermCode as string, 10) : null
loadCurrentTerm(termCode)
},
{ immediate: true },
)
</script>
Loading
Loading