DefaultTable.vue 9.6 KB

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