@ngbplatform/ui 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +44 -0
  3. package/package.json +61 -0
  4. package/public/favicon.svg +6 -0
  5. package/public/silent-check-sso.html +8 -0
  6. package/src/assets/ngb_logo.svg +17 -0
  7. package/src/assets/ngb_logo_color.svg +17 -0
  8. package/src/index.ts +1023 -0
  9. package/src/ngb/README.md +27 -0
  10. package/src/ngb/accounting/NgbAccountingPeriodClosingPage.vue +1157 -0
  11. package/src/ngb/accounting/NgbChartOfAccountEditor.vue +590 -0
  12. package/src/ngb/accounting/NgbChartOfAccountsPage.vue +420 -0
  13. package/src/ngb/accounting/NgbGeneralJournalEntryEditPage.vue +796 -0
  14. package/src/ngb/accounting/NgbGeneralJournalEntryLinesEditor.vue +454 -0
  15. package/src/ngb/accounting/NgbGeneralJournalEntryListPage.vue +186 -0
  16. package/src/ngb/accounting/api.ts +79 -0
  17. package/src/ngb/accounting/generalJournalEntry.ts +127 -0
  18. package/src/ngb/accounting/generalJournalEntryApi.ts +112 -0
  19. package/src/ngb/accounting/generalJournalEntryTypes.ts +155 -0
  20. package/src/ngb/accounting/navigation.ts +83 -0
  21. package/src/ngb/accounting/periodClosing.ts +75 -0
  22. package/src/ngb/accounting/periodClosingApi.ts +60 -0
  23. package/src/ngb/accounting/periodClosingTypes.ts +73 -0
  24. package/src/ngb/accounting/types.ts +57 -0
  25. package/src/ngb/api/audit.ts +23 -0
  26. package/src/ngb/api/catalogs.ts +50 -0
  27. package/src/ngb/api/contracts.ts +285 -0
  28. package/src/ngb/api/documents.ts +170 -0
  29. package/src/ngb/api/http.ts +446 -0
  30. package/src/ngb/api/lookups.ts +14 -0
  31. package/src/ngb/api/types.ts +6 -0
  32. package/src/ngb/auth/index.ts +13 -0
  33. package/src/ngb/auth/keycloak.ts +374 -0
  34. package/src/ngb/auth/router.ts +35 -0
  35. package/src/ngb/auth/types.ts +12 -0
  36. package/src/ngb/auth/useAuthStore.ts +209 -0
  37. package/src/ngb/command-palette/NgbCommandPaletteDialog.vue +342 -0
  38. package/src/ngb/command-palette/NgbCommandPaletteEmptyState.vue +55 -0
  39. package/src/ngb/command-palette/NgbCommandPaletteFooterHints.vue +29 -0
  40. package/src/ngb/command-palette/api.ts +10 -0
  41. package/src/ngb/command-palette/config.ts +16 -0
  42. package/src/ngb/command-palette/presentation.ts +85 -0
  43. package/src/ngb/command-palette/search.ts +130 -0
  44. package/src/ngb/command-palette/staticItems.ts +243 -0
  45. package/src/ngb/command-palette/storage.ts +45 -0
  46. package/src/ngb/command-palette/store.ts +716 -0
  47. package/src/ngb/command-palette/types.ts +120 -0
  48. package/src/ngb/command-palette/useCommandPaletteHotkeys.ts +27 -0
  49. package/src/ngb/command-palette/useCommandPalettePageContext.ts +18 -0
  50. package/src/ngb/components/NgbConfirmDialog.vue +63 -0
  51. package/src/ngb/components/NgbDialog.vue +44 -0
  52. package/src/ngb/components/NgbDrawer.vue +156 -0
  53. package/src/ngb/components/NgbHeaderActionCluster.vue +140 -0
  54. package/src/ngb/components/NgbModalShell.vue +99 -0
  55. package/src/ngb/components/NgbToolbar.vue +14 -0
  56. package/src/ngb/components/forms/NgbFormLayout.vue +13 -0
  57. package/src/ngb/components/forms/NgbFormRow.vue +21 -0
  58. package/src/ngb/components/forms/NgbFormSection.vue +16 -0
  59. package/src/ngb/components/forms/NgbValidationSummary.vue +18 -0
  60. package/src/ngb/components/navigation/NgbNavigationTree.vue +68 -0
  61. package/src/ngb/components/navigation/NgbTreeNode.vue +94 -0
  62. package/src/ngb/components/register/NgbColumnChooser.vue +101 -0
  63. package/src/ngb/components/register/NgbRegisterGrid.vue +473 -0
  64. package/src/ngb/components/register/registerTypes.ts +51 -0
  65. package/src/ngb/components/register/useRegisterColumnResize.ts +99 -0
  66. package/src/ngb/components/register/useRegisterColumnState.ts +220 -0
  67. package/src/ngb/components/register/useRegisterRows.ts +209 -0
  68. package/src/ngb/components/register/useRegisterViewport.ts +56 -0
  69. package/src/ngb/editor/NgbDocumentEffectsPage.vue +512 -0
  70. package/src/ngb/editor/NgbDocumentFlowPage.vue +409 -0
  71. package/src/ngb/editor/NgbDocumentPrintPage.vue +712 -0
  72. package/src/ngb/editor/NgbEditorDiscardDialog.vue +40 -0
  73. package/src/ngb/editor/NgbEntityAuditSidebar.vue +288 -0
  74. package/src/ngb/editor/NgbEntityEditor.vue +250 -0
  75. package/src/ngb/editor/NgbEntityEditorDrawerActions.vue +88 -0
  76. package/src/ngb/editor/NgbEntityEditorHeader.vue +114 -0
  77. package/src/ngb/editor/catalogNavigation.ts +13 -0
  78. package/src/ngb/editor/config.ts +252 -0
  79. package/src/ngb/editor/documentCopyDraft.ts +245 -0
  80. package/src/ngb/editor/documentEffects.ts +43 -0
  81. package/src/ngb/editor/documentEffectsDateFormatting.ts +18 -0
  82. package/src/ngb/editor/documentNavigation.ts +115 -0
  83. package/src/ngb/editor/documentStatus.ts +45 -0
  84. package/src/ngb/editor/entityEditorErrors.ts +162 -0
  85. package/src/ngb/editor/entityEditorPersistence.ts +241 -0
  86. package/src/ngb/editor/extensions.ts +40 -0
  87. package/src/ngb/editor/types.ts +295 -0
  88. package/src/ngb/editor/useConfiguredEntityEditorDocumentActions.ts +183 -0
  89. package/src/ngb/editor/useDocumentEditorDrawerState.ts +152 -0
  90. package/src/ngb/editor/useEditorDrawerState.ts +88 -0
  91. package/src/ngb/editor/useEntityEditorBusinessContext.ts +97 -0
  92. package/src/ngb/editor/useEntityEditorCapabilities.ts +190 -0
  93. package/src/ngb/editor/useEntityEditorCommandPalette.ts +154 -0
  94. package/src/ngb/editor/useEntityEditorCommitHandlers.ts +102 -0
  95. package/src/ngb/editor/useEntityEditorHeaderActions.ts +253 -0
  96. package/src/ngb/editor/useEntityEditorLeaveGuard.ts +66 -0
  97. package/src/ngb/editor/useEntityEditorNavigationActions.ts +235 -0
  98. package/src/ngb/editor/useEntityEditorOutputs.ts +59 -0
  99. package/src/ngb/editor/useEntityEditorPageActions.ts +57 -0
  100. package/src/ngb/editor/useRouteQueryEditorDrawer.ts +173 -0
  101. package/src/ngb/env/runtimeConfig.ts +25 -0
  102. package/src/ngb/lookup/config.ts +42 -0
  103. package/src/ngb/lookup/defaultConfig.ts +136 -0
  104. package/src/ngb/lookup/navigation.ts +76 -0
  105. package/src/ngb/lookup/prefetch.ts +40 -0
  106. package/src/ngb/lookup/store.ts +261 -0
  107. package/src/ngb/metadata/NgbDocumentListFiltersDrawer.vue +78 -0
  108. package/src/ngb/metadata/NgbDocumentPeriodFilter.vue +53 -0
  109. package/src/ngb/metadata/NgbEntityForm.vue +84 -0
  110. package/src/ngb/metadata/NgbEntityFormFieldsBlock.vue +110 -0
  111. package/src/ngb/metadata/NgbEntityListPageHeader.vue +80 -0
  112. package/src/ngb/metadata/NgbFilterFieldControl.vue +104 -0
  113. package/src/ngb/metadata/NgbMetadataCatalogEditPage.vue +42 -0
  114. package/src/ngb/metadata/NgbMetadataCatalogListPage.vue +269 -0
  115. package/src/ngb/metadata/NgbMetadataDocumentEditPage.vue +68 -0
  116. package/src/ngb/metadata/NgbMetadataDocumentListPage.vue +353 -0
  117. package/src/ngb/metadata/NgbMetadataFieldRenderer.vue +124 -0
  118. package/src/ngb/metadata/NgbMetadataLookupControl.vue +92 -0
  119. package/src/ngb/metadata/NgbRecycleBinFilter.vue +53 -0
  120. package/src/ngb/metadata/NgbRegisterPageLayout.vue +143 -0
  121. package/src/ngb/metadata/config.ts +28 -0
  122. package/src/ngb/metadata/dataTypes.ts +27 -0
  123. package/src/ngb/metadata/entityForm.ts +114 -0
  124. package/src/ngb/metadata/entityModel.ts +34 -0
  125. package/src/ngb/metadata/entityValueFormatting.ts +83 -0
  126. package/src/ngb/metadata/fieldRendererState.ts +84 -0
  127. package/src/ngb/metadata/filtering.ts +153 -0
  128. package/src/ngb/metadata/lookup.ts +25 -0
  129. package/src/ngb/metadata/monthPagedListQuery.ts +108 -0
  130. package/src/ngb/metadata/normalization.ts +100 -0
  131. package/src/ngb/metadata/referenceHydration.ts +99 -0
  132. package/src/ngb/metadata/register.ts +135 -0
  133. package/src/ngb/metadata/routePages.ts +86 -0
  134. package/src/ngb/metadata/store.ts +35 -0
  135. package/src/ngb/metadata/types.ts +276 -0
  136. package/src/ngb/metadata/useMetadataListFilters.ts +306 -0
  137. package/src/ngb/metadata/useMetadataRegisterPageData.ts +246 -0
  138. package/src/ngb/metadata/useValidationFocus.ts +49 -0
  139. package/src/ngb/primitives/NgbBadge.vue +31 -0
  140. package/src/ngb/primitives/NgbButton.vue +53 -0
  141. package/src/ngb/primitives/NgbCheckbox.vue +33 -0
  142. package/src/ngb/primitives/NgbDatePicker.vue +141 -0
  143. package/src/ngb/primitives/NgbIcon.vue +545 -0
  144. package/src/ngb/primitives/NgbInput.vue +57 -0
  145. package/src/ngb/primitives/NgbLookup.vue +264 -0
  146. package/src/ngb/primitives/NgbMonthPicker.vue +132 -0
  147. package/src/ngb/primitives/NgbMultiSelect.vue +179 -0
  148. package/src/ngb/primitives/NgbPeriodPicker.vue +125 -0
  149. package/src/ngb/primitives/NgbPickerNavButton.vue +31 -0
  150. package/src/ngb/primitives/NgbPickerPopover.vue +79 -0
  151. package/src/ngb/primitives/NgbRadioGroup.vue +53 -0
  152. package/src/ngb/primitives/NgbSelect.vue +108 -0
  153. package/src/ngb/primitives/NgbStatusIcon.vue +63 -0
  154. package/src/ngb/primitives/NgbSwitch.vue +42 -0
  155. package/src/ngb/primitives/NgbTabs.vue +116 -0
  156. package/src/ngb/primitives/NgbToastHost.vue +49 -0
  157. package/src/ngb/primitives/NgbTooltip.vue +27 -0
  158. package/src/ngb/primitives/iconNames.ts +83 -0
  159. package/src/ngb/primitives/toast.ts +66 -0
  160. package/src/ngb/primitives/useAsyncComboboxQuery.ts +85 -0
  161. package/src/ngb/primitives/useFloatingDropdownPosition.ts +129 -0
  162. package/src/ngb/reporting/NgbReportComposerCollectionSection.vue +106 -0
  163. package/src/ngb/reporting/NgbReportComposerPanel.vue +1187 -0
  164. package/src/ngb/reporting/NgbReportDateRangeFilter.vue +54 -0
  165. package/src/ngb/reporting/NgbReportPage.vue +1124 -0
  166. package/src/ngb/reporting/NgbReportSheet.vue +467 -0
  167. package/src/ngb/reporting/api.ts +35 -0
  168. package/src/ngb/reporting/composer.ts +904 -0
  169. package/src/ngb/reporting/config.ts +120 -0
  170. package/src/ngb/reporting/defaultConfig.ts +15 -0
  171. package/src/ngb/reporting/lookupFilters.ts +43 -0
  172. package/src/ngb/reporting/navigation.ts +181 -0
  173. package/src/ngb/reporting/pageHelpers.ts +73 -0
  174. package/src/ngb/reporting/pageSession.ts +92 -0
  175. package/src/ngb/reporting/paging.ts +66 -0
  176. package/src/ngb/reporting/types.ts +333 -0
  177. package/src/ngb/router/backNavigation.ts +122 -0
  178. package/src/ngb/router/queryParams.ts +129 -0
  179. package/src/ngb/router/queryState.ts +121 -0
  180. package/src/ngb/router/routeAliases.ts +66 -0
  181. package/src/ngb/router/routeParams.ts +15 -0
  182. package/src/ngb/router/shareLink.ts +39 -0
  183. package/src/ngb/security/NgbAccessAuditPanel.vue +32 -0
  184. package/src/ngb/security/NgbAccessDeniedState.vue +27 -0
  185. package/src/ngb/security/NgbEffectiveAccessPanel.vue +60 -0
  186. package/src/ngb/security/NgbPermissionMatrix.vue +160 -0
  187. package/src/ngb/security/NgbRoleEditorPage.vue +319 -0
  188. package/src/ngb/security/NgbRolesPage.vue +125 -0
  189. package/src/ngb/security/NgbUserEditorPage.vue +615 -0
  190. package/src/ngb/security/NgbUsersPage.vue +125 -0
  191. package/src/ngb/security/api.ts +91 -0
  192. package/src/ngb/security/audit.ts +21 -0
  193. package/src/ngb/security/permissions.ts +65 -0
  194. package/src/ngb/security/types.ts +157 -0
  195. package/src/ngb/security/useAccessStore.ts +69 -0
  196. package/src/ngb/site/NgbDashboardAsOfToolbar.vue +100 -0
  197. package/src/ngb/site/NgbDashboardStatusBanner.vue +91 -0
  198. package/src/ngb/site/NgbLogo.vue +44 -0
  199. package/src/ngb/site/NgbPageHeader.vue +49 -0
  200. package/src/ngb/site/NgbSiteShell.vue +286 -0
  201. package/src/ngb/site/NgbSiteSidebar.vue +270 -0
  202. package/src/ngb/site/NgbTopBar.vue +425 -0
  203. package/src/ngb/site/NgbTrendChart.vue +224 -0
  204. package/src/ngb/site/dashboardData.ts +324 -0
  205. package/src/ngb/site/mainMenuStore.ts +49 -0
  206. package/src/ngb/site/types.ts +31 -0
  207. package/src/ngb/site/useDashboardPageState.ts +98 -0
  208. package/src/ngb/site/useTheme.ts +95 -0
  209. package/src/ngb/utils/clone.ts +43 -0
  210. package/src/ngb/utils/dateValues.ts +107 -0
  211. package/src/ngb/utils/errorMessage.ts +14 -0
  212. package/src/ngb/utils/guid.ts +21 -0
  213. package/src/ngb/utils/stableValue.ts +21 -0
  214. package/src/ngb/utils/storage.ts +135 -0
  215. package/src/styles/tailwind.css +165 -0
  216. package/src/vite/publicAssets.ts +104 -0
  217. package/vite-public-assets.js +68 -0
