Bladeren bron

style: :lipstick: style (pagamentos) criado layout da tela de pagamentos

foi criado o layout da tela de pagamentos do prestador (somente visual, sem funcionamento)

fase:dev | origin:escopo
Gustavo Zanatta 1 maand geleden
bovenliggende
commit
adb9920df2

+ 52 - 0
src/components/payments/AntecipacaoConfirmDialog.vue

@@ -0,0 +1,52 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="confirm-card bg-surface" :flat="false">
+      <div class="row justify-end q-pt-sm q-pr-sm">
+        <q-btn flat round dense icon="close" color="grey-6" size="sm" @click="onDialogCancel" />
+      </div>
+
+      <q-card-section class="column items-center q-pt-xs q-pb-sm q-px-lg">
+        <div class="text-body1 text-weight-bold text-text text-center">
+          {{ $t('provider.payments.anticipation_confirm_title') }}
+        </div>
+      </q-card-section>
+
+      <q-card-section class="row q-gutter-x-sm q-pt-xs q-pb-lg q-px-lg">
+        <q-btn
+          unelevated
+          rounded
+          no-caps
+          color="grey-5"
+          text-color="white"
+          class="col"
+          :label="$t('provider.payments.anticipation_confirm_cancel')"
+          @click="onDialogCancel"
+        />
+        <q-btn
+          unelevated
+          rounded
+          no-caps
+          color="primary"
+          class="col"
+          :label="$t('provider.payments.anticipation_confirm_ok')"
+          @click="onDialogOK()"
+        />
+      </q-card-section>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { useDialogPluginComponent } from 'quasar';
+
+defineEmits([...useDialogPluginComponent.emits]);
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+</script>
+
+<style scoped>
+.confirm-card {
+  width: 300px;
+  max-width: 96vw;
+  border-radius: 20px !important;
+}
+</style>

+ 104 - 0
src/components/payments/AntecipacaoDialog.vue

@@ -0,0 +1,104 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="anticipation-card bg-surface" :flat="false">
+      <div class="row justify-end q-pt-sm q-pr-sm">
+        <q-btn flat round dense icon="close" color="grey-6" size="sm" @click="onDialogCancel" />
+      </div>
+
+      <q-card-section class="q-pt-xs q-pb-xs q-px-lg">
+        <div class="text-center q-mb-xs">
+          <span class="text-body2 text-grey-7">{{ $t('provider.payments.anticipation_available') }}</span>
+          <span class="text-body2 text-weight-bold text-secondary q-ml-xs">{{ formatCurrency(service.total_amount) }}</span>
+        </div>
+
+        <div class="text-center q-mb-sm">
+          <span class="text-body2 text-grey-7">{{ $t('provider.payments.anticipation_fee_label', { percent: ANTICIPATION_FEE_PERCENT }) }}</span>
+          <span class="text-body2 text-weight-bold text-secondary q-ml-xs">{{ '-' + formatCurrency(fee) }}</span>
+        </div>
+
+        <q-separator class="q-my-sm" />
+
+        <div class="text-body2 text-grey-7 text-center q-mb-xs">
+          {{ $t('provider.payments.anticipation_total_label') }}
+        </div>
+        <div class="anticipation-total gradient-diarista text-center q-mb-md">
+          {{ formatCurrency(totalAfterFee) }}
+        </div>
+
+        <div class="info-note row items-start q-gutter-x-xs q-mb-sm">
+          <q-icon name="mdi-alert-outline" color="primary" size="16px" class="q-mt-xxs" />
+          <span class="text-caption text-grey-7 col">{{ $t('provider.payments.anticipation_note') }}</span>
+        </div>
+      </q-card-section>
+
+      <q-card-section class="row q-gutter-x-sm q-pt-xs q-pb-sm q-px-lg">
+        <q-btn
+          unelevated
+          rounded
+          no-caps
+          color="grey-4"
+          text-color="grey-8"
+          class="col"
+          :label="$t('provider.payments.anticipation_btn_close')"
+          @click="onDialogCancel"
+        />
+        <q-btn
+          unelevated
+          rounded
+          no-caps
+          color="secondary"
+          class="col"
+          :label="$t('provider.payments.anticipation_btn_anticipate')"
+          @click="onDialogOK()"
+        />
+      </q-card-section>
+
+      <q-card-section class="q-pt-xs q-pb-lg q-px-lg">
+        <div class="text-caption text-grey-6 text-center">
+          <strong>{{ $t('provider.payments.anticipation_remind_label') }}</strong>
+          {{ ' ' + $t('provider.payments.anticipation_reminder') }}
+        </div>
+      </q-card-section>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { computed } from 'vue';
+import { useDialogPluginComponent } from 'quasar';
+import { formatCurrency } from 'src/helpers/utils';
+
+const props = defineProps({
+  service: {
+    type: Object,
+    required: true,
+  },
+});
+
+defineEmits([...useDialogPluginComponent.emits]);
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+
+const ANTICIPATION_FEE_PERCENT = 3.5;
+const fee = computed(() => +(props.service.total_amount * ANTICIPATION_FEE_PERCENT / 100).toFixed(2));
+const totalAfterFee = computed(() => +(props.service.total_amount - fee.value).toFixed(2));
+</script>
+
+<style scoped>
+.anticipation-card {
+  width: 320px;
+  max-width: 96vw;
+  border-radius: 20px !important;
+}
+
+.anticipation-total {
+  font-size: 28px;
+  font-weight: 700;
+  line-height: 1.2;
+}
+
+.info-note {
+  background: #EFF6FF;
+  border-radius: 8px;
+  padding: 8px 10px;
+}
+</style>

