MovimentacoesDialog.vue 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. <template>
  2. <q-dialog ref="dialogRef" maximized transition-show="slide-up" transition-hide="slide-down" @hide="onDialogHide">
  3. <q-card class="bg-page column no-wrap" style="height: 100dvh;">
  4. <div class="movements-header row items-center bg-white q-px-sm">
  5. <q-btn flat round dense icon="mdi-arrow-left" color="text" @click="onDialogCancel" />
  6. <q-space />
  7. <span class="text-subtitle1 text-weight-bold gradient-diarista">{{ $t('provider.payments.movements_title') }}</span>
  8. <q-space />
  9. <div style="width: 40px" />
  10. </div>
  11. <q-scroll-area class="col" style="flex: 1 1 0;">
  12. <div v-if="loading" class="flex flex-center q-py-xl">
  13. <q-spinner color="primary" size="40px" />
  14. </div>
  15. <div v-else class="q-pa-md">
  16. <q-card
  17. v-for="item in movements"
  18. :key="item.id"
  19. class="movement-card bg-surface shadow-card q-mb-sm"
  20. :flat="false"
  21. >
  22. <q-card-section class="q-pa-sm">
  23. <div class="row items-center no-wrap q-gutter-x-sm">
  24. <q-avatar size="40px" :color="movBgColor(item.type)" :text-color="movIconColor(item.type)">
  25. <q-icon :name="movIcon(item.type)" size="20px" />
  26. </q-avatar>
  27. <div class="col column">
  28. <span class="mov-label">{{ movLabel(item) }}</span>
  29. <span class="mov-date">{{ formatMovDate(item.created_at) }}</span>
  30. </div>
  31. <span class="mov-value" :class="item.gross_amount >= 0 ? 'text-success' : 'text-error'">
  32. {{ (item.gross_amount >= 0 ? '+' : '') + formatCurrency(Math.abs(item.gross_amount)) }}
  33. </span>
  34. </div>
  35. </q-card-section>
  36. </q-card>
  37. </div>
  38. </q-scroll-area>
  39. </q-card>
  40. </q-dialog>
  41. </template>
  42. <script setup>
  43. import { ref, onMounted, computed } from 'vue';
  44. import { useDialogPluginComponent, useQuasar } from 'quasar';
  45. import { useI18n } from 'vue-i18n';
  46. import { formatCurrency } from 'src/helpers/utils';
  47. import { getPaymentSplits } from 'src/api/paymentSplit';
  48. import { getWithdrawals } from 'src/api/providerWithdrawal';
  49. defineEmits([...useDialogPluginComponent.emits]);
  50. const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
  51. const { t } = useI18n();
  52. const $q = useQuasar();
  53. const loading = ref(true);
  54. const allMovements = ref([]);
  55. const movements = computed(() => {
  56. return [...allMovements.value].sort((a, b) =>
  57. new Date(b.created_at) - new Date(a.created_at)
  58. );
  59. });
  60. const loadMovements = async () => {
  61. loading.value = true;
  62. try {
  63. const [splits, withdrawals] = await Promise.all([
  64. getPaymentSplits(),
  65. getWithdrawals(),
  66. ]);
  67. const serviceItems = (splits || []).map((s) => ({
  68. id: `split-${s.id}`,
  69. type: 'servico',
  70. client_name: s.client_name,
  71. gross_amount: parseFloat(s.gross_amount || 0),
  72. created_at: s.created_at,
  73. }));
  74. const withdrawalItems = (withdrawals || []).map((w) => ({
  75. id: `withdrawal-${w.id}`,
  76. type: 'saque',
  77. gross_amount: -(parseFloat(w.gross_amount || 0)),
  78. created_at: w.created_at,
  79. }));
  80. allMovements.value = [...serviceItems, ...withdrawalItems];
  81. } catch (error) {
  82. $q.notify({ type: 'negative', message: error?.response?.data?.message || error.message });
  83. } finally {
  84. loading.value = false;
  85. }
  86. };
  87. const movIcon = (type) => {
  88. const map = { saque: 'mdi-bank-transfer-out', servico: 'mdi-broom' };
  89. return map[type] ?? 'mdi-circle';
  90. };
  91. const movBgColor = (type) => {
  92. const map = { saque: 'secondary-bg', servico: 'success-bg' };
  93. return map[type] ?? 'neutral-bg';
  94. };
  95. const movIconColor = (type) => {
  96. const map = { saque: 'secondary', servico: 'success' };
  97. return map[type] ?? 'text';
  98. };
  99. const movLabel = (item) => {
  100. if (item.type === 'servico') return t('provider.payments.mov_servico') + (item.client_name ? ` - ${item.client_name}` : '');
  101. if (item.type === 'saque') return t('provider.payments.mov_saque');
  102. return '';
  103. };
  104. const formatMovDate = (dateStr) => {
  105. if (!dateStr) return '';
  106. try { return new Date(dateStr).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric' }); }
  107. catch { return dateStr; }
  108. };
  109. onMounted(() => {
  110. loadMovements();
  111. });
  112. </script>
  113. <style scoped lang="scss">
  114. .movements-header {
  115. padding-top: calc(env(safe-area-inset-top) + 12px);
  116. padding-bottom: 12px;
  117. min-height: calc(50px + env(safe-area-inset-top));
  118. box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.08);
  119. }
  120. .movement-card {
  121. border-radius: 12px;
  122. }
  123. .mov-label {
  124. font-size: 13px;
  125. font-weight: 600;
  126. color: #3a3a4a;
  127. }
  128. .mov-date {
  129. font-size: 11px;
  130. color: #888;
  131. margin-top: 2px;
  132. }
  133. .mov-value {
  134. font-size: 14px;
  135. font-weight: 700;
  136. white-space: nowrap;
  137. }
  138. </style>