DefaultTable.vue 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. <template>
  2. <q-table
  3. v-model:fullscreen="fullscreen"
  4. flat
  5. row-key="id"
  6. :pagination="{ rowsPerPage }"
  7. :pagination-label="getPaginationLabel"
  8. :rows-per-page-label="$t('common.ui.table.rows_per_page')"
  9. :grid="$q.screen.lt.sm"
  10. :visible-columns
  11. :filter
  12. :columns
  13. :loading
  14. :rows
  15. class="softpar-table q-pa-sm q-mt-md"
  16. @row-click="onRowClick"
  17. >
  18. <template #top>
  19. <div
  20. class="flex full-width align-center q-mb-md q-pl-sm"
  21. style="gap: 1rem"
  22. >
  23. <div v-if="title" class="column text-h6">
  24. <span>{{ title }}</span>
  25. <span class="text-body2">{{
  26. `${rows.length} ${descricao} ${feminino ? "cadastradas" : "cadastrados"}`
  27. }}</span>
  28. </div>
  29. <slot name="top" :rows="rows" />
  30. <q-space />
  31. <q-btn
  32. v-if="addItem"
  33. color="primary"
  34. style="width: 40px; height: 40px"
  35. icon="mdi-plus"
  36. :outline="outlineAdd"
  37. @click="onAddItem"
  38. >
  39. </q-btn>
  40. </div>
  41. <div class="flex full-width align-center q-mb-md" style="gap: 1rem">
  42. <DefaultInput
  43. v-if="showSearchField"
  44. v-model="filter"
  45. debounce="250"
  46. label="Busque por Unidade, status ou Responsavel"
  47. :placeholder="$t('common.actions.search')"
  48. clearable
  49. class="q-mt-sm full-width search-input"
  50. color="primary"
  51. bg-color="transparent"
  52. dense
  53. input-class="q-pl-xs"
  54. >
  55. <template #prepend>
  56. <q-icon name="mdi-magnify" color="grey-6" />
  57. </template>
  58. </DefaultInput>
  59. <DefaultSelect
  60. v-if="showColumnsSelect"
  61. v-model="visibleColumns"
  62. multiple
  63. options-outlined
  64. :display-value="$q.lang.table.columns"
  65. emit-value
  66. map-options
  67. :options="mapColumns"
  68. style="width: 150px"
  69. options-selected-class="text-bold"
  70. />
  71. </div>
  72. </template>
  73. <template #body-cell-actions="{ row }">
  74. <q-td auto-width>
  75. <q-item-section class="no-wrap" style="flex-direction: row">
  76. <slot name="body-cell-actions" :row="row" />
  77. <q-btn
  78. v-if="deleteFunction"
  79. outline
  80. icon="mdi-trash-can-outline"
  81. style="width: 36px"
  82. class="q-ml-auto q-mr-sm"
  83. @click.prevent.stop="onDelete(row.id)"
  84. />
  85. </q-item-section>
  86. </q-td>
  87. </template>
  88. <template #item="{ cols, row, index }">
  89. <div class="q-pa-xs col-xs-12 col-sm-6 col-md-4 col-lg-3">
  90. <q-card
  91. bordered
  92. flat
  93. class="q-pa-sm"
  94. @click="onRowClick($event, row, index)"
  95. >
  96. <q-list dense>
  97. <q-item
  98. v-for="col in cols.filter((col) => col.name !== 'desc')"
  99. :key="col.name"
  100. >
  101. <template v-if="col.name !== 'actions'">
  102. <q-item-section>
  103. <q-item-label caption>{{ col.label }}</q-item-label>
  104. <q-item-label>{{ col.value }}</q-item-label>
  105. </q-item-section>
  106. </template>
  107. <template v-else>
  108. <slot name="body-cell-actions" :row="row" />
  109. <q-item-section v-if="deleteFunction">
  110. <q-btn
  111. v-if="deleteFunction"
  112. outline
  113. icon="mdi-trash-can-outline"
  114. style="width: 36px"
  115. class="q-mr-sm"
  116. @click.prevent.stop="onDelete(row.id)"
  117. />
  118. </q-item-section>
  119. </template>
  120. </q-item>
  121. </q-list>
  122. </q-card>
  123. </div>
  124. </template>
  125. <template #loading>
  126. <q-inner-loading showing color="primary" />
  127. </template>
  128. <template v-if="!hideNoDataLabel" #no-data>
  129. <div v-if="!loading" class="q-my-md row justify-center full-width">
  130. <div class="q-pa-md body2">
  131. {{ $t("http.errors.no_records_found") }}
  132. </div>
  133. </div>
  134. </template>
  135. <template v-for="slotName in usableSlots($slots)" #[slotName]="data">
  136. <slot :name="slotName" v-bind="data" />
  137. </template>
  138. </q-table>
  139. </template>
  140. <script setup>
  141. import { ref, onMounted, toRaw, watch } from "vue";
  142. import { useI18n } from "vue-i18n";
  143. import { useRouter } from "vue-router";
  144. import { useQuasar } from "quasar";
  145. import DefaultInput from "./DefaultInput.vue";
  146. import DefaultSelect from "./DefaultSelect.vue";
  147. const emit = defineEmits(["onRowClick", "onAddItem", "noRows"]);
  148. const {
  149. columns,
  150. apiCall,
  151. outlineAdd,
  152. openItem,
  153. openItemRoute,
  154. addItem,
  155. addItemRoute,
  156. rowsPerPage,
  157. showSearchField,
  158. noApiCall,
  159. hideNoDataLabel,
  160. deleteFunction,
  161. } = defineProps({
  162. title: {
  163. type: String,
  164. default: null,
  165. },
  166. columns: {
  167. type: Array,
  168. required: true,
  169. },
  170. apiCall: {
  171. type: Function,
  172. default: null,
  173. },
  174. descricao: {
  175. type: String,
  176. default: "linhas",
  177. },
  178. feminino: {
  179. type: Boolean,
  180. default: true,
  181. },
  182. outlineAdd: {
  183. type: Boolean,
  184. default: false,
  185. },
  186. openItem: {
  187. type: Boolean,
  188. default: false,
  189. },
  190. openItemRoute: {
  191. type: String,
  192. default: "",
  193. },
  194. addItem: {
  195. type: Boolean,
  196. default: false,
  197. },
  198. addItemRoute: {
  199. type: String,
  200. default: "",
  201. },
  202. rowsPerPage: {
  203. type: Number,
  204. default: 10,
  205. },
  206. showSearchField: {
  207. type: Boolean,
  208. default: true,
  209. },
  210. showColumnsSelect: {
  211. type: Boolean,
  212. default: false,
  213. },
  214. noApiCall: {
  215. type: Boolean,
  216. default: false,
  217. },
  218. hideNoDataLabel: {
  219. type: Boolean,
  220. default: false,
  221. },
  222. deleteFunction: {
  223. type: Function,
  224. default: null,
  225. },
  226. });
  227. const router = useRouter();
  228. const { t } = useI18n();
  229. const $q = useQuasar();
  230. const rows = defineModel("rows", { type: Array, default: [] });
  231. const filter = ref("");
  232. const loading = ref(true);
  233. const fullscreen = ref(false);
  234. const mapColumns = columns.reduce((accm, column) => {
  235. if (!column.required) {
  236. accm.push({
  237. label: column.label,
  238. value: column.name,
  239. });
  240. }
  241. return accm;
  242. }, []);
  243. const visibleColumns = ref(mapColumns.map((column) => column.value));
  244. const getPaginationLabel = (from, to, last) => {
  245. return `${from}-${to} ${t("common.ui.table.of")} ${last}`;
  246. };
  247. const onRowClick = (evt, row, index) => {
  248. const item = toRaw(row);
  249. if (openItem) {
  250. if (openItemRoute) {
  251. router.push({ name: openItemRoute, params: { id: item.id } });
  252. } else {
  253. emit("onRowClick", { evt, row, index });
  254. }
  255. }
  256. };
  257. const onAddItem = () => {
  258. if (addItem) {
  259. if (addItemRoute) {
  260. router.push({ name: addItemRoute });
  261. } else {
  262. emit("onAddItem");
  263. }
  264. }
  265. };
  266. const onDelete = async (id) => {
  267. if (deleteFunction) {
  268. $q.dialog({
  269. title: t("common.ui.messages.confirm_action"),
  270. message: t("common.ui.messages.are_you_sure_delete"),
  271. ok: {
  272. color: "negative",
  273. label: t("common.actions.delete"),
  274. },
  275. cancel: {
  276. color: "primary",
  277. outline: true,
  278. label: t("common.actions.cancel"),
  279. },
  280. }).onOk(async () => {
  281. loading.value = true;
  282. try {
  283. await deleteFunction(id);
  284. await onRequest();
  285. } catch (error) {
  286. console.error(error);
  287. } finally {
  288. loading.value = false;
  289. }
  290. });
  291. }
  292. };
  293. const onRequest = async () => {
  294. if (noApiCall) {
  295. loading.value = false;
  296. return;
  297. }
  298. loading.value = true;
  299. const response = await apiCall();
  300. rows.value = response;
  301. if (rows.value.length == 0) {
  302. emit("noRows");
  303. }
  304. loading.value = false;
  305. };
  306. const usableSlots = (slots) => {
  307. const availableSlots = Object.keys(slots);
  308. const usableSlots = availableSlots.filter(
  309. (slot) =>
  310. !["body-cell-actions", "top", "loading", "no-data"].includes(slot),
  311. );
  312. return usableSlots;
  313. };
  314. watch(
  315. () => apiCall,
  316. async () => {
  317. await onRequest();
  318. },
  319. );
  320. onMounted(async () => {
  321. await onRequest({
  322. filter: undefined,
  323. });
  324. });
  325. defineExpose({
  326. refresh: onRequest,
  327. });
  328. </script>
  329. <style lang="scss">
  330. @import "src/css/table.scss";
  331. </style>