DefaultTable.vue 8.2 KB

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