Skip to content

Commit 7c6bdf1

Browse files
committed
feat(clinical-scheduler): Phase 9 - interactive schedule management
- Introduce interactive schedule editing with conflict detection and audit trail support - Add RecentSelections component for quick clinician and rotation access - Extract scheduling logic into composables (management, display, permissions, state updates) - Implement InstructorScheduleService with CRUD operations and backend integration - Enhance ClinicianScheduleView and RotationScheduleView with interactive features - Add primary evaluator management and permission-based editing controls - Reorganize styles (site.css → styles/index.css) with centralized brand color system - Improve navigation with parameterized route handling and active state fixes - Standardize error handling with consistent user feedback
1 parent f4071f4 commit 7c6bdf1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+3521
-888
lines changed

VueApp/src/CTS/cts.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,29 @@ import { createApp } from 'vue'
44
import { createPinia } from 'pinia';
55
import router from './router'
66
import App from './App.vue'
7-
import { Quasar, Loading, QSpinnerOval } from 'quasar'
7+
import { Quasar } from 'quasar'
88
// Import icon libraries
99
import '@quasar/extras/material-icons/material-icons.css'
1010
import '@quasar/extras/material-symbols-outlined/material-symbols-outlined.css'
1111
import IconSet from 'quasar/icon-set/material-symbols-outlined.js'
1212

1313
// Import Quasar css
1414
import 'quasar/dist/quasar.css'
15-
import { useQuasarConfig } from '@/composables/QuasarConfig'
15+
import { initializeQuasar } from '@/composables/QuasarConfig'
1616

1717
//import our css
18-
import '@/assets/site.css'
18+
import '@/styles/index.css'
1919
import '@/cts/assets/cts.css'
2020

21-
const { quasarConfig } = useQuasarConfig()
2221
const pinia = createPinia()
2322
const app = createApp(App)
2423
Quasar.iconSet.set(IconSet)
2524
app.provide("apiURL", import.meta.env.VITE_API_URL)
2625

2726
app.use(pinia)
2827
app.use(router)
29-
app.use(Quasar, quasarConfig)
28+
29+
// Initialize Quasar with our brand colors
30+
initializeQuasar(app)
31+
3032
app.mount('#myApp')

VueApp/src/ClinicalScheduler/clinicalscheduler.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,24 @@ import { createApp } from 'vue'
22
import { createPinia } from 'pinia';
33
import router from './router'
44
import App from './App.vue'
5-
import { Quasar } from 'quasar'
65
// Import icon libraries
76
import '@quasar/extras/material-icons/material-icons.css'
87

98
// Import Quasar css
109
import 'quasar/dist/quasar.css'
11-
import { useQuasarConfig } from '@/composables/QuasarConfig'
10+
import { initializeQuasar } from '@/composables/QuasarConfig'
1211

1312
//import our css
14-
import '@/assets/site.css'
13+
import '@/styles/index.css'
1514

16-
const { quasarConfig } = useQuasarConfig()
1715
const pinia = createPinia()
1816
const app = createApp(App)
1917
app.provide("apiURL", import.meta.env.VITE_API_URL)
2018
app.provide("viperOneUrl", import.meta.env.VITE_VIPER_1_HOME)
2119
app.use(pinia)
2220
app.use(router)
23-
app.use(Quasar, quasarConfig)
21+
22+
// Initialize Quasar with our brand colors
23+
initializeQuasar(app)
24+
2425
app.mount('#myApp')
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<template>
2+
<q-card
3+
class="q-mb-md"
4+
flat
5+
bordered
6+
>
7+
<q-card-section class="q-pa-sm">
8+
<div class="column q-gutter-sm">
9+
<!-- Recent items section -->
10+
<div>
11+
<div class="row items-center q-gutter-sm q-mb-xs">
12+
<div class="text-body2 text-weight-medium">
13+
{{ recentLabel }}
14+
</div>
15+
<!-- Clear selection button -->
16+
<q-btn
17+
v-if="selectedItem"
18+
flat
19+
color="grey-7"
20+
size="xs"
21+
@click="clearSelection"
22+
:title="`Clear ${itemType} selection`"
23+
class="q-px-sm"
24+
>
25+
Clear selection
26+
</q-btn>
27+
</div>
28+
<div class="q-gutter-none">
29+
<q-chip
30+
v-for="item in items"
31+
:key="getItemKey(item)"
32+
:color="isSelected(item) ? 'primary' : undefined"
33+
:text-color="isSelected(item) ? 'white' : 'dark'"
34+
:outline="!isSelected(item)"
35+
clickable
36+
size="sm"
37+
class="q-mr-xs"
38+
@click="selectItem(item)"
39+
>
40+
{{ getItemDisplayName(item) }}
41+
</q-chip>
42+
</div>
43+
</div>
44+
45+
<!-- Dropdown section -->
46+
<div>
47+
<div
48+
class="text-body2 text-weight-medium"
49+
:class="`q-mb-${labelSpacing}`"
50+
>
51+
{{ addNewLabel }}
52+
</div>
53+
<div
54+
v-if="selectorSpacing !== 'none'"
55+
:class="`q-mb-${selectorSpacing}`"
56+
>
57+
<slot name="selector" />
58+
</div>
59+
<div v-else>
60+
<slot name="selector" />
61+
</div>
62+
</div>
63+
</div>
64+
</q-card-section>
65+
</q-card>
66+
</template>
67+
68+
<script setup lang="ts" generic="T extends Record<string, any>">
69+
interface Props<T> {
70+
items: T[]
71+
selectedItem: T | null
72+
recentLabel: string
73+
addNewLabel: string
74+
itemType: string
75+
itemKeyField: keyof T
76+
itemDisplayField: keyof T
77+
labelSpacing?: 'xs' | 'sm' | 'md' | 'lg'
78+
selectorSpacing?: 'none' | 'xs' | 'sm' | 'md' | 'lg'
79+
}
80+
81+
interface Emits<T> {
82+
(e: 'select-item', item: T): void
83+
(e: 'clear-selection'): void
84+
}
85+
86+
const props = withDefaults(defineProps<Props<T>>(), {
87+
labelSpacing: 'xs',
88+
selectorSpacing: 'none'
89+
})
90+
const emit = defineEmits<Emits<T>>()
91+
92+
function getItemKey(item: T): string | number {
93+
return item[props.itemKeyField] as string | number
94+
}
95+
96+
function getItemDisplayName(item: T): string {
97+
return String(item[props.itemDisplayField])
98+
}
99+
100+
function isSelected(item: T): boolean {
101+
if (!props.selectedItem) return false
102+
return getItemKey(item) === getItemKey(props.selectedItem)
103+
}
104+
105+
function selectItem(item: T): void {
106+
emit('select-item', item)
107+
}
108+
109+
function clearSelection(): void {
110+
emit('clear-selection')
111+
}
112+
</script>