+ 128 - 0
src/components/payments/MovimentacoesDialog.vue

@@ -0,0 +1,128 @@
+<template>
+  <q-dialog ref="dialogRef" maximized transition-show="slide-up" transition-hide="slide-down" @hide="onDialogHide">
+    <q-card class="bg-page column no-wrap" style="height: 100dvh;">
+      <div class="movements-header row items-center bg-white q-px-sm">
+        <q-btn flat round dense icon="mdi-arrow-left" color="text" @click="onDialogCancel" />
+        <q-space />
+        <span class="text-subtitle1 text-weight-bold gradient-diarista">{{ $t('provider.payments.movements_title') }}</span>
+        <q-space />
+        <div style="width: 40px" />
+      </div>
+
+      <q-scroll-area class="col" style="flex: 1 1 0;">
+        <div class="q-pa-md">
+          <q-card
+            v-for="item in movements"
+            :key="item.id"
+            class="movement-card bg-surface shadow-card q-mb-sm"
+            :flat="false"
+          >
+            <q-card-section class="q-pa-sm">
+              <div class="row items-center no-wrap q-gutter-x-sm">
+                <q-avatar size="40px" :color="movBgColor(item.type)" :text-color="movIconColor(item.type)">
+                  <q-icon :name="movIcon(item.type)" size="20px" />
+                </q-avatar>
+                <div class="col column">
+                  <span class="mov-label">{{ movLabel(item.type, item.description) }}</span>
+                  <span v-if="item.type === 'servico' && item.service_id" class="mov-code">
+                    {{ $t('provider.payments.mov_service_code', { id: item.service_id }) }}
+                  </span>
+                  <span class="mov-date">{{ formatMovDate(item.date) }}</span>
+                </div>
+                <span class="mov-value" :class="item.value >= 0 ? 'text-success' : 'text-error'">
+                  {{ (item.value >= 0 ? '+' : '') + formatCurrency(Math.abs(item.value)) }}
+                </span>
+              </div>
+            </q-card-section>
+          </q-card>
+        </div>
+      </q-scroll-area>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { useDialogPluginComponent } from 'quasar';
+import { useI18n } from 'vue-i18n';
+import { formatCurrency } from 'src/helpers/utils';
+
+defineEmits([...useDialogPluginComponent.emits]);
+const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
+const { t } = useI18n();
+
+const movements = [
+  { id: 1, type: 'servico', description: 'Maria Silva',  service_id: 1001, date: '2026-05-10', value:  220.00 },
+  { id: 2, type: 'tarifa',  description: '',             service_id: null, date: '2026-05-10', value:  -22.00 },
+  { id: 3, type: 'servico', description: 'Carlos Lima',  service_id: 1002, date: '2026-05-08', value:  160.00 },
+  { id: 4, type: 'tarifa',  description: '',             service_id: null, date: '2026-05-08', value:  -16.00 },
+  { id: 5, type: 'saque',   description: '',             service_id: null, date: '2026-05-07', value: -300.00 },
+  { id: 6, type: 'servico', description: 'Ana Costa',    service_id: 1003, date: '2026-05-05', value:  100.00 },
+  { id: 7, type: 'tarifa',  description: '',             service_id: null, date: '2026-05-05', value:  -10.00 },
+  { id: 8, type: 'servico', description: 'Pedro Mendes', service_id: 1004, date: '2026-05-01', value:   80.00 },
+];
+
+const movIcon = (type) => {
+  const map = { tarifa: 'mdi-percent-outline', saque: 'mdi-bank-transfer-out', servico: 'mdi-broom' };
+  return map[type] ?? 'mdi-circle';
+};
+
+const movBgColor = (type) => {
+  const map = { tarifa: 'warning-bg', saque: 'secondary-bg', servico: 'success-bg' };
+  return map[type] ?? 'neutral-bg';
+};
+
+const movIconColor = (type) => {
+  const map = { tarifa: 'warning', saque: 'secondary', servico: 'success' };
+  return map[type] ?? 'text';
+};
+
+const movLabel = (type, description) => {
+  if (type === 'servico') return t('provider.payments.mov_servico') + (description ? ` - ${description}` : '');
+  if (type === 'tarifa') return t('provider.payments.mov_tarifa');
+  if (type === 'saque') return t('provider.payments.mov_saque');
+  return description;
+};
+
+const formatMovDate = (dateStr) => {
+  if (!dateStr) return '';
+  const [y, m, d] = dateStr.split('-');
+  return new Date(+y, +m - 1, +d).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric' });
+};
+</script>
+
+<style scoped lang="scss">
+.movements-header {
+  padding-top: calc(env(safe-area-inset-top) + 12px);
+  padding-bottom: 12px;
+  min-height: calc(50px + env(safe-area-inset-top));
+  box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.08);
+}
+
+.movement-card {
+  border-radius: 12px;
+}
+
+.mov-label {
+  font-size: 13px;
+  font-weight: 600;
+  color: #3a3a4a;
+}
+
+.mov-code {
+  font-size: 10px;
+  color: #888;
+  margin-top: 1px;
+}
+
+.mov-date {
+  font-size: 11px;
+  color: #888;
+  margin-top: 2px;
+}
+
+.mov-value {
+  font-size: 14px;
+  font-weight: 700;
+  white-space: nowrap;
+}
+</style>

