ParceirosConveniosPage.vue 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. <template>
  2. <div class="parceiros-page">
  3. <input
  4. ref="importParceiroInput"
  5. type="file"
  6. accept=".xlsx"
  7. class="hidden"
  8. @change="onParceiroFileSelected"
  9. />
  10. <DefaultHeaderPage>
  11. <template #after>
  12. <div v-if="isAdministrador" class="flex gap-sm q-mt-md">
  13. <q-btn
  14. unelevated
  15. class="btn-gradient"
  16. :label="$t('parceiro.import_parceiros')"
  17. icon="mdi-upload"
  18. padding="8px 12px"
  19. :loading="importingParceiros"
  20. @click="onImportParceirosClick"
  21. />
  22. <q-btn
  23. unelevated
  24. class="btn-gradient"
  25. icon="mdi-clock-outline"
  26. padding="8px 12px"
  27. :title="$t('import_log.history_title')"
  28. @click="onOpenImportHistory"
  29. />
  30. <q-btn
  31. unelevated
  32. class="btn-gradient"
  33. :label="$t('common.actions.add')"
  34. icon="mdi-plus"
  35. padding="8px 12px"
  36. @click="onAddItem"
  37. />
  38. </div>
  39. </template>
  40. </DefaultHeaderPage>
  41. <div class="q-pb-sm">
  42. <q-input
  43. v-model="searchQuery"
  44. outlined
  45. dense
  46. bg-color="white"
  47. :placeholder="$t('parceiro.search_placeholder')"
  48. clearable
  49. class="parceiros-page__search-input"
  50. >
  51. <template #prepend>
  52. <q-icon name="mdi-magnify" />
  53. </template>
  54. </q-input>
  55. </div>
  56. <div class="row q-gutter-xs q-mb-md flex-wrap">
  57. <div
  58. :class="['cat-chip', activeCategory === 'all' ? 'cat-chip--selected' : 'cat-chip--default']"
  59. @click="activeCategory = 'all'"
  60. >
  61. {{ $t('common.terms.all') }}
  62. </div>
  63. <div
  64. v-for="cat in categories"
  65. :key="cat.id"
  66. :class="['cat-chip', activeCategory === String(cat.id) ? 'cat-chip--selected' : 'cat-chip--default']"
  67. @click="activeCategory = String(cat.id)"
  68. >
  69. {{ cat.name }}
  70. </div>
  71. </div>
  72. <div v-if="loading" class="flex flex-center q-pa-xl">
  73. <q-spinner color="violet-normal" size="50px" />
  74. </div>
  75. <div v-else-if="filteredPartners.length === 0" class="flex flex-center q-pa-xl text-grey-6">
  76. {{ $t("http.errors.no_records_found") }}
  77. </div>
  78. <div v-else class="row q-col-gutter-md">
  79. <div
  80. v-for="partner in filteredPartners"
  81. :key="partner.id"
  82. class="col-xl-2 col-lg-3 col-md-4 col-sm-6 col-12"
  83. >
  84. <PartnerAgreementCard
  85. :partner="partner"
  86. :editable="isAdministrador"
  87. @edit="onEditItem"
  88. />
  89. </div>
  90. </div>
  91. </div>
  92. </template>
  93. <script setup>
  94. import { ref, computed, onMounted, useTemplateRef, defineAsyncComponent } from "vue";
  95. import { useRouter } from "vue-router";
  96. import { useQuasar } from "quasar";
  97. import { useI18n } from "vue-i18n";
  98. import { permissionStore } from "src/stores/permission";
  99. import { userStore } from "src/stores/user";
  100. import { getPartnerAgreements, importParceiros } from "src/api/partnerAgreement";
  101. import { getCategories } from "src/api/category";
  102. import { normalizeString } from "src/helpers/utils";
  103. import { useImportPoller } from "src/composables/useImportPoller";
  104. const ImportHistoryDialog = defineAsyncComponent(
  105. () => import("src/components/ImportHistoryDialog.vue"),
  106. );
  107. import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
  108. import PartnerAgreementCard from "./components/PartnerAgreementCard.vue";
  109. const router = useRouter();
  110. const $q = useQuasar();
  111. const { t } = useI18n();
  112. const permission_store = permissionStore();
  113. const { isAdministrador } = userStore();
  114. const loading = ref(true);
  115. const allPartners = ref([]);
  116. const categories = ref([]);
  117. const activeCategory = ref("all");
  118. const searchQuery = ref("");
  119. const importParceiroInput = useTemplateRef("importParceiroInput");
  120. const { polling: importingParceiros, start: startParceiroPolling } = useImportPoller();
  121. const filteredPartners = computed(() => {
  122. let list = allPartners.value;
  123. if (activeCategory.value !== "all") {
  124. list = list.filter((p) => String(p.category_id) === activeCategory.value);
  125. }
  126. if (searchQuery.value) {
  127. const needle = normalizeString(searchQuery.value);
  128. list = list.filter((p) => {
  129. const fields = [p.company_name, p.cnpj, p.responsible, p.email, p.phone, p.category?.name, p.address, p.city?.name];
  130. return fields.some((f) => f && normalizeString(String(f)).includes(needle));
  131. });
  132. }
  133. return list;
  134. });
  135. const onAddItem = () => {
  136. if (!permission_store.getAccess("parceiro.convenio", "add")) {
  137. $q.notify({ type: "negative", message: t("validation.permissions.add") });
  138. return;
  139. }
  140. router.push({ name: "ParceiroCadastroPage" });
  141. };
  142. const onEditItem = (partner) => {
  143. if (!permission_store.getAccess("parceiro.convenio", "edit")) {
  144. $q.notify({ type: "negative", message: t("validation.permissions.edit") });
  145. return;
  146. }
  147. router.push({ name: "ParceiroCadastroPage", params: { id: partner.id } });
  148. };
  149. const loadPartners = async () => {
  150. const [partners, cats] = await Promise.all([
  151. getPartnerAgreements({ type: "partner" }),
  152. getCategories("partner"),
  153. ]);
  154. allPartners.value = partners;
  155. categories.value = cats;
  156. };
  157. const onOpenImportHistory = () => {
  158. $q.dialog({ component: ImportHistoryDialog, componentProps: { type: 'parceiro' } });
  159. };
  160. const onImportParceirosClick = () => {
  161. if (!permission_store.getAccess("parceiro.convenio", "add")) {
  162. $q.notify({ type: "negative", message: t("validation.permissions.add") });
  163. return;
  164. }
  165. importParceiroInput.value.value = null;
  166. importParceiroInput.value.click();
  167. };
  168. const onParceiroFileSelected = async (event) => {
  169. const file = event.target.files?.[0];
  170. if (!file) return;
  171. try {
  172. const { import_id } = await importParceiros(file);
  173. startParceiroPolling(import_id, {
  174. onComplete: async (stats) => {
  175. $q.notify({
  176. type: "positive",
  177. message: t("parceiro.import_parceiros_result", { created: stats.created, updated: stats.updated, inactivated: stats.inactivated }),
  178. timeout: 7000,
  179. });
  180. await loadPartners();
  181. },
  182. onError: () => $q.notify({ type: "negative", message: t("http.errors.failed") }),
  183. onTimeout: () => $q.notify({ type: "warning", message: t("parceiro.import_processing") }),
  184. });
  185. } catch {
  186. $q.notify({ type: "negative", message: t("http.errors.failed") });
  187. }
  188. };
  189. onMounted(async () => {
  190. try { await loadPartners(); }
  191. finally { loading.value = false; }
  192. });
  193. </script>
  194. <style scoped>
  195. .gap-sm { gap: 8px; }
  196. .parceiros-page { min-height: 100vh; }
  197. .parceiros-page__search-input { max-width: 700px; width: 100%; }
  198. .cat-chip {
  199. display: inline-flex; align-items: center; justify-content: center;
  200. height: 28px; padding: 0 12px; border-radius: 5px;
  201. font-size: 12px; font-weight: 500; cursor: pointer;
  202. user-select: none; transition: background 0.15s, color 0.15s;
  203. }
  204. .cat-chip--default { background: #c9a3dc; color: #fff; }
  205. .cat-chip--selected { background: #4d1658; color: #fff; }
  206. .btn-gradient {
  207. background: linear-gradient(90deg, #4d1658 0%, #8b30a5 100%) !important;
  208. color: white !important; border-radius: 8px !important;
  209. }
  210. .btn-gradient :deep(.q-icon) { color: white !important; }
  211. </style>