VueApp/src/ClinicalScheduler/components/RotationSelector.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,15 @@ watch(() => props.onlyWithScheduledWeeks, () => {
199199
loadRotations()
200200
})
201201
202+
// Watch for model value changes to clear search
203+
watch(() => props.modelValue, (newValue) => {
204+
if (newValue === null) {
205+
searchQuery.value = ''
206+
// Reset filtered rotations when cleared
207+
filteredRotations.value = rotations.value
208+
}
209+
})
210+
202211
// Lifecycle
203212
onMounted(() => {
204213
loadRotations()
Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,40 @@
11
<template>
2+
<!-- Navigation tabs with proper active state for parameterized routes -->
23
<q-tabs
3-
v-model="currentTab"
44
class="text-grey q-mb-md"
55
active-color="primary"
66
indicator-color="primary"
77
align="left"
88
no-caps
9+
role="tablist"
10+
aria-label="Clinical Scheduler Navigation"
911
>
1012
<q-route-tab
1113
name="home"
1214
label="Home"
1315
to="/ClinicalScheduler/"
14-
exact
16+
:exact="true"
17+
:aria-controls="`home-panel`"
18+
:id="`home-tab`"
19+
role="tab"
1520
/>
1621
<q-route-tab
1722
name="rotation"
1823
label="Schedule by Rotation"
19-
to="/ClinicalScheduler/rotation"
24+
:to="{ name: 'RotationSchedule' }"
25+
:class="rotationTabClass"
26+
:aria-controls="`rotation-panel`"
27+
:id="`rotation-tab`"
28+
role="tab"
2029
/>
2130
<q-route-tab
2231
name="clinician"
2332
label="Schedule by Clinician"
24-
to="/ClinicalScheduler/clinician"
33+
:to="{ name: 'ClinicianSchedule' }"
34+
:class="clinicianTabClass"
35+
:aria-controls="`clinician-panel`"
36+
:id="`clinician-tab`"
37+
role="tab"
2538
/>
2639
</q-tabs>
2740
</template>
@@ -32,16 +45,21 @@ import { useRoute } from 'vue-router'
3245
3346
const route = useRoute()
3447
35-
// Determine current tab based on route
36-
const currentTab = computed(() => {
37-
const path = route.path
38-
if (path === '/ClinicalScheduler/' || path === '/ClinicalScheduler') {
39-
return 'home'
40-
} else if (path.startsWith('/ClinicalScheduler/rotation')) {
41-
return 'rotation'
42-
} else if (path.startsWith('/ClinicalScheduler/clinician')) {
43-
return 'clinician'
44-
}
45-
return 'home'
46-
})
47-
</script>
48+
// Helper to check if a tab should be active based on route name
49+
const isTabActive = (baseRouteName: string): string => {
50+
const routeName = route.name as string
51+
const isActive = routeName === baseRouteName || routeName === `${baseRouteName}WithId`
52+
return isActive ? 'q-tab--active' : ''
53+
}
54+
55+
const rotationTabClass = computed(() => isTabActive('RotationSchedule'))
56+
const clinicianTabClass = computed(() => isTabActive('ClinicianSchedule'))
57+
</script>
58+
59+
<style scoped>
60+
/* Fix text color when tab is manually marked as active but Quasar thinks it's inactive */
61+
:deep(.q-tab--active.q-tab--inactive) {
62+
color: var(--q-primary) !important;
63+
opacity: 1 !important;
64+
}
65+
</style>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export { useScheduleManagement } from './useScheduleManagement'
2+
export { useSchedulePermissions } from './useSchedulePermissions'
3+
export { useScheduleDisplay } from './useScheduleDisplay'
4+
5+
export type {
6+
ScheduleAddRequest,
7+
ScheduleUpdateCallbacks
8+
} from './useScheduleManagement'
9+
10+
export type {
11+
ScheduleWeek
12+
} from './useScheduleDisplay'

0 commit comments

Comments
 (0)