+ 5 - 1
src/css/quasar.variables.scss

@@ -101,8 +101,12 @@ $colors: (
   // Light Pink background
   "neutral-bg": #F3F4F6,
   // Light Grey background
-  "status-finished": #9CA3AF
+  "status-finished": #9CA3AF,
   // Grey text for finished status
+
+  // Teal — Pago antecipado chip
+  "teal-bg": #CCFBF1,
+  "teal":    #0D9488
 );
 
 // Standalone status chip variables (usable in component scoped SCSS)

+ 41 - 0
src/i18n/locales/en.json

@@ -376,6 +376,47 @@
         "empty_upcoming": "No scheduled services",
         "empty_completed": "No completed services"
       }
+    },
+    "payments": {
+      "title": "Payments",
+      "total_earnings_title": "View total earnings",
+      "total_earnings_count": "{count} services completed",
+      "total_earnings_days": "Referring to the last {days} days",
+      "period_week": "week",
+      "period_month": "month",
+      "period_year": "year",
+      "available_balance_label": "Available balance",
+      "available_balance_prefix": "Values are available for withdrawal within",
+      "available_balance_highlight": "5 days after service completion.",
+      "btn_withdraw": "withdraw",
+      "pending_balance_label": "Balance to be released",
+      "pending_balance_desc": "Amount from services awaiting release.",
+      "pending_balance_empty": "No pending payments.",
+      "services_title": "Services",
+      "btn_view_movements": "view movements",
+      "services_date_service": "Service date:",
+      "services_date_payment": "Payment date:",
+      "pay_status_pending": "Pending",
+      "pay_status_paid": "Paid",
+      "pay_status_anticipated": "Anticipated payment",
+      "pay_status_cancelled": "Cancelled service",
+      "btn_anticipate": "anticipate",
+      "movements_title": "Movements",
+      "mov_tarifa": "Platform fee",
+      "mov_saque": "Withdrawal",
+      "mov_servico": "Service",
+      "mov_service_code": "Service code: {id}",
+      "anticipation_available": "Total available:",
+      "anticipation_fee_label": "Anticipation fee ({percent}%)*:",
+      "anticipation_total_label": "Total to be paid using anticipation:",
+      "anticipation_note": "*The anticipation fee is charged by the payment system. The Diária app has no participation in this operation.",
+      "anticipation_remind_label": "Remember:",
+      "anticipation_reminder": "You can wait for the expected payment date for this service and avoid the fee charged by anticipation.",
+      "anticipation_btn_close": "close",
+      "anticipation_btn_anticipate": "anticipate",
+      "anticipation_confirm_title": "Are you sure you want to anticipate this payment?",
+      "anticipation_confirm_cancel": "cancel",
+      "anticipation_confirm_ok": "confirm"
     }
   },
   "business": {

+ 41 - 0
src/i18n/locales/es.json

@@ -374,6 +374,47 @@
         "empty_upcoming": "Sin servicios programados",
         "empty_completed": "Sin servicios completados"
       }
