GestaoAssociadosPage.vue 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. <template>
  2. <div>
  3. <input
  4. ref="importFileInput"
  5. type="file"
  6. accept=".xlsx"
  7. class="hidden"
  8. @change="onFileSelected"
  9. />
  10. <DefaultHeaderPage>
  11. <template #after>
  12. <div class="flex gap-sm q-mt-md">
  13. <q-btn
  14. unelevated
  15. class="btn-gradient"
  16. :label="$t('associado.import_afastamentos')"
  17. icon="mdi-upload"
  18. padding="8px 12px"
  19. />
  20. <q-btn
  21. unelevated
  22. class="btn-gradient"
  23. :label="$t('associado.import_associados')"
  24. icon="mdi-upload"
  25. padding="8px 12px"
  26. :loading="importing"
  27. @click="onImportClick"
  28. />
  29. <q-btn
  30. unelevated
  31. class="btn-gradient"
  32. icon="mdi-clock-outline"
  33. padding="8px 12px"
  34. :title="$t('import_log.history_title')"
  35. @click="onOpenImportHistory"
  36. />
  37. <q-btn
  38. unelevated
  39. class="btn-gradient"
  40. :label="$t('common.actions.new')"
  41. icon="mdi-plus"
  42. padding="8px 12px"
  43. @click="onAddItem"
  44. />
  45. </div>
  46. </template>
  47. </DefaultHeaderPage>
  48. <DefaultTableServerSide
  49. ref="tableRef"
  50. :columns="columns"
  51. :api-call="getAssociadosPaginated"
  52. :add-item="false"
  53. open-item
  54. @on-row-click="onRowClick"
  55. >
  56. <template #body-cell-actions="{ row }">
  57. <div class="row no-wrap items-center" style="gap: 4px">
  58. <q-btn flat round dense color="violet-normal" icon="mdi-pencil" @click.stop="onRowClick({ row })" />
  59. <q-btn
  60. flat round dense
  61. :color="row.status === 'on_leave' || row.status?.value === 'on_leave' ? 'grey-4' : 'violet-normal'"
  62. icon="mdi-account-minus"
  63. :disable="row.status === 'on_leave' || row.status?.value === 'on_leave'"
  64. @click.stop="onSetOnLeave(row)"
  65. />
  66. <q-btn flat round dense color="violet-normal" icon="mdi-calendar" @click.stop />
  67. <q-btn flat round dense color="violet-normal" icon="mdi-sofa" @click.stop />
  68. </div>
  69. </template>
  70. <template #body-cell-status="{ row }">
  71. <q-td class="text-center">
  72. <q-badge
  73. outline
  74. :color="getStatusColor(row.status)"
  75. :label="$t(getStatusI18nKey(row.status))"
  76. />
  77. </q-td>
  78. </template>
  79. </DefaultTableServerSide>
  80. <q-separator class="q-my-xl" />
  81. <AccessLogsTable />
  82. </div>
  83. </template>
  84. <script setup>
  85. import { ref, computed, defineAsyncComponent, useTemplateRef } from "vue";
  86. import { useQuasar } from "quasar";
  87. import { useI18n } from "vue-i18n";
  88. import { permissionStore } from "src/stores/permission";
  89. import { getAssociadosPaginated, importAssociados, setUserOnLeave } from "src/api/user";
  90. import { getStatusColor, getStatusI18nKey } from "src/helpers/utils";
  91. import { useImportPoller } from "src/composables/useImportPoller";
  92. import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
  93. import DefaultTableServerSide from "src/components/defaults/DefaultTableServerSide.vue";
  94. import AccessLogsTable from "./components/AccessLogsTable.vue";
  95. const AddEditAssociadoDialog = defineAsyncComponent(
  96. () => import("./components/AddEditAssociadoDialog.vue"),
  97. );
  98. const ImportHistoryDialog = defineAsyncComponent(
  99. () => import("src/components/ImportHistoryDialog.vue"),
  100. );
  101. const permission_store = permissionStore();
  102. const $q = useQuasar();
  103. const { t } = useI18n();
  104. const tableRef = ref(null);
  105. const importFileInput = useTemplateRef("importFileInput");
  106. const { polling: importing, start: startPolling } = useImportPoller();
  107. const columns = computed(() => [
  108. {
  109. name: "registration",
  110. label: t("associado.cracha"),
  111. field: "registration",
  112. align: "left",
  113. sortable: true,
  114. width: "10%",
  115. },
  116. {
  117. name: "name",
  118. label: t("common.terms.name"),
  119. field: "name",
  120. align: "left",
  121. sortable: true,
  122. width: "25%",
  123. },
  124. {
  125. name: "position",
  126. label: t("associado.position"),
  127. field: (row) => row.position?.name ?? "—",
  128. align: "left",
  129. sortable: true,
  130. width: "25%",
  131. },
  132. {
  133. name: "admission_date",
  134. label: t("associado.admission"),
  135. field: (row) => (row.admission_date ? row.admission_date : "—"),
  136. align: "left",
  137. sortable: true,
  138. width: "10%",
  139. },
  140. {
  141. name: "expiry_date",
  142. label: t("associado.validity"),
  143. field: (row) => (row.expiry_date ? row.expiry_date : "—"),
  144. align: "left",
  145. sortable: true,
  146. width: "10%",
  147. },
  148. {
  149. name: "actions",
  150. label: t("common.terms.actions"),
  151. align: "right",
  152. required: true,
  153. width: "15%",
  154. },
  155. {
  156. name: "status",
  157. label: t("common.terms.status"),
  158. field: "status",
  159. align: "center",
  160. sortable: true,
  161. width: "5%",
  162. },
  163. ]);
  164. const onAddItem = async () => {
  165. if (!permission_store.getAccess("associado", "add")) {
  166. $q.notify({ type: "negative", message: t("validation.permissions.add") });
  167. return;
  168. }
  169. $q.dialog({ component: AddEditAssociadoDialog }).onOk(() => {
  170. tableRef.value?.refresh();
  171. });
  172. };
  173. const onRowClick = async ({ row }) => {
  174. if (!permission_store.getAccess("associado", "edit")) {
  175. $q.notify({ type: "negative", message: t("validation.permissions.edit") });
  176. return;
  177. }
  178. $q.dialog({
  179. component: AddEditAssociadoDialog,
  180. componentProps: {
  181. associado: row,
  182. title: () => t("common.actions.edit") + " " + t("ui.navigation.associados"),
  183. },
  184. }).onOk(() => {
  185. tableRef.value?.refresh();
  186. });
  187. };
  188. const onSetOnLeave = (row) => {
  189. if (!permission_store.getAccess("associado", "edit")) {
  190. $q.notify({ type: "negative", message: t("validation.permissions.edit") });
  191. return;
  192. }
  193. $q.dialog({
  194. title: t("associado.set_on_leave_title"),
  195. message: t("associado.set_on_leave_message", { name: row.name }),
  196. cancel: { flat: true, label: t("common.actions.cancel"), color: "grey-7" },
  197. ok: { unelevated: true, label: t("associado.set_on_leave_confirm"), color: "violet-normal" },
  198. persistent: true,
  199. }).onOk(async () => {
  200. try {
  201. await setUserOnLeave(row.id);
  202. tableRef.value?.refresh();
  203. } catch {
  204. // silent
  205. }
  206. });
  207. };
  208. const onOpenImportHistory = () => {
  209. $q.dialog({ component: ImportHistoryDialog, componentProps: { type: 'associado' } });
  210. };
  211. const onImportClick = () => {
  212. if (!permission_store.getAccess("associado", "add")) {
  213. $q.notify({ type: "negative", message: t("validation.permissions.add") });
  214. return;
  215. }
  216. importFileInput.value.value = null;
  217. importFileInput.value.click();
  218. };
  219. const onFileSelected = async (event) => {
  220. const file = event.target.files?.[0];
  221. if (!file) return;
  222. try {
  223. const { import_id } = await importAssociados(file);
  224. startPolling(import_id, {
  225. onComplete: (stats) => {
  226. $q.notify({
  227. type: "positive",
  228. message: t("associado.import_sync_result", {
  229. created: stats.created,
  230. updated: stats.updated,
  231. inactivated: stats.inactivated,
  232. }),
  233. timeout: 6000,
  234. });
  235. tableRef.value?.refresh();
  236. },
  237. onError: () => $q.notify({ type: "negative", message: t("http.errors.failed") }),
  238. onTimeout: () => $q.notify({ type: "warning", message: t("associado.import_processing") }),
  239. });
  240. } catch {
  241. $q.notify({ type: "negative", message: t("http.errors.failed") });
  242. }
  243. };
  244. </script>
  245. <style scoped>
  246. .gap-sm {
  247. gap: 8px;
  248. }
  249. .btn-gradient {
  250. background: linear-gradient(90deg, #4d1658 0%, #8b30a5 100%) !important;
  251. color: white !important;
  252. border-radius: 8px !important;
  253. }
  254. .btn-gradient :deep(.q-icon) {
  255. color: white !important;
  256. }
  257. </style>