@@ -0,0 +1,319 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from 'vue'
3
+ import { useRoute, useRouter } from 'vue-router'
4
+ import { ApiError } from '../api/http'
5
+ import NgbDrawer from '../components/NgbDrawer.vue'
6
+ import NgbBadge from '../primitives/NgbBadge.vue'
7
+ import NgbConfirmDialog from '../components/NgbConfirmDialog.vue'
8
+ import NgbIcon from '../primitives/NgbIcon.vue'
9
+ import NgbInput from '../primitives/NgbInput.vue'
10
+ import NgbStatusIcon from '../primitives/NgbStatusIcon.vue'
11
+ import NgbTabs from '../primitives/NgbTabs.vue'
12
+ import { useOptionalToasts } from '../primitives/toast'
13
+ import NgbEntityAuditSidebar from '../editor/NgbEntityAuditSidebar.vue'
14
+ import NgbPageHeader from '../site/NgbPageHeader.vue'
15
+ import { toErrorMessage } from '../utils/errorMessage'
16
+ import NgbAccessDeniedState from './NgbAccessDeniedState.vue'
17
+ import NgbPermissionMatrix from './NgbPermissionMatrix.vue'
18
+ import { ROLE_AUDIT_BEHAVIOR } from './audit'
19
+ import {
20
+ createRole,
21
+ deactivateRole,
22
+ getPermissionDefinitions,
23
+ getRole,
24
+ reactivateRole,
25
+ updateRole,
26
+ } from './api'
27
+ import { useAccessStore } from './useAccessStore'
28
+ import type { PermissionAssignmentDto, PermissionDefinitionDto, RoleDetailsDto } from './types'
29
+
30
+ type RoleForm = {
31
+ code: string
32
+ name: string
33
+ description: string
34
+ }
35
+
36
+ const AUDIT_ENTITY_KIND_SECURITY_ROLE = 9
37
+
38
+ const route = useRoute()
39
+ const router = useRouter()
40
+ const access = useAccessStore()
41
+
42
+ const loading = ref(false)
43
+ const saving = ref(false)
44
+ const activating = ref(false)
45
+ const error = ref<string | null>(null)
46
+ const accessDenied = ref(false)
47
+ const role = ref<RoleDetailsDto | null>(null)
48
+ const definitions = ref<PermissionDefinitionDto[]>([])
49
+ const permissions = ref<PermissionAssignmentDto[]>([])
50
+ const confirmMode = ref<'deactivate' | 'reactivate' | null>(null)
51
+ const auditOpen = ref(false)
52
+ const activeTab = ref('permissions')
53
+ const toasts = useOptionalToasts()
54
+
55
+ const form = ref<RoleForm>({
56
+ code: '',
57
+ name: '',
58
+ description: '',
59
+ })
60
+
61
+ const roleId = computed(() => String(route.params.roleId ?? 'new'))
62
+ const isNew = computed(() => roleId.value === 'new')
63
+ const canEdit = computed(() => access.canManageRoles)
64
+ const canOpenAudit = computed(() => !isNew.value && !!role.value)
65
+ const title = computed(() => isNew.value ? 'New role' : role.value?.name ?? 'Role')
66
+ const auditEntityTitle = computed(() => title.value)
67
+ const roleTabs = computed(() => [
68
+ { key: 'permissions', label: 'Permissions' },
69
+ ...(role.value ? [{ key: 'assigned-users', label: 'Assigned users' }] : []),
70
+ ])
71
+
72
+ function resetForm(): void {
73
+ role.value = null
74
+ permissions.value = []
75
+ auditOpen.value = false
76
+ activeTab.value = 'permissions'
77
+ form.value = {
78
+ code: '',
79
+ name: '',
80
+ description: '',
81
+ }
82
+ }
83
+
84
+ function applyRole(next: RoleDetailsDto): void {
85
+ role.value = next
86
+ form.value = {
87
+ code: next.code,
88
+ name: next.name,
89
+ description: next.description ?? '',
90
+ }
91
+ permissions.value = [...next.permissions]
92
+ }
93
+
94
+ async function load(): Promise<void> {
95
+ loading.value = true
96
+ error.value = null
97
+ accessDenied.value = false
98
+ resetForm()
99
+
100
+ try {
101
+ await access.load()
102
+ if (isNew.value && !access.canManageRoles) {
103
+ accessDenied.value = true
104
+ return
105
+ }
106
+
107
+ definitions.value = await getPermissionDefinitions()
108
+ if (!isNew.value) {
109
+ applyRole(await getRole(roleId.value))
110
+ }
111
+ } catch (cause) {
112
+ accessDenied.value = cause instanceof ApiError && cause.status === 403
113
+ error.value = accessDenied.value ? null : toErrorMessage(cause, 'Failed to load role')
114
+ } finally {
115
+ loading.value = false
116
+ }
117
+ }
118
+
119
+ async function save(): Promise<void> {
120
+ if (!canEdit.value || saving.value) return
121
+
122
+ saving.value = true
123
+ error.value = null
124
+
125
+ try {
126
+ if (isNew.value) {
127
+ const created = await createRole({
128
+ code: form.value.code,
129
+ name: form.value.name,
130
+ description: form.value.description || null,
131
+ permissions: permissions.value,
132
+ })
133
+ applyRole(created)
134
+ toasts?.push({ title: 'Role created', message: 'Role was saved.', tone: 'success' })
135
+ await router.replace(`/admin/security/roles/${encodeURIComponent(created.roleId)}`)
136
+ return
137
+ }
138
+
139
+ const updated = await updateRole(roleId.value, {
140
+ code: form.value.code,
141
+ name: form.value.name,
142
+ description: form.value.description || null,
143
+ isActive: role.value?.isActive ?? true,
144
+ permissions: permissions.value,
145
+ })
146
+ applyRole(updated)
147
+ toasts?.push({ title: 'Role saved', message: 'Changes were saved.', tone: 'success' })
148
+ } catch (cause) {
149
+ error.value = toErrorMessage(cause, 'Failed to save role')
150
+ } finally {
151
+ saving.value = false
152
+ }
153
+ }
154
+
155
+ async function confirmActivationChange(): Promise<void> {
156
+ if (!role.value || !confirmMode.value) return
157
+
158
+ activating.value = true
159
+ error.value = null
160
+
161
+ try {
162
+ if (confirmMode.value === 'deactivate') await deactivateRole(role.value.roleId)
163
+ else await reactivateRole(role.value.roleId)
164
+ confirmMode.value = null
165
+ await load()
166
+ } catch (cause) {
167
+ error.value = toErrorMessage(cause, 'Failed to update role status')
168
+ } finally {
169
+ activating.value = false
170
+ }
171
+ }
172
+
173
+ function goBack(): void {
174
+ void router.push('/admin/security/roles')
175
+ }
176
+
177
+ function openAuditLog(): void {
178
+ if (!canOpenAudit.value) return
179
+ auditOpen.value = true
180
+ }
181
+
182
+ function closeAuditLog(): void {
183
+ auditOpen.value = false
184
+ }
185
+
186
+ watch(
187
+ () => route.params.roleId,
188
+ () => {
189
+ void load()
190
+ },
191
+ { immediate: true },
192
+ )
193
+ </script>
194
+
195
+ <template>
196
+ <NgbAccessDeniedState v-if="accessDenied" />
197
+
198
+ <div v-else class="flex h-full min-h-0 flex-col">
199
+ <NgbPageHeader :title="title" :can-back="true" :breadcrumbs="['Roles']" @back="goBack">
200
+ <template #secondary>
201
+ <div class="flex min-w-0 items-center gap-2">
202
+ <NgbBadge v-if="role" :tone="role.isActive ? 'success' : 'danger'">{{ role.isActive ? 'Active' : 'Inactive' }}</NgbBadge>
203
+ <NgbBadge v-if="role?.isSystem" tone="neutral">System</NgbBadge>
204
+ </div>
205
+ </template>
206
+ <template #actions>
207
+ <button v-if="canOpenAudit" type="button" class="ngb-iconbtn" title="Audit log" :disabled="loading || saving" @click="openAuditLog">
208
+ <NgbIcon name="history" />
209
+ </button>
210
+ <button
211
+ v-if="role && canEdit && role.isActive"
212
+ type="button"
213
+ class="ngb-iconbtn"
214
+ title="Deactivate"
215
+ aria-label="Deactivate"
216
+ :disabled="saving || activating"
217
+ @click="confirmMode = 'deactivate'"
218
+ >
219
+ <NgbStatusIcon status="marked" title="Deactivate" />
220
+ </button>
221
+ <button
222
+ v-if="role && canEdit && !role.isActive"
223
+ type="button"
224
+ class="ngb-iconbtn"
225
+ title="Reactivate"
226
+ aria-label="Reactivate"
227
+ :disabled="saving || activating"
228
+ @click="confirmMode = 'reactivate'"
229
+ >
230
+ <NgbStatusIcon status="posted" title="Reactivate" />
231
+ </button>
232
+ <button
233
+ type="button"
234
+ class="ngb-iconbtn"
235
+ title="Save"
236
+ aria-label="Save"
237
+ :disabled="!canEdit || loading || saving || !form.code.trim() || !form.name.trim()"
238
+ @click="save"
239
+ >
240
+ <NgbIcon name="save" />
241
+ </button>
242
+ </template>
243
+ </NgbPageHeader>
244
+
245
+ <main class="flex-1 min-h-0 overflow-auto p-6">
246
+ <div v-if="loading" class="text-sm text-ngb-muted">Loading...</div>
247
+
248
+ <div v-else class="space-y-6">
249
+ <section class="rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-4 shadow-card">
250
+ <div class="grid gap-4 md:grid-cols-2">
251
+ <NgbInput v-model="form.code" label="Code" :disabled="!canEdit || role?.isSystem === true" />
252
+ <NgbInput v-model="form.name" label="Name" :disabled="!canEdit" />
253
+ <div class="md:col-span-2">
254
+ <NgbInput v-model="form.description" label="Description" :disabled="!canEdit" />
255
+ </div>
256
+ </div>
257
+
258
+ <div
259
+ v-if="error"
260
+ class="mt-4 rounded-[var(--ngb-radius)] border border-red-200 bg-red-50 p-3 text-sm text-red-900 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-100"
261
+ >
262
+ {{ error }}
263
+ </div>
264
+ </section>
265
+
266
+ <NgbTabs v-model="activeTab" :tabs="roleTabs" full-width-bar>
267
+ <template #default="{ active }">
268
+ <NgbPermissionMatrix
269
+ v-if="active === 'permissions'"
270
+ v-model="permissions"
271
+ :definitions="definitions"
272
+ :disabled="!canEdit"
273
+ />
274
+
275
+ <section v-else-if="role" class="rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card shadow-card">
276
+ <div class="divide-y divide-ngb-border">
277
+ <div
278
+ v-for="assignedUser in role.assignedUsers"
279
+ :key="assignedUser.userId"
280
+ class="flex items-center justify-between gap-3 px-4 py-3"
281
+ >
282
+ <div class="min-w-0">
283
+ <div class="truncate text-sm font-medium text-ngb-text">{{ assignedUser.displayName || assignedUser.email || assignedUser.userId }}</div>
284
+ <div v-if="assignedUser.email" class="mt-0.5 truncate text-xs text-ngb-muted">{{ assignedUser.email }}</div>
285
+ </div>
286
+ <NgbBadge :tone="assignedUser.isActive ? 'success' : 'danger'">{{ assignedUser.isActive ? 'Active' : 'Inactive' }}</NgbBadge>
287
+ </div>
288
+ <div v-if="role.assignedUsers.length === 0" class="px-4 py-6 text-sm text-ngb-muted">No assigned users.</div>
289
+ </div>
290
+ </section>
291
+ </template>
292
+ </NgbTabs>
293
+ </div>
294
+ </main>
295
+
296
+ <NgbConfirmDialog
297
+ :open="confirmMode !== null"
298
+ :title="confirmMode === 'reactivate' ? 'Reactivate role?' : 'Deactivate role?'"
299
+ :message="confirmMode === 'reactivate' ? 'Users assigned to this role will receive its permissions again after their access version is refreshed.' : 'Assigned users keep their history, but this role stops contributing permissions.'"
300
+ :confirm-text="confirmMode === 'reactivate' ? 'Reactivate' : 'Deactivate'"
301
+ :danger="confirmMode === 'deactivate'"
302
+ :confirm-loading="activating"
303
+ @update:open="(value) => { if (!value) confirmMode = null }"
304
+ @confirm="confirmActivationChange"
305
+ />
306
+
307
+ <NgbDrawer v-model:open="auditOpen" title="Audit Log" hide-header flush-body>
308
+ <NgbEntityAuditSidebar
309
+ :open="auditOpen"
310
+ :entity-kind="AUDIT_ENTITY_KIND_SECURITY_ROLE"
311
+ :entity-id="role?.roleId ?? null"
312
+ :entity-title="auditEntityTitle"
313
+ :behavior="ROLE_AUDIT_BEHAVIOR"
314
+ @back="closeAuditLog"
315
+ @close="closeAuditLog"
316
+ />
317
+ </NgbDrawer>
318
+ </div>
319
+ </template>
@@ -0,0 +1,125 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, ref } from 'vue'
3
+ import { useRouter } from 'vue-router'
4
+ import { ApiError } from '../api/http'
5
+ import type { RegisterColumn, RegisterDataRow } from '../components/register/registerTypes'
6
+ import NgbRecycleBinFilter from '../metadata/NgbRecycleBinFilter.vue'
7
+ import NgbRegisterPageLayout from '../metadata/NgbRegisterPageLayout.vue'
8
+ import type { QueryTrashMode } from '../router/queryParams'
9
+ import { toErrorMessage } from '../utils/errorMessage'
10
+ import NgbAccessDeniedState from './NgbAccessDeniedState.vue'
11
+ import { getRoles } from './api'
12
+ import { useAccessStore } from './useAccessStore'
13
+ import type { RoleListItemDto } from './types'
14
+
15
+ const router = useRouter()
16
+ const access = useAccessStore()
17
+
18
+ const loading = ref(false)
19
+ const error = ref<string | null>(null)
20
+ const accessDenied = ref(false)
21
+ const roles = ref<RoleListItemDto[]>([])
22
+ const status = ref<QueryTrashMode>('active')
23
+
24
+ const columns: RegisterColumn[] = [
25
+ {
26
+ key: 'role',
27
+ title: 'Role',
28
+ width: 320,
29
+ minWidth: 240,
30
+ pinned: 'left',
31
+ format: (value) => Array.isArray(value) ? value.join('\n') : String(value ?? ''),
32
+ },
33
+ { key: 'description', title: 'Description', width: 360, minWidth: 220, wrap: true },
34
+ {
35
+ key: 'assignedUsersCount',
36
+ title: 'Users',
37
+ width: 110,
38
+ minWidth: 90,
39
+ align: 'right',
40
+ format: (value) => String(Number.isFinite(Number(value)) ? Math.trunc(Number(value)) : 0),
41
+ },
42
+ { key: 'systemLabel', title: 'System', width: 110, minWidth: 90, align: 'center', sortable: false },
43
+ ]
44
+
45
+ const filteredRoles = computed(() => {
46
+ return roles.value
47
+ .filter((role) => {
48
+ if (status.value === 'active') return role.isActive
49
+ if (status.value === 'deleted') return !role.isActive
50
+ return true
51
+ })
52
+ .sort((a, b) => a.name.localeCompare(b.name))
53
+ })
54
+
55
+ const rows = computed<RegisterDataRow[]>(() => filteredRoles.value.map((role) => ({
56
+ key: role.roleId,
57
+ __status: role.isActive ? 'posted' : 'marked',
58
+ __statusTitle: role.isActive ? 'Active' : 'Inactive',
59
+ role: [role.name, role.code],
60
+ description: role.description || '-',
61
+ assignedUsersCount: role.assignedUsersCount,
62
+ systemLabel: role.isSystem ? 'Yes' : 'No',
63
+ })))
64
+
65
+ async function load(): Promise<void> {
66
+ loading.value = true
67
+ error.value = null
68
+ accessDenied.value = false
69
+
70
+ try {
71
+ await access.load()
72
+ roles.value = await getRoles()
73
+ } catch (cause) {
74
+ roles.value = []
75
+ accessDenied.value = cause instanceof ApiError && cause.status === 403
76
+ error.value = accessDenied.value ? null : toErrorMessage(cause, 'Failed to load roles')
77
+ } finally {
78
+ loading.value = false
79
+ }
80
+ }
81
+
82
+ function openRole(roleId: string): void {
83
+ void router.push(`/admin/security/roles/${encodeURIComponent(roleId)}`)
84
+ }
85
+
86
+ function createRole(): void {
87
+ void router.push('/admin/security/roles/new')
88
+ }
89
+
90
+ function goBack(): void {
91
+ void router.push('/home')
92
+ }
93
+
94
+ onMounted(() => {
95
+ void load()
96
+ })
97
+ </script>
98
+
99
+ <template>
100
+ <NgbAccessDeniedState v-if="accessDenied" />
101
+
102
+ <NgbRegisterPageLayout
103
+ v-else
104
+ title="Roles and permissions"
105
+ :items-count="filteredRoles.length"
106
+ :total="roles.length"
107
+ :loading="loading"
108
+ :error="error"
109
+ :show-filter="false"
110
+ :disable-create="!access.canManageRoles"
111
+ :disable-prev="true"
112
+ :disable-next="true"
113
+ :columns="columns"
114
+ :rows="rows"
115
+ storage-key="ngb:security:roles"
116
+ @back="goBack"
117
+ @refresh="load"
118
+ @create="createRole"
119
+ @rowActivate="openRole"
120
+ >
121
+ <template #filters>
122
+ <NgbRecycleBinFilter v-model="status" />
123
+ </template>
124
+ </NgbRegisterPageLayout>
125
+ </template>