+    },
+    "payments": {
+      "title": "Pagos",
+      "total_earnings_title": "Ver ganancias totales",
+      "total_earnings_count": "{count} servicios realizados",
+      "total_earnings_days": "Referente a los últimos {days} días",
+      "period_week": "semana",
+      "period_month": "mes",
+      "period_year": "año",
+      "available_balance_label": "Saldo disponible",
+      "available_balance_prefix": "Los valores están disponibles para retiro en hasta",
+      "available_balance_highlight": "5 días después de la conclusión del servicio.",
+      "btn_withdraw": "retirar",
+      "pending_balance_label": "Saldo a liberar",
+      "pending_balance_desc": "Monto de servicios esperando liberación.",
+      "pending_balance_empty": "Ningún pago pendiente.",
+      "services_title": "Servicios",
+      "btn_view_movements": "ver movimientos",
+      "services_date_service": "Fecha del servicio:",
+      "services_date_payment": "Fecha de pago:",
+      "pay_status_pending": "Pendiente",
+      "pay_status_paid": "Pagado",
+      "pay_status_anticipated": "Pago anticipado",
+      "pay_status_cancelled": "Servicio cancelado",
+      "btn_anticipate": "anticipar",
+      "movements_title": "Movimientos",
+      "mov_tarifa": "Tarifa de plataforma",
+      "mov_saque": "Retiro",
+      "mov_servico": "Servicio",
+      "mov_service_code": "Cód. del servicio: {id}",
+      "anticipation_available": "Total disponible:",
+      "anticipation_fee_label": "Tasa de anticipación ({percent}%)*:",
+      "anticipation_total_label": "Total a pagar usando la anticipación:",
+      "anticipation_note": "*La tasa de anticipación es cobrada por el sistema de pago. La aplicación Diária no participa en esta operación.",
+      "anticipation_remind_label": "Recuerda:",
+      "anticipation_reminder": "Puede esperar la fecha de pago prevista para este servicio y evitar la tarifa cobrada por la anticipación.",
+      "anticipation_btn_close": "cerrar",
+      "anticipation_btn_anticipate": "anticipar",
+      "anticipation_confirm_title": "¿Está seguro de que desea anticipar este pago?",
+      "anticipation_confirm_cancel": "cancelar",
+      "anticipation_confirm_ok": "confirmar"
     }
   },
   "business": {

+ 41 - 0
src/i18n/locales/pt.json

@@ -388,6 +388,47 @@
         "empty_upcoming": "Nenhum serviço agendado",
         "empty_completed": "Nenhum serviço concluído"
       }
