PermissionGroupDialog.vue 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. <template>
  2. <q-dialog ref="dialogRef" full-height @hide="onDialogHide">
  3. <q-card
  4. class="column no-wrap"
  5. style="width: 100%; max-width: 95vw; height: 100%"
  6. >
  7. <DefaultDialogHeader
  8. :title="`Permissões: ${group.label}`"
  9. @close="onDialogCancel"
  10. />
  11. <q-card-section class="col column no-wrap q-pa-none" style="min-height: 0">
  12. <div class="row items-stretch col no-wrap" style="min-height: 0">
  13. <!-- Menu -->
  14. <div class="col-12 col-md-6 column no-wrap" style="min-height: 0">
  15. <div class="text-center text-subtitle1 text-weight-medium q-pa-sm">
  16. Menu
  17. </div>
  18. <q-separator />
  19. <q-inner-loading :showing="loading" />
  20. <q-list v-if="!loading" separator class="col scroll">
  21. <q-item
  22. v-for="item in menus"
  23. :key="item.scope"
  24. clickable
  25. :active="selectedScope === item.scope"
  26. active-class="bg-blue-1 text-primary"
  27. @click="selectedScope = item.scope"
  28. >
  29. <q-item-section avatar>
  30. <q-icon :name="item.icon" color="dark" />
  31. </q-item-section>
  32. <q-item-section>
  33. <q-item-label>
  34. {{ item.label }}
  35. <q-icon
  36. v-if="item.children?.length"
  37. name="mdi-chevron-right"
  38. size="16px"
  39. class="q-ml-xs"
  40. />
  41. </q-item-label>
  42. <q-item-label caption>
  43. <PermissionLevelBadges :bits="bitsFor(item.scope)" />
  44. </q-item-label>
  45. </q-item-section>
  46. <q-item-section side>
  47. <q-btn
  48. flat
  49. round
  50. dense
  51. icon="mdi-pencil-outline"
  52. color="dark"
  53. @click.stop
  54. >
  55. <q-tooltip>Editar permissões</q-tooltip>
  56. <PermissionLevelMenu
  57. :bits="bitsFor(item.scope)"
  58. @update:bits="(value) => setBits(item.scope, value)"
  59. />
  60. </q-btn>
  61. </q-item-section>
  62. </q-item>
  63. </q-list>
  64. </div>
  65. <q-separator vertical />
  66. <!-- SubCategorias -->
  67. <div class="col column no-wrap" style="min-height: 0">
  68. <div class="text-center text-subtitle1 text-weight-medium q-pa-sm">
  69. SubCategorias
  70. </div>
  71. <q-separator />
  72. <div
  73. v-if="!selected"
  74. class="col column flex-center text-center text-foreground q-pa-md"
  75. >
  76. <div>Nenhum menu selecionado!</div>
  77. <div class="text-caption">
  78. ← Selecione um menu à esquerda para carregar as subcategorias.
  79. </div>
  80. </div>
  81. <div
  82. v-else-if="!children.length"
  83. class="col column flex-center text-center text-foreground q-pa-md"
  84. >
  85. <div>Nenhuma subcategoria</div>
  86. <div class="text-caption">
  87. O menu "{{ selected.label }}" não possui subcategorias.
  88. </div>
  89. </div>
  90. <q-list v-else separator class="col scroll">
  91. <q-item v-for="child in children" :key="child.scope">
  92. <q-item-section avatar>
  93. <q-icon :name="child.icon" color="dark" />
  94. </q-item-section>
  95. <q-item-section>
  96. <q-item-label>{{ child.label }}</q-item-label>
  97. <q-item-label caption>
  98. <PermissionLevelBadges :bits="bitsFor(child.scope)" />
  99. </q-item-label>
  100. </q-item-section>
  101. <q-item-section side>
  102. <q-btn
  103. flat
  104. round
  105. dense
  106. icon="mdi-pencil-outline"
  107. color="dark"
  108. >
  109. <q-tooltip>Editar permissões</q-tooltip>
  110. <PermissionLevelMenu
  111. :bits="bitsFor(child.scope)"
  112. @update:bits="(value) => setBits(child.scope, value)"
  113. />
  114. </q-btn>
  115. </q-item-section>
  116. </q-item>
  117. </q-list>
  118. </div>
  119. </div>
  120. </q-card-section>
  121. <q-separator />
  122. <q-card-actions align="right" class="col-auto q-pa-md">
  123. <q-btn
  124. label="Cancelar"
  125. color="primary"
  126. outline
  127. @click="onDialogCancel"
  128. />
  129. <q-btn
  130. label="Salvar"
  131. color="primary"
  132. :loading="saving"
  133. :disable="loading"
  134. @click="onSave"
  135. />
  136. </q-card-actions>
  137. </q-card>
  138. </q-dialog>
  139. </template>
  140. <script setup>
  141. import { computed, onMounted, ref } from "vue";
  142. import { Notify, useDialogPluginComponent } from "quasar";
  143. import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
  144. import PermissionLevelBadges from "src/pages/permissions/components/PermissionLevelBadges.vue";
  145. import PermissionLevelMenu from "src/pages/permissions/components/PermissionLevelMenu.vue";
  146. import {
  147. getUserTypePermissions,
  148. updateUserTypePermissions,
  149. } from "src/api/user_type";
  150. import {
  151. FRANCHISOR_PERMISSION_MENUS,
  152. applyComputedLevels,
  153. } from "src/pages/permissions/permission_menus";
  154. const { group } = defineProps({
  155. group: {
  156. type: Object,
  157. required: true,
  158. },
  159. });
  160. defineEmits([...useDialogPluginComponent.emits]);
  161. const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
  162. useDialogPluginComponent();
  163. const loading = ref(false);
  164. const saving = ref(false);
  165. const selectedScope = ref(null);
  166. // permission_id e bits atuais do grupo, indexados pelo scope do menu.
  167. const permissionIdByScope = ref({});
  168. const bitsByScope = ref({});
  169. const menus = computed(() =>
  170. FRANCHISOR_PERMISSION_MENUS.filter(
  171. (menu) => permissionIdByScope.value[menu.scope] !== undefined,
  172. ),
  173. );
  174. const selected = computed(
  175. () => menus.value.find((menu) => menu.scope === selectedScope.value) ?? null,
  176. );
  177. const children = computed(() =>
  178. (selected.value?.children ?? []).filter(
  179. (child) => permissionIdByScope.value[child.scope] !== undefined,
  180. ),
  181. );
  182. const bitsFor = (scope) => bitsByScope.value[scope] ?? 0;
  183. const setBits = (scope, value) => {
  184. bitsByScope.value[scope] = value;
  185. };
  186. const scopesInMenu = () =>
  187. FRANCHISOR_PERMISSION_MENUS.flatMap((menu) => [
  188. menu.scope,
  189. ...(menu.children ?? []).map((child) => child.scope),
  190. ]);
  191. const fetchPermissions = async () => {
  192. loading.value = true;
  193. try {
  194. const payload = await getUserTypePermissions(group.slug);
  195. const byScope = Object.fromEntries(
  196. payload.permissions.map((p) => [p.scope, p]),
  197. );
  198. permissionIdByScope.value = {};
  199. bitsByScope.value = {};
  200. for (const scope of scopesInMenu()) {
  201. const permission = byScope[scope];
  202. if (!permission) continue;
  203. permissionIdByScope.value[scope] = permission.id;
  204. // Normaliza dados anteriores à regra (ex.: Visualizar gravado sem Menu).
  205. bitsByScope.value[scope] = applyComputedLevels(permission.bits);
  206. }
  207. } catch {
  208. Notify.create({
  209. message: "Não foi possível carregar as permissões do grupo.",
  210. type: "negative",
  211. });
  212. } finally {
  213. loading.value = false;
  214. }
  215. };
  216. const onSave = async () => {
  217. saving.value = true;
  218. try {
  219. await updateUserTypePermissions(
  220. group.slug,
  221. Object.entries(permissionIdByScope.value).map(([scope, id]) => ({
  222. permission_id: id,
  223. bits: bitsByScope.value[scope] ?? 0,
  224. })),
  225. );
  226. Notify.create({ message: "Permissões atualizadas.", type: "positive" });
  227. onDialogOK();
  228. } catch {
  229. Notify.create({
  230. message: "Não foi possível salvar as permissões.",
  231. type: "negative",
  232. });
  233. } finally {
  234. saving.value = false;
  235. }
  236. };
  237. onMounted(fetchPermissions);
  238. </script>