+    },
+    "payments": {
+      "title": "Pagamentos",
+      "total_earnings_title": "Ver ganhos totais",
+      "total_earnings_count": "{count} serviços realizados",
+      "total_earnings_days": "Valores referentes aos últimos {days} dias",
+      "period_week": "semana",
+      "period_month": "mês",
+      "period_year": "ano",
+      "available_balance_label": "Saldo disponível",
+      "available_balance_prefix": "Os valores ficam disponíveis para saque em até",
+      "available_balance_highlight": "5 dias após a conclusão do serviço.",
+      "btn_withdraw": "sacar",
+      "pending_balance_label": "Saldo a liberar",
+      "pending_balance_desc": "Valor de serviços aguardando liberação.",
+      "pending_balance_empty": "Nenhum pagamento pendente.",
+      "services_title": "Serviços",
+      "btn_view_movements": "ver movimentações",
+      "services_date_service": "Data do serviço:",
+      "services_date_payment": "Data do pagamento:",
+      "pay_status_pending": "Pendente",
+      "pay_status_paid": "Pago",
+      "pay_status_anticipated": "Pago antecipado",
+      "pay_status_cancelled": "Serviço cancelado",
+      "btn_anticipate": "antecipar",
+      "movements_title": "Movimentações",
+      "mov_tarifa": "Tarifa da plataforma",
+      "mov_saque": "Saque",
+      "mov_servico": "Serviço",
+      "mov_service_code": "Cód. do serviço: {id}",
+      "anticipation_available": "Total disponível:",
+      "anticipation_fee_label": "Taxa de antecipação ({percent}%)*:",
+      "anticipation_total_label": "Total a ser pago utilizando a antecipação:",
+      "anticipation_note": "*A taxa de antecipação é cobrada pelo sistema de pagamento. O aplicativo Diária não tem participação nessa operação.",
+      "anticipation_remind_label": "Lembre-se:",
+      "anticipation_reminder": "Você pode aguardar a data prevista do pagamento deste serviço e evitar a taxa cobrada pela antecipação.",
+      "anticipation_btn_close": "fechar",
+      "anticipation_btn_anticipate": "antecipar",
+      "anticipation_confirm_title": "Tem certeza que deseja antecipar esse pagamento?",
+      "anticipation_confirm_cancel": "cancelar",
+      "anticipation_confirm_ok": "confirmar"
     }
   },
   "business": {

+ 1 - 1
src/layouts/MainLayout.vue

@@ -70,7 +70,7 @@ const navItems = computed(() => [
     icon: "mdi-home-outline",
   },
   {
-    name: "PagamentosPage",
+    name: "PaymentsPage",
     label: t('nav.payments'),
     icon: "mdi-credit-card-outline",
   },

+ 450 - 0
src/pages/payments/PaymentsPage.vue

@@ -0,0 +1,450 @@
+<template>
+  <q-page class="bg-page q-pb-xl">
+    <div class="payments-header row items-center bg-white">
+      <q-space />
+      <span class="text-subtitle1 text-weight-bold gradient-diarista">{{ $t('provider.payments.title') }}</span>
+      <q-space />
+    </div>
+
+    <div class="q-mx-md q-mt-md">
+      <q-card class="earnings-card bg-primary" :flat="false" style="cursor: pointer;" @click="earningsExpanded = !earningsExpanded">
+        <q-card-section class="q-pa-md">
+          <div class="row items-center no-wrap">
+            <q-icon name="mdi-currency-usd" size="22px" color="white" class="q-mr-sm" />
+            <span class="text-body1 text-weight-bold text-white">{{ $t('provider.payments.total_earnings_title') }}</span>
+            <q-space />
+            <q-icon
+              :name="earningsExpanded ? 'mdi-chevron-up' : 'mdi-chevron-down'"
+              size="22px"
+              color="white"
+            />
+          </div>
+        </q-card-section>
+
+        <q-slide-transition>
+          <div v-show="earningsExpanded" @click.stop>
+            <q-separator color="white" style="opacity: 0.2;" />
+            <q-card-section class="q-pa-md">
+              <div class="earnings-value text-white">
+                {{ formatCurrency(earningsByPeriod[selectedPeriod].value) }}
+              </div>
+              <div class="earnings-count text-white q-mb-md">
+                {{ $t('provider.payments.total_earnings_count', { count: earningsByPeriod[selectedPeriod].count }) }}
+              </div>
+
+              <div class="row q-gutter-x-xs q-mb-md">
+                <q-btn
+                  v-for="period in periods"
+                  :key="period.key"
+                  dense
+                  no-caps
+                  :unelevated="selectedPeriod === period.key"
+                  :flat="selectedPeriod !== period.key"
+                  :rounded="true"
+                  :color="selectedPeriod === period.key ? 'white' : undefined"
+                  :text-color="selectedPeriod === period.key ? 'primary' : 'white'"
+                  :label="$t(period.labelKey)"
+                  class="period-btn"
+                  @click.stop="selectedPeriod = period.key"
+                />
+              </div>
+
+              <div class="text-caption text-white" style="opacity: 0.8;">
+                {{ $t('provider.payments.total_earnings_days', { days: periodDays[selectedPeriod] }) }}
+              </div>
+            </q-card-section>
+          </div>
+        </q-slide-transition>
+      </q-card>
+    </div>
+
+    <div class="q-mx-md q-mt-sm">
+      <q-card class="balance-card bg-surface shadow-card" :flat="false">
+        <q-card-section class="q-pa-md">
+          <div class="row items-center no-wrap q-mb-xs">
+            <div class="col">
+              <div class="text-caption text-grey-6 text-weight-bold">{{ $t('provider.payments.available_balance_label') }}</div>
+              <div class="balance-value text-secondary">{{ formatCurrency(saldoDisponivel) }}</div>
+            </div>
+            <q-btn
+              unelevated
+              no-caps
+              color="secondary"
+              class="btn-withdraw"
+              :label="$t('provider.payments.btn_withdraw')"
+            />
+          </div>
+          <div class="text-caption text-grey-6">
+            {{ $t('provider.payments.available_balance_prefix') }}
+            <strong>{{ $t('provider.payments.available_balance_highlight') }}</strong>
+          </div>
+        </q-card-section>
+      </q-card>
+    </div>
+
+    <div class="q-mx-md q-mt-sm">
+      <q-card class="balance-card bg-secondary shadow-card" :flat="false">
+        <q-card-section class="q-pa-md">
+          <div class="row items-center no-wrap">
+            <div class="col">
+              <div class="text-caption text-white">{{ $t('provider.payments.pending_balance_label') }}</div>
+              <div class="balance-value text-white">{{ formatCurrency(saldoALiberar) }}</div>
+              <div class="text-caption text-white q-mt-xs" style="opacity: 0.85;">
+                {{ $t('provider.payments.pending_balance_desc') }}
+              </div>
+            </div>
+            <q-icon name="mdi-clock-outline" color="white" size="32px" style="opacity: 0.7;" />
+          </div>
+        </q-card-section>
+      </q-card>
+    </div>
+
+    <div class="q-mx-md q-mt-md">
+      <div class="row items-center no-wrap q-mb-sm">
+        <span class="section-title gradient-diarista">{{ $t('provider.payments.services_title') }}</span>
+        <q-space />
+        <q-btn
+          flat
+          no-caps
+          color="primary"
+          size="sm"
+          :label="$t('provider.payments.btn_view_movements')"
+          icon-right="mdi-chevron-right"
+          @click="openMovimentacoes"
+        />
+      </div>
+
+      <q-card
+        v-for="item in mockServices"
+        :key="item.id"
+        class="service-card bg-surface shadow-card q-mb-sm"
+        :flat="false"
+      >
+        <q-card-section class="q-pa-sm">
+          <div class="row no-wrap items-start q-gutter-x-sm">
+            <q-avatar size="44px">
+              <img :src="item.client_photo || defaultAvatar">
+            </q-avatar>
+
+            <div class="col column">
+              <span class="text-name ellipsis">{{ item.client_name }}</span>
+              <div class="text-date-regular">
+                <span class="text-date-bold">{{ $t('provider.payments.services_date_service') }}</span>
+                {{ ' ' + formatShortDate(item.date) }}
+              </div>
+              <div class="text-date-regular">
+                <span class="text-date-bold">{{ $t('provider.payments.services_date_payment') }}</span>
+                {{ ' ' + formatShortDate(item.payment_date) }}
+              </div>
+            </div>
+
+            <div class="col-auto column items-end">
+              <q-chip
+                dense
+                square
+                :color="payStatusBgColor(item.payment_status)"
+                :text-color="payStatusTextColor(item.payment_status)"
+                :label="payStatusLabel(item.payment_status)"
+                class="status-chip"
+              />
+              <span class="text-price">{{ formatCurrency(item.total_amount) }}</span>
+              <span class="text-period">{{ periodLabel(item.period_type) }}</span>
+            </div>
+          </div>
+
+          <div class="row items-center no-wrap q-mt-xs">
+            <span class="type-label" :class="item.schedule_type === 'custom' ? 'type-custom' : 'type-default'">
+              {{ item.schedule_type === 'custom' ? $t('provider.dashboard.agenda.type_custom') : $t('provider.dashboard.agenda.type_default') }}
+            </span>
+            <q-space />
+            <q-btn
+              v-if="item.payment_status === 'pending' && item.hours_until_service < 48"
+              unelevated
+              rounded
+              no-caps
+              color="secondary"
+              size="xs"
+              class="btn-anticipate"
+              :label="$t('provider.payments.btn_anticipate')"
+              @click="openAntecipacaoDialog(item)"
+            />
+          </div>
+        </q-card-section>
+      </q-card>
+    </div>
+  </q-page>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import { useQuasar } from 'quasar';
+import { useI18n } from 'vue-i18n';
+import { formatCurrency } from 'src/helpers/utils';
+import { labelsPeriodTypes } from 'src/helpers/arraysOptions/labelsPeriodTypes.js';
+import MovimentacoesDialog from 'src/components/payments/MovimentacoesDialog.vue';
+import AntecipacaoDialog from 'src/components/payments/AntecipacaoDialog.vue';
+import AntecipacaoConfirmDialog from 'src/components/payments/AntecipacaoConfirmDialog.vue';
+
+const $q = useQuasar();
+const { t } = useI18n();
+
+const defaultAvatar = 'https://cdn.quasar.dev/img/avatar.png';
+const earningsExpanded = ref(false);
+const selectedPeriod = ref('week');
+
+const periods = [
+  { key: 'week',  labelKey: 'provider.payments.period_week'  },
+  { key: 'month', labelKey: 'provider.payments.period_month' },
+  { key: 'year',  labelKey: 'provider.payments.period_year'  },
+];
+
+const periodDays = { week: 7, month: 30, year: 365 };
+
+const earningsByPeriod = {
+  week:  { value: 320.00,  count: 5   },
+  month: { value: 1240.00, count: 22  },
+  year:  { value: 8560.00, count: 187 },
+};
+
+const saldoDisponivel = ref(420.00);
+const saldoALiberar   = ref(180.00);
+
+const mockServices = ref([
+  {
+    id: 1,
+    client_name: 'Maria Silva',
+    client_photo: null,
+    date: '2026-05-12',
+    payment_date: '2026-05-17',
+    total_amount: 120.00,
+    period_type: 4,
+    schedule_type: 'default',
+    payment_status: 'pending',
+    hours_until_service: 20,
+  },
+  {
+    id: 2,
+    client_name: 'Ana Souza',
+    client_photo: null,
+    date: '2026-05-15',
+    payment_date: '2026-05-20',
+    total_amount: 220.00,
+    period_type: 8,
+    schedule_type: 'custom',
+    payment_status: 'pending',
+    hours_until_service: 72,
+  },
+  {
+    id: 3,
+    client_name: 'Carlos Lima',
+    client_photo: null,
+    date: '2026-05-10',
+    payment_date: '2026-05-15',
+    total_amount: 160.00,
+    period_type: 6,
+    schedule_type: 'default',
+    payment_status: 'paid',
+    hours_until_service: null,
+  },
+  {
+    id: 4,
+    client_name: 'Julia Ferreira',
+    client_photo: null,
+    date: '2026-05-08',
+    payment_date: '2026-05-13',
+    total_amount: 100.00,
+    period_type: 4,
+    schedule_type: 'custom',
+    payment_status: 'anticipated',
+    hours_until_service: null,
+  },
+  {
+    id: 5,
+    client_name: 'Pedro Alves',
+    client_photo: null,
+    date: '2026-05-05',
+    payment_date: '2026-05-10',
+    total_amount: 80.00,
+    period_type: 2,
+    schedule_type: 'default',
+    payment_status: 'cancelled',
+    hours_until_service: null,
+  },
+]);
+
+const parseLocalDate = (dateStr) => {
+  if (!dateStr) return null;
+  const s = String(dateStr);
+  const iso = s.match(/^(\d{4})-(\d{2})-(\d{2})/);
+  if (iso) return new Date(+iso[1], +iso[2] - 1, +iso[3]);
+  const dmy = s.match(/^(\d{2})\/(\d{2})\/(\d{4})/);
+  if (dmy) return new Date(+dmy[3], +dmy[2] - 1, +dmy[1]);
+  return null;
+};
+
+const formatShortDate = (dateStr) => {
+  const d = parseLocalDate(dateStr);
+  if (!d) return '';
+  return d.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
+};
+
+const periodLabel = (periodType) => {
+  const found = labelsPeriodTypes.find(l => l.value == periodType);
+  return found ? t(found.label) : '';
+};
+
+const payStatusLabel = (status) => {
+  const map = {
+    pending:     t('provider.payments.pay_status_pending'),
+    paid:        t('provider.payments.pay_status_paid'),
+    anticipated: t('provider.payments.pay_status_anticipated'),
+    cancelled:   t('provider.payments.pay_status_cancelled'),
+  };
+  return map[status] ?? status;
+};
+
+const payStatusBgColor = (status) => {
+  const map = {
+    pending:     'warning-bg',
+    paid:        'success-bg',
+    anticipated: 'teal-bg',
+    cancelled:   'neutral-bg',
+  };
+  return map[status] ?? 'neutral-bg';
+};
+
+const payStatusTextColor = (status) => {
+  const map = {
+    pending:     'warning',
+    paid:        'success',
+    anticipated: 'teal',
+    cancelled:   'status-finished',
+  };
+  return map[status] ?? 'text';
+};
+
+const openMovimentacoes = () => {
+  $q.dialog({ component: MovimentacoesDialog });
+};
+
+const openAntecipacaoDialog = (item) => {
+  $q.dialog({
+    component: AntecipacaoDialog,
+    componentProps: { service: item },
+  }).onOk(() => {
+    $q.dialog({ component: AntecipacaoConfirmDialog }).onOk(() => {
+      const svc = mockServices.value.find(s => s.id === item.id);
+      if (svc) svc.payment_status = 'anticipated';
+    });
+  });
+};
+</script>
+
+<style scoped lang="scss">
+.payments-header {
+  padding-top: calc(env(safe-area-inset-top) + 12px);
+  padding-bottom: 12px;
+  box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.08);
+}
+
+.earnings-card,
+.balance-card,
+.service-card {
+  border-radius: 12px;
+  width: 100%;
+}
+
+.earnings-value {
+  font-size: 28px;
+  font-weight: 700;
+  line-height: 1.2;
+}
+
+.earnings-count {
+  font-size: 12px;
+  opacity: 0.9;
+}
+
+.balance-value {
+  font-size: 20px;
+  font-weight: 700;
+  line-height: 1.2;
+}
+
+.btn-withdraw {
+  border-radius: 8px !important;
+  font-size: 13px;
+  font-weight: 700;
+  padding: 6px 18px;
+}
+
+.period-btn {
+  font-size: 12px;
+  font-weight: 600;
+}
+
+.type-label {
+  font-size: 10px;
+  font-weight: 600;
+  line-height: 1.2;
+}
+
+.type-default {
+  color: var(--q-primary);
+}
+
+.type-custom {
+  color: var(--q-secondary);
+}
+
+.text-name {
+  font-size: 13px;
+  font-weight: 700;
+  color: #3a3a4a;
+  max-width: 130px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.text-date-bold {
+  font-family: 'Inter', sans-serif;
+  font-size: 11px;
+  font-weight: 700;
+  color: #3a3a4a;
+}
+
+.text-date-regular {
+  font-family: 'Inter', sans-serif;
+  font-size: 11px;
+  font-weight: 400;
+  color: #666;
+}
+
+.text-price {
+  font-size: 13px;
+  font-weight: 700;
+  color: #3a3a4a;
+  white-space: nowrap;
+}
+
+.text-period {
+  font-size: 10px;
+  color: #888;
+  text-align: right;
+  white-space: nowrap;
+}
+
+.status-chip {
+  font-size: 11px !important;
+  font-weight: 700;
+  height: auto;
+  padding: 2px 2px;
+}
+
+.btn-anticipate {
+  font-size: 11px;
+  font-weight: 700;
+  padding: 3px 10px;
+}
+</style>

+ 0 - 52
src/pages/search/PagamentosPage.vue

@@ -1,52 +0,0 @@
-<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
-<template>
-  <section class="mobile-placeholder">
-    <div class="mobile-placeholder__badge">
-      <q-icon name="mdi-magnify" />
-    </div>
-    <h1 class="mobile-placeholder__title">Busca</h1>
-    <p class="mobile-placeholder__description">
-      Área reservada para a parte de pagamentos, que ainda está em desenvolvimento
-    </p>
-  </section>
-</template>
-
-<style scoped>
-.mobile-placeholder {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  min-height: calc(100dvh - 240px);
-  padding: 32px 20px;
-  text-align: center;
-}
-
-.mobile-placeholder__badge {
-  display: grid;
-  place-items: center;
-  width: 88px;
-  height: 88px;
-  border-radius: 28px;
-  margin-bottom: 20px;
-  background: linear-gradient(180deg, rgba(255, 0, 234, 0.14), rgba(107, 17, 203, 0.08));
-  color: #ff00ea;
-  font-size: 44px;
-}
-
-.mobile-placeholder__title {
-  margin: 0 0 8px;
-  font-size: 28px;
-  font-weight: 700;
-  line-height: 1.1;
-  color: #4d4d4d;
-}
-
-.mobile-placeholder__description {
-  max-width: 280px;
-  margin: 0;
-  font-size: 16px;
-  line-height: 1.5;
-  color: #8d8d8d;
-}
-</style>

+ 2 - 2
src/router/routes/navbar.route.js

@@ -1,8 +1,8 @@
 export default [
   {
     path: "pagamentos",
-    name: "PagamentosPage",
-    component: () => import("src/pages/search/PagamentosPage.vue"),
+    name: "PaymentsPage",
+    component: () => import("src/pages/payments/PaymentsPage.vue"),
   },
   {
     path: "agenda",