Преглед на файлове

Merge branch 'development' of gogs.softpar.inf.br:Softpar/sfp_front_vue_diarista_prestador into development

Gustavo Zanatta преди 2 седмици
родител
ревизия
4995299cc9

+ 221 - 0
quasar.config.js.temporary.compiled.1779814851016.mjs

@@ -0,0 +1,221 @@
+/* eslint-disable */
+/**
+ * THIS FILE IS GENERATED AUTOMATICALLY.
+ * 1. DO NOT edit this file directly as it won't do anything.
+ * 2. EDIT the original quasar.config file INSTEAD.
+ * 3. DO NOT git commit this file. It should be ignored.
+ *
+ * This file is still here because there was an error in
+ * the original quasar.config file and this allows you to
+ * investigate the Node.js stack error.
+ *
+ * After you fix the original file, this file will be
+ * deleted automatically.
+ **/
+
+
+// quasar.config.js
+import { defineConfig } from "@quasar/app-vite/wrappers";
+import { fileURLToPath } from "node:url";
+var __quasar_inject_import_meta_url__ = "file:///home/mantovani/development/softpar/frontend/sfp_front_vue_diarista_prestador/quasar.config.js";
+var quasar_config_default = defineConfig((ctx) => {
+  return {
+    bin: {
+      linuxAndroidStudio: "/snap/bin/android-studio"
+    },
+    // https://v2.quasar.dev/quasar-cli-vite/prefetch-feature
+    // preFetch: true,
+    // app boot file (/src/boot)
+    // --> boot files are part of "main.js"
+    // https://v2.quasar.dev/quasar-cli-vite/boot-files
+    boot: [
+      "axios",
+      "i18n",
+      "defaultPropsComponents"
+      // "socket.io",
+    ],
+    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
+    css: ["app.scss"],
+    // https://github.com/quasarframework/quasar/tree/dev/extras
+    extras: [
+      // 'ionicons-v4',
+      "mdi-v7",
+      // 'fontawesome-v6',
+      // 'eva-icons',
+      // 'themify',
+      // "line-awesome",
+      // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
+      "roboto-font",
+      // optional, you are not bound to it
+      "material-icons"
+      // optional, you are not bound to it
+    ],
+    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
+    build: {
+      target: {
+        browser: ["es2022", "firefox115", "chrome115", "safari14"],
+        node: "node22"
+      },
+      vueRouterMode: "history",
+      // available values: 'hash', 'history'
+      // vueRouterBase,
+      // vueDevtools,
+      // vueOptionsAPI: false,
+      // rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
+      // publicPath: '/',
+      // analyze: true,
+      env: {
+        API_URL: ctx.dev ? "http://localhost:3000" : "http://localhost:3000",
+        PASSWORD: ctx.dev ? "S@ft2080." : "",
+        WEBSOCKET_API: ctx.dev ? "http://localhost:4321/" : "http://localhost:4321/",
+        WEBSOCKET_PATH: ctx.dev ? "/socket.io" : "/socket.io",
+        WEBSOCKET_ROOM: ctx.dev ? "LARAVEL" : "LARAVEL",
+        WEBSOCKET_API_KEY: "7wArC/kl0nTbt4zBu0agw.NXLyjA96I6x1XmBcuokwPqfo3/CIxzqYw.PTthh5eqa08Uf4ubFlOqatpShoz1CRRID9pZReEFvBk3il6E9u"
+      },
+      // rawDefine: {}
+      // ignorePublicFolder: true,
+      // minify: false,
+      // polyfillModulePreload: true,
+      // distDir
+      // extendViteConf (viteConf) {},
+      // viteVuePluginOptions: {},
+      vitePlugins: [
+        [
+          "@intlify/unplugin-vue-i18n/vite",
+          {
+            include: [fileURLToPath(new URL("./src/i18n", __quasar_inject_import_meta_url__))],
+            ssr: ctx.modeName === "ssr"
+          }
+        ],
+        [
+          "vite-plugin-checker",
+          {
+            eslint: {
+              lintCommand: 'eslint -c ./eslint.config.js "./src*/**/*.{js,mjs,cjs,vue}"',
+              useFlatConfig: true
+            }
+          },
+          { server: false }
+        ]
+      ]
+    },
+    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
+    devServer: {
+      // https: true
+      open: false
+      // opens browser window automatically
+    },
+    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
+    framework: {
+      lang: "pt-BR",
+      config: {
+        dark: "auto",
+        notify: {
+          position: "top-right"
+        }
+      },
+      // iconSet: 'material-icons', // Quasar icon set
+      // lang: 'en-US', // Quasar language pack
+      // For special cases outside of where the auto-import strategy can have an impact
+      // (like functional components as one of the examples),
+      // you can manually specify Quasar components/directives to be available everywhere:
+      //
+      // components: [],
+      // directives: [],
+      // Quasar plugins
+      plugins: ["Dialog", "Notify", "Loading", "Cookies", "Dark"]
+    },
+    // animations: 'all', // --- includes all animations
+    // https://v2.quasar.dev/options/animations
+    animations: [],
+    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#property-sourcefiles
+    // sourceFiles: {
+    //   rootComponent: 'src/App.vue',
+    //   router: 'src/router/index',
+    //   store: 'src/store/index',
+    //   pwaRegisterServiceWorker: 'src-pwa/register-service-worker',
+    //   pwaServiceWorker: 'src-pwa/custom-service-worker',
+    //   pwaManifestFile: 'src-pwa/manifest.json',
+    //   electronMain: 'src-electron/electron-main',
+    //   electronPreload: 'src-electron/electron-preload'
+    //   bexManifestFile: 'src-bex/manifest.json
+    // },
+    // https://v2.quasar.dev/quasar-cli-vite/developing-ssr/configuring-ssr
+    ssr: {
+      prodPort: 3e3,
+      // The default port that the production server should use
+      // (gets superseded if process.env.PORT is specified at runtime)
+      middlewares: [
+        "render"
+        // keep this as last one
+      ],
+      // extendPackageJson (json) {},
+      // extendSSRWebserverConf (esbuildConf) {},
+      // manualStoreSerialization: true,
+      // manualStoreSsrContextInjection: true,
+      // manualStoreHydration: true,
+      // manualPostHydrationTrigger: true,
+      pwa: false
+      // pwaOfflineHtmlFilename: 'offline.html', // do NOT use index.html as name!
+      // will mess up SSR
+      // pwaExtendGenerateSWOptions (cfg) {},
+      // pwaExtendInjectManifestOptions (cfg) {}
+    },
+    // https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa
+    pwa: {
+      workboxMode: "GenerateSW"
+      // 'GenerateSW' or 'InjectManifest'
+      // swFilename: 'sw.js',
+      // manifestFilename: 'manifest.json'
+      // extendManifestJson (json) {},
+      // useCredentialsForManifestTag: true,
+      // injectPwaMetaTags: false,
+      // extendPWACustomSWConf (esbuildConf) {},
+      // extendGenerateSWOptions (cfg) {},
+      // extendInjectManifestOptions (cfg) {}
+    },
+    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-cordova-apps/configuring-cordova
+    cordova: {
+      // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
+    },
+    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-capacitor-apps/configuring-capacitor
+    capacitor: {
+      hideSplashscreen: true
+    },
+    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/configuring-electron
+    electron: {
+      // extendElectronMainConf (esbuildConf) {},
+      // extendElectronPreloadConf (esbuildConf) {},
+      // extendPackageJson (json) {},
+      // Electron preload scripts (if any) from /src-electron, WITHOUT file extension
+      preloadScripts: ["electron-preload"],
+      // specify the debugging port to use for the Electron app when running in development mode
+      inspectPort: 5858,
+      bundler: "packager",
+      // 'packager' or 'builder'
+      packager: {
+        // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
+        // OS X / Mac App Store
+        // appBundleId: '',
+        // appCategoryType: '',
+        // osxSign: '',
+        // protocol: 'myapp://path',
+        // Windows only
+        // win32metadata: { ... }
+      },
+      builder: {
+        // https://www.electron.build/configuration/configuration
+        appId: "quasar-skeleton"
+      }
+    },
+    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
+    bex: {
+      // extendBexScriptsConf (esbuildConf) {},
+      // extendBexManifestJson (json) {},
+      contentScripts: ["my-content-script"]
+    }
+  };
+});
+export {
+  quasar_config_default as default
+};

+ 6 - 0
src/api/paymentSplit.js

@@ -0,0 +1,6 @@
+import api from 'src/api'
+
+export const getPaymentSplits = async () => {
+  const { data } = await api.get('/provider/payment-splits')
+  return data.payload
+}

+ 16 - 0
src/api/providerWithdrawal.js

@@ -0,0 +1,16 @@
+import api from 'src/api'
+
+export const getBalance = async () => {
+  const { data } = await api.get('/provider/withdrawals/balance')
+  return data.payload
+}
+
+export const getWithdrawals = async () => {
+  const { data } = await api.get('/provider/withdrawals')
+  return data.payload
+}
+
+export const requestWithdrawal = async () => {
+  const { data } = await api.post('/provider/withdrawals')
+  return data.payload
+}

+ 36 - 5
src/api/user.js

@@ -33,14 +33,45 @@ export const userTypes = async () => {
 export const sendCode = async (email, phone) => {
   const data = await api.post("/provider-send-code", { email, phone });
   return data;
-}
+};
 
 export const validateCode = async (email, phone, code, isLogin = false) => {
-  const data = await api.post("/validate-code-provider", { email, phone, code, isLogin });
+  const data = await api.post("/validate-code-provider", {
+    email,
+    phone,
+    code,
+    isLogin,
+  });
   return data;
-}
+};
+
+const removeEmptyRegistrationFields = (data) => {
+  if (Array.isArray(data)) {
+    return data.map(removeEmptyRegistrationFields);
+  }
+
+  if (!data || typeof data !== "object") {
+    return data;
+  }
+
+  return Object.entries(data).reduce((payload, [key, value]) => {
+    if (
+      value === undefined ||
+      key === "no_complement" ||
+      key.endsWith("_file")
+    ) {
+      return payload;
+    }
+
+    payload[key] = removeEmptyRegistrationFields(value);
+    return payload;
+  }, {});
+};
 
 export const createUserAndProvider = async (data) => {
-  const response = await api.post("/register-provider", data);
+  const response = await api.post(
+    "/register-provider",
+    removeEmptyRegistrationFields(data),
+  );
   return response;
-}
+};

+ 65 - 5
src/components/dashboard/DashboardHeaderBar.vue

@@ -17,16 +17,69 @@
       <img :src="LogoDiariaColorida" alt="Diária" class="dashboard-logo" />
     </div>
 
-    <div class="col row justify-end items-center">
-      <q-btn flat round dense icon="mdi-bell-outline" color="grey-7" size="sm" />
-    </div>
+   <div class="col row justify-end items-center">
+
+  <q-btn
+    flat
+    round
+    dense
+    color="grey-7"
+    size="sm"
+    @click="goToNotifications"
+  >
+
+    <q-icon
+      name="mdi-bell-outline"
+      size="20px"
+    />
+
+    <q-badge
+      v-if="unreadNotifications > 0"
+      floating
+      rounded
+      color="pink"
+      class="notification-badge"
+    >
+      {{ unreadNotifications }}
+    </q-badge>
+
+  </q-btn>
+
+</div>
   </div>
 </template>
 
 <script setup>
-import LogoDiariaColorida from 'src/assets/logo_diaria_colorido_sem_texto.svg';
+import { computed } from 'vue'
+import { useRouter } from 'vue-router'
+import LogoDiariaColorida from 'src/assets/logo_diaria_colorido_sem_texto.svg'
+
+const router = useRouter()
+
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => null
+  },
 
-defineProps({ data: { type: Object, default: () => null } });
+  notifications: {
+    type: Array,
+    default: () => []
+  }
+})
+
+const unreadNotifications = computed(() => {
+  return props.notifications.filter((notification) => !notification.read).length
+})
+
+const goToNotifications = () => {
+  router.push({
+    name: 'NotificationsPage',
+    query: {
+      notifications: JSON.stringify(props.notifications)
+    }
+  })
+}
 </script>
 
 <style scoped lang="scss">
@@ -53,4 +106,11 @@ defineProps({ data: { type: Object, default: () => null } });
   color: #999;
   line-height: 1;
 }
+
+.notification-badge {
+  min-width: 16px;
+  height: 16px;
+  font-size: 10px;
+  font-weight: 700;
+}
 </style>

+ 26 - 18
src/components/dashboard/DashboardTodayServices.vue

@@ -290,27 +290,35 @@ const openHelp = () => {
   font-size: 9px;
 }
 
-.code-input-row {
+.code-input-row { 
   display: flex;
-  gap: 5px;
+  align-items: center; 
+  justify-content: center; 
+  gap: 4px; background: #d1d5db; 
+  border-radius: 999px; 
+  padding: 4px 8px; 
+  width: 110px; 
+  height: 28px; 
+  margin: 0 auto; 
 }
 
-.code-input-box {
-  width: 16px;
-  height: 22px;
-  background: #d1d5db;
-  border: none;
-  border-radius: 6px;
-  text-align: center;
-  font-size: 11px;
-  font-weight: 700;
-  color: #1a1a2e;
-  outline: none;
-  caret-color: transparent;
-  &:focus {
-    background: #bec3cc;
-    box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.4);
-  }
+.code-input-box { 
+  width: 16px; 
+  height: 18px; 
+  background: transparent; 
+  border: none; 
+  border-bottom: 2px solid white; 
+  border-radius: 0; 
+  text-align: center; 
+  font-size: 11px; 
+  font-weight: 700; 
+  color: #1a1a2e; 
+  outline: none; 
+  caret-color: transparent; 
+  &:focus { 
+    border-bottom: 2px solid #ffffff; 
+    box-shadow: none; 
+  } 
 }
 
 .code-error-text {

+ 1 - 1
src/components/dashboard/SolicitationDetailsDialog.vue

@@ -337,7 +337,7 @@ const fullDateLabel = computed(() => {
   return d.toLocaleDateString('pt-BR', { day: '2-digit', month: 'long', year: 'numeric' })
 })
 
-const SERVICE_FEE_RATE = 0.10
+const SERVICE_FEE_RATE = 0.11
 
 const serviceFee = computed(() => {
   const total = parseFloat(props.solicitation.total_amount) || 0

+ 4 - 0
src/components/defaults/DefaultCurrencyInput.vue

@@ -77,4 +77,8 @@ watch(
     setValue(value);
   },
 );
+
+watch(numberValue, (value) => {
+  model.value = value;
+});
 </script>

+ 66 - 35
src/components/payments/MovimentacoesDialog.vue

@@ -10,7 +10,11 @@
       </div>
 
       <q-scroll-area class="col" style="flex: 1 1 0;">
-        <div class="q-pa-md">
+        <div v-if="loading" class="flex flex-center q-py-xl">
+          <q-spinner color="primary" size="40px" />
+        </div>
+
+        <div v-else class="q-pa-md">
           <q-card
             v-for="item in movements"
             :key="item.id"
@@ -23,14 +27,11 @@
                   <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>
+                  <span class="mov-label">{{ movLabel(item) }}</span>
+                  <span class="mov-date">{{ formatMovDate(item.created_at) }}</span>
                 </div>
-                <span class="mov-value" :class="item.value >= 0 ? 'text-success' : 'text-error'">
-                  {{ (item.value >= 0 ? '+' : '') + formatCurrency(Math.abs(item.value)) }}
+                <span class="mov-value" :class="item.gross_amount >= 0 ? 'text-success' : 'text-error'">
+                  {{ (item.gross_amount >= 0 ? '+' : '') + formatCurrency(Math.abs(item.gross_amount)) }}
                 </span>
               </div>
             </q-card-section>
@@ -42,52 +43,88 @@
 </template>
 
 <script setup>
-import { useDialogPluginComponent } from 'quasar';
+import { ref, onMounted, computed } from 'vue';
+import { useDialogPluginComponent, useQuasar } from 'quasar';
 import { useI18n } from 'vue-i18n';
 import { formatCurrency } from 'src/helpers/utils';
+import { getPaymentSplits } from 'src/api/paymentSplit';
+import { getWithdrawals } from 'src/api/providerWithdrawal';
 
 defineEmits([...useDialogPluginComponent.emits]);
 const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
 const { t } = useI18n();
+const $q = useQuasar();
+
+const loading = ref(true);
+const allMovements = ref([]);
+
+const movements = computed(() => {
+  return [...allMovements.value].sort((a, b) =>
+    new Date(b.created_at) - new Date(a.created_at)
+  );
+});
+
+const loadMovements = async () => {
+  loading.value = true;
+  try {
+    const [splits, withdrawals] = await Promise.all([
+      getPaymentSplits(),
+      getWithdrawals(),
+    ]);
+
+    const serviceItems = (splits || []).map((s) => ({
+      id: `split-${s.id}`,
+      type: 'servico',
+      client_name: s.client_name,
+      gross_amount: parseFloat(s.gross_amount || 0),
+      created_at: s.created_at,
+    }));
 
-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 withdrawalItems = (withdrawals || []).map((w) => ({
+      id: `withdrawal-${w.id}`,
+      type: 'saque',
+      gross_amount: -(parseFloat(w.gross_amount || 0)),
+      created_at: w.created_at,
+    }));
+
+    allMovements.value = [...serviceItems, ...withdrawalItems];
+  } catch (error) {
+    $q.notify({ type: 'negative', message: error?.response?.data?.message || error.message });
+  } finally {
+    loading.value = false;
+  }
+};
 
 const movIcon = (type) => {
-  const map = { tarifa: 'mdi-percent-outline', saque: 'mdi-bank-transfer-out', servico: 'mdi-broom' };
+  const map = { 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' };
+  const map = { saque: 'secondary-bg', servico: 'success-bg' };
   return map[type] ?? 'neutral-bg';
 };
 
 const movIconColor = (type) => {
-  const map = { tarifa: 'warning', saque: 'secondary', servico: 'success' };
+  const map = { 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 movLabel = (item) => {
+  if (item.type === 'servico') return t('provider.payments.mov_servico') + (item.client_name ? ` - ${item.client_name}` : '');
+  if (item.type === 'saque') return t('provider.payments.mov_saque');
+  return '';
 };
 
 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' });
+  try { return new Date(dateStr).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric' }); }
+  catch { return dateStr; }
 };
+
+onMounted(() => {
+  loadMovements();
+});
 </script>
 
 <style scoped lang="scss">
@@ -108,12 +145,6 @@ const formatMovDate = (dateStr) => {
   color: #3a3a4a;
 }
 
-.mov-code {
-  font-size: 10px;
-  color: #888;
-  margin-top: 1px;
-}
-
 .mov-date {
   font-size: 11px;
   color: #888;

+ 73 - 0
src/components/payments/WithdrawConfirmDialog.vue

@@ -0,0 +1,73 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="withdraw-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">
+        <q-icon name="mdi-bank-transfer-out" color="secondary" size="42px" class="q-mb-sm" />
+        <div class="text-body1 text-weight-bold text-text text-center q-mb-xs">
+          {{ $t('provider.payments.withdraw_confirm_title') }}
+        </div>
+        <div class="withdraw-amount text-secondary text-center">
+          {{ formatCurrency(amount) }}
+        </div>
+        <div class="text-caption text-grey-6 text-center q-mt-xs">
+          {{ $t('provider.payments.withdraw_confirm_message', { amount: formatCurrency(amount) }) }}
+        </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-4"
+          text-color="grey-8"
+          class="col"
+          :label="$t('provider.payments.anticipation_confirm_cancel')"
+          @click="onDialogCancel"
+        />
+        <q-btn
+          unelevated
+          rounded
+          no-caps
+          color="secondary"
+          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';
+import { formatCurrency } from 'src/helpers/utils';
+
+defineProps({
+  amount: {
+    type: Number,
+    required: true,
+  },
+});
+
+defineEmits([...useDialogPluginComponent.emits]);
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+</script>
+
+<style scoped>
+.withdraw-card {
+  width: 320px;
+  max-width: 96vw;
+  border-radius: 20px !important;
+}
+
+.withdraw-amount {
+  font-size: 28px;
+  font-weight: 700;
+  line-height: 1.2;
+}
+</style>

+ 2 - 0
src/css/quasar.variables.scss

@@ -99,6 +99,8 @@ $colors: (
   // Light Blue background
   "secondary-bg": #FCE7F3,
   // Light Pink background
+  "error-bg": #FEE2E2,
+  // Light Red background (softer failed chip)
   "neutral-bg": #F3F4F6,
   // Light Grey background
   "status-finished": #9CA3AF,

+ 4 - 1
src/i18n/locales/en.json

@@ -432,7 +432,10 @@
       "anticipation_btn_anticipate": "anticipate",
       "anticipation_confirm_title": "Are you sure you want to anticipate this payment?",
       "anticipation_confirm_cancel": "cancel",
-      "anticipation_confirm_ok": "confirm"
+      "anticipation_confirm_ok": "confirm",
+      "default_client_name": "Client",
+      "withdraw_confirm_title": "Confirm withdrawal",
+      "withdraw_confirm_message": "Do you want to withdraw {amount} to your account?"
     }
   },
   "business": {

+ 4 - 1
src/i18n/locales/es.json

@@ -430,7 +430,10 @@
       "anticipation_btn_anticipate": "anticipar",
       "anticipation_confirm_title": "¿Está seguro de que desea anticipar este pago?",
       "anticipation_confirm_cancel": "cancelar",
-      "anticipation_confirm_ok": "confirmar"
+      "anticipation_confirm_ok": "confirmar",
+      "default_client_name": "Cliente",
+      "withdraw_confirm_title": "Confirmar retiro",
+      "withdraw_confirm_message": "¿Desea retirar {amount} a su cuenta?"
     }
   },
   "business": {

+ 12 - 1
src/i18n/locales/pt.json

@@ -414,6 +414,9 @@
       "services_date_payment": "Data do pagamento:",
       "pay_status_pending": "Pendente",
       "pay_status_paid": "Pago",
+      "pay_status_authorized": "Autorizado",
+      "pay_status_processing": "Processando",
+      "pay_status_failed": "Falhou",
       "pay_status_anticipated": "Pago antecipado",
       "pay_status_cancelled": "Serviço cancelado",
       "btn_anticipate": "antecipar",
@@ -432,7 +435,10 @@
       "anticipation_btn_anticipate": "antecipar",
       "anticipation_confirm_title": "Tem certeza que deseja antecipar esse pagamento?",
       "anticipation_confirm_cancel": "cancelar",
-      "anticipation_confirm_ok": "confirmar"
+      "anticipation_confirm_ok": "confirmar",
+      "default_client_name": "Cliente",
+      "withdraw_confirm_title": "Confirmar saque",
+      "withdraw_confirm_message": "Deseja sacar {amount} para sua conta?"
     }
   },
   "business": {
@@ -808,5 +814,10 @@
     "payments": "Pagamentos",
     "agenda": "Agenda",
     "profile": "Perfil"
+  },
+  "notifications": {
+    "title": "Notificações",
+    "unread": "Não lidas",
+    "mark_all_read": " Marcar todas como lidas"
   }
 }

+ 272 - 61
src/pages/LoginPage.vue

@@ -2,7 +2,12 @@
 <template>
   <q-page class="login-page bg-surface-dark">
     <Transition name="fade-slide" mode="out-in">
-      <div v-if="!clicked" key="splash" class="splash-screen" @click="clicked = true">
+      <div
+        v-if="!clicked"
+        key="splash"
+        class="splash-screen"
+        @click="clicked = true"
+      >
         <img :src="BackgroundLogin" class="splash-layer splash-layer--bg" />
         <img :src="FotoDiarista" class="splash-layer splash-layer--photo" />
         <img :src="LogoLogin" class="splash-layer splash-layer--logo" />
@@ -22,26 +27,190 @@
           spellcheck="false"
           @submit="onSubmit"
         >
-          <div class="flow-content" :class="{ 'flow-content--centered': steps <= 2 && !showSubStep }">
-            <LoginStepOnePanel v-if="steps === 1" v-model:email="email" v-model:phone="phone" />
+          <div
+            class="flow-content"
+            :class="{ 'flow-content--centered': steps <= 2 && !showSubStep }"
+          >
+            <LoginStepOnePanel
+              v-if="steps === 1"
+              v-model:email="email"
+              v-model:phone="phone"
+            />
             <LoginStepTwoPanel v-else-if="steps === 2" v-model:code="code" />
-            <LoginStepThreePanel v-else-if="steps === 3" v-model="stepThreeForm" />
+            <LoginStepThreePanel
+              v-else-if="steps === 3"
+              v-model="stepThreeForm"
+            />
             <LoginStepFourPanel
               v-else-if="steps === 4"
               v-model="stepFourForm"
               @update:show-sub-step="showSubStep = $event"
             />
-            <LoginStepFivePanel v-else-if="steps === 5" v-model="stepFiveForm" />
-            <LoginStepSixPanel v-else-if="steps === 6" v-model="stepSixForm" />
+            <LoginStepFivePanel
+              v-else-if="steps === 5"
+              v-model="stepFiveForm"
+            />
+            <q-card-section v-else-if="steps === 6" class="no-padding">
+              <div
+                class="text-subtitle1 text-center text-weight-bold text-text q-mb-md"
+              >
+                Dados bancários
+              </div>
+
+              <div class="text-caption text-grey-7 text-center q-mb-md">
+                Informe uma conta vinculada ao mesmo CPF do cadastro para
+                receber os pagamentos.
+              </div>
+
+              <div class="row q-mb-sm items-center">
+                <q-radio
+                  v-model="stepSixForm.account_type"
+                  val="checking"
+                  label="Conta corrente"
+                  color="primary"
+                  keep-color
+                  class="q-mr-lg text-text"
+                />
+                <q-radio
+                  v-model="stepSixForm.account_type"
+                  val="savings"
+                  label="Poupança"
+                  color="primary"
+                  keep-color
+                  class="text-text"
+                />
+              </div>
+
+              <div class="text-text">
+                <span class="text-weight-medium">Código do banco</span>
+              </div>
+              <q-input
+                v-model="stepSixForm.bank"
+                no-error-icon
+                outlined
+                rounded
+                class="bg-surface q-mt-sm q-mb-md"
+                input-class="text-text"
+                placeholder="Ex: 001"
+                hide-bottom-space
+                :rules="[requiredRule]"
+                lazy-rules
+                maxlength="20"
+              />
+
+              <div class="row q-col-gutter-sm">
+                <div class="col-8">
+                  <div class="text-text">
+                    <span class="text-weight-medium">Agência</span>
+                  </div>
+                  <q-input
+                    v-model="stepSixForm.branch_number"
+                    no-error-icon
+                    outlined
+                    rounded
+                    class="bg-surface q-mt-sm q-mb-md"
+                    input-class="text-text"
+                    placeholder="0000"
+                    hide-bottom-space
+                    :rules="[requiredRule]"
+                    lazy-rules
+                    maxlength="20"
+                  />
+                </div>
+                <div class="col-4">
+                  <div class="text-text">
+                    <span class="text-weight-medium">Dígito</span>
+                  </div>
+                  <q-input
+                    v-model="stepSixForm.branch_check_digit"
+                    no-error-icon
+                    outlined
+                    rounded
+                    class="bg-surface q-mt-sm q-mb-md"
+                    input-class="text-text"
+                    placeholder="0"
+                    hide-bottom-space
+                    lazy-rules
+                    maxlength="10"
+                  />
+                </div>
+              </div>
+
+              <div class="row q-col-gutter-sm">
+                <div class="col-8">
+                  <div class="text-text">
+                    <span class="text-weight-medium">Conta</span>
+                  </div>
+                  <q-input
+                    v-model="stepSixForm.account_number"
+                    no-error-icon
+                    outlined
+                    rounded
+                    class="bg-surface q-mt-sm q-mb-md"
+                    input-class="text-text"
+                    placeholder="000000"
+                    hide-bottom-space
+                    :rules="[requiredRule]"
+                    lazy-rules
+                    maxlength="20"
+                  />
+                </div>
+                <div class="col-4">
+                  <div class="text-text">
+                    <span class="text-weight-medium">Dígito</span>
+                  </div>
+                  <q-input
+                    v-model="stepSixForm.account_check_digit"
+                    no-error-icon
+                    outlined
+                    rounded
+                    class="bg-surface q-mt-sm q-mb-md"
+                    input-class="text-text"
+                    placeholder="0"
+                    hide-bottom-space
+                    :rules="[requiredRule]"
+                    lazy-rules
+                    maxlength="10"
+                  />
+                </div>
+              </div>
+
+              <div class="text-text">
+                <span class="text-weight-medium">Chave Pix</span>
+              </div>
+              <q-input
+                v-model="stepSixForm.pix_key"
+                no-error-icon
+                outlined
+                rounded
+                class="bg-surface q-mt-sm q-mb-md"
+                input-class="text-text"
+                placeholder="Opcional"
+                hide-bottom-space
+                lazy-rules
+                maxlength="255"
+              />
+            </q-card-section>
+            <LoginStepSixPanel
+              v-else-if="steps === 7"
+              v-model="stepSevenForm"
+            />
 
-            <div v-else-if="steps === 7" class="column items-center justify-center q-gutter-md text-center">
+            <div
+              v-else-if="steps === 8"
+              class="column items-center justify-center q-gutter-md text-center"
+            >
               <q-icon name="mdi-clock-outline" size="64px" color="warning" />
-              <div class="text-h6 text-text">{{ $t('provider.login.pending_approval.title') }}</div>
-              <div class="text-body2 text-grey-5">{{ $t('provider.login.pending_approval.description') }}</div>
+              <div class="text-h6 text-text">
+                {{ $t("provider.login.pending_approval.title") }}
+              </div>
+              <div class="text-body2 text-grey-5">
+                {{ $t("provider.login.pending_approval.description") }}
+              </div>
             </div>
           </div>
 
-          <div v-if="!showSubStep && steps !== 7" class="flow-footer">
+          <div v-if="!showSubStep && steps !== 8" class="flow-footer">
             <q-btn
               color="primary-button"
               :label="actionLabel"
@@ -63,24 +232,24 @@
 </template>
 
 <script setup>
-import { computed, ref } from 'vue';
-import { useQuasar } from 'quasar';
-import { useRouter } from 'vue-router';
-import { useI18n } from 'vue-i18n';
-import { createUserAndProvider, sendCode, validateCode } from 'src/api/user';
-import { useAuth } from 'src/composables/useAuth';
-
-import BackgroundLogin from 'src/assets/background-login.svg';
-import FotoDiarista from 'src/assets/foto_diarista_login.svg';
-import LogoLogin from 'src/assets/logo_diaria_login.svg';
-import LogoDiariaCampos from 'src/assets/logo_diaria_campos_login.svg';
-
-import LoginStepOnePanel from 'src/components/login/LoginStepOnePanel.vue';
-import LoginStepTwoPanel from 'src/components/login/LoginStepTwoPanel.vue';
-import LoginStepThreePanel from 'src/components/login/LoginStepThreePanel.vue';
-import LoginStepFourPanel from 'src/components/login/LoginStepFourPanel.vue';
-import LoginStepFivePanel from 'src/components/login/LoginStepFivePanel.vue';
-import LoginStepSixPanel from 'src/components/login/LoginStepSixPanel.vue';
+import { computed, ref } from "vue";
+import { useQuasar } from "quasar";
+import { useRouter } from "vue-router";
+import { useI18n } from "vue-i18n";
+import { createUserAndProvider, sendCode, validateCode } from "src/api/user";
+import { useAuth } from "src/composables/useAuth";
+
+import BackgroundLogin from "src/assets/background-login.svg";
+import FotoDiarista from "src/assets/foto_diarista_login.svg";
+import LogoLogin from "src/assets/logo_diaria_login.svg";
+import LogoDiariaCampos from "src/assets/logo_diaria_campos_login.svg";
+
+import LoginStepOnePanel from "src/components/login/LoginStepOnePanel.vue";
+import LoginStepTwoPanel from "src/components/login/LoginStepTwoPanel.vue";
+import LoginStepThreePanel from "src/components/login/LoginStepThreePanel.vue";
+import LoginStepFourPanel from "src/components/login/LoginStepFourPanel.vue";
+import LoginStepFivePanel from "src/components/login/LoginStepFivePanel.vue";
+import LoginStepSixPanel from "src/components/login/LoginStepSixPanel.vue";
 
 const { t } = useI18n();
 const $q = useQuasar();
@@ -94,26 +263,26 @@ const submitting = ref(false);
 const loginForm = ref(null);
 const isLogin = ref(false);
 
-const email = ref('');
-const phone = ref('');
-const code = ref('');
+const email = ref("");
+const phone = ref("");
+const code = ref("");
 
 const stepThreeForm = ref({
-  name: '',
-  phone: '',
-  email: '',
-  rg: '',
-  document: '',
-  birth_date: '',
-  zip_code: '',
-  address: '',
-  complement: '',
+  name: "",
+  phone: "",
+  email: "",
+  rg: "",
+  document: "",
+  birth_date: "",
+  zip_code: "",
+  address: "",
+  complement: "",
   no_complement: false,
-  city: '',
-  state: '',
-  address_type: 'home',
-  nickname: 'Principal',
-  instructions: '',
+  city: "",
+  state: "",
+  address_type: "home",
+  nickname: "Principal",
+  instructions: "",
 });
 
 const stepFourForm = ref({
@@ -131,18 +300,30 @@ const stepFiveForm = ref({
 });
 
 const stepSixForm = ref({
+  account_type: "checking",
+  bank: "",
+  branch_number: "",
+  branch_check_digit: "",
+  account_number: "",
+  account_check_digit: "",
+  pix_key: "",
+});
+
+const stepSevenForm = ref({
   working_days: {},
 });
 
 const actionLabel = computed(() => {
-  if (steps.value === 1) return t('provider.login.steps.step_1.action');
-  if (steps.value === 2) return t('provider.login.steps.step_2.action');
-  if (steps.value === 6) return t('provider.login.steps.step_6.action');
-  return t('provider.login.steps.step_3.action');
+  if (steps.value === 1) return t("provider.login.steps.step_1.action");
+  if (steps.value === 2) return t("provider.login.steps.step_2.action");
+  if (steps.value === 7) return t("provider.login.steps.step_6.action");
+  return t("provider.login.steps.step_3.action");
 });
 
+const requiredRule = (value) => !!value || t("validation.rules.required");
+
 const toISODate = (value) => {
-  const matches = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(value || '');
+  const matches = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(value || "");
   if (!matches) return null;
 
   return `${matches[3]}-${matches[2]}-${matches[1]}`;
@@ -150,14 +331,14 @@ const toISODate = (value) => {
 
 const mapWorkingDays = () => {
   const mapped = [];
-  const workingDays = stepSixForm.value.working_days || {};
+  const workingDays = stepSevenForm.value.working_days || {};
 
   Object.entries(workingDays).forEach(([dayKey, periods]) => {
     if (periods?.morning) {
-      mapped.push({ day: Number(dayKey), period: 'morning' });
+      mapped.push({ day: Number(dayKey), period: "morning" });
     }
     if (periods?.afternoon) {
-      mapped.push({ day: Number(dayKey), period: 'afternoon' });
+      mapped.push({ day: Number(dayKey), period: "afternoon" });
     }
   });
 
@@ -168,6 +349,11 @@ const hasWorkingDaySelected = () => {
   return mapWorkingDays().length > 0;
 };
 
+const normalizeCurrency = (value) => {
+  const numberValue = Number(value);
+  return Number.isFinite(numberValue) ? numberValue : 0;
+};
+
 const validateCurrentStep = async () => {
   const isValid = await loginForm.value?.validate();
 
@@ -182,17 +368,29 @@ const validateCurrentStep = async () => {
 
     if (!hasSelfie || !hasDocumentFront || !hasDocumentBack) {
       $q.notify({
-        type: 'negative',
-        message: t('provider.login.steps.step_4.upload_all_photos'),
+        type: "negative",
+        message: t("provider.login.steps.step_4.upload_all_photos"),
       });
       return false;
     }
   }
 
-  if (steps.value === 6 && !hasWorkingDaySelected()) {
+  if (steps.value === 5) {
+    const dailyPrice8h = normalizeCurrency(stepFiveForm.value.daily_price_8h);
+
+    if (dailyPrice8h < 100 || dailyPrice8h > 500) {
+      $q.notify({
+        type: "negative",
+        message: "Informe uma diária entre R$ 100,00 e R$ 500,00.",
+      });
+      return false;
+    }
+  }
+
+  if (steps.value === 7 && !hasWorkingDaySelected()) {
     $q.notify({
-      type: 'negative',
-      message: t('provider.login.steps.step_6.select_at_least_one'),
+      type: "negative",
+      message: t("provider.login.steps.step_6.select_at_least_one"),
     });
     return false;
   }
@@ -209,12 +407,17 @@ const sendValidationCode = async () => {
 };
 
 const validateCodeInput = async () => {
-  const response = await validateCode(email.value, phone.value, code.value, isLogin.value);
+  const response = await validateCode(
+    email.value,
+    phone.value,
+    code.value,
+    isLogin.value,
+  );
 
   if (response.status === 200) {
     if (isLogin.value === true) {
       await setAuthDataFromPayload(response.data.payload);
-      router.push({ name: 'DashboardPage' });
+      router.push({ name: "DashboardPage" });
       return;
     }
 
@@ -310,6 +513,12 @@ const onSubmit = async () => {
         break;
       }
       case 6: {
+        if (await validateCurrentStep()) {
+          steps.value = 7;
+        }
+        break;
+      }
+      case 7: {
         if (await validateCurrentStep()) {
           await registerUserAndProvider();
         }
@@ -329,7 +538,9 @@ const onSubmit = async () => {
 <style lang="scss" scoped>
 .fade-slide-enter-active,
 .fade-slide-leave-active {
-  transition: opacity 0.35s ease, transform 0.35s ease;
+  transition:
+    opacity 0.35s ease,
+    transform 0.35s ease;
 }
 .fade-slide-enter-from {
   opacity: 0;

+ 6 - 1
src/pages/dashboard/DashboardPage.vue

@@ -6,7 +6,10 @@
       </div>
     </template>
     <template v-else>
-      <DashboardHeaderBar :data="headerBar" />
+      <DashboardHeaderBar
+  :data="headerBar"
+  :notifications="notifications"
+/>
       <DashboardSummaryInfos :data="summaryInfos" />
       <DashboardPriceSuggest :data="priceSuggestion"/>
       <DashboardTodayServices v-if="todayServices?.length > 0" :data="todayServices" @refresh="loadDashboard" @rate="openRatingDialog" />
@@ -52,6 +55,7 @@ const solicitations = ref([]);
 const todayServices = ref([]);
 const nextSchedules = ref([]);
 const opportunities = ref([]);
+const notifications = ref([]);
 
 const showSuccessModal = ref(router.currentRoute.value.fullPath.includes('showSuccessModal') || false);
 
@@ -69,6 +73,7 @@ const loadDashboard = async () => {
     todayServices.value = response.todayServices ?? [];
     nextSchedules.value = response.nextSchedules ?? [];
     opportunities.value = response.opportunities ?? [];
+    notifications.value = response.notifications ?? [];
   }
 
   if( showSuccessModal.value == true) {

+ 274 - 0
src/pages/notifications/NotificationsPage.vue

@@ -0,0 +1,274 @@
+<template>
+  <q-page class="notifications-page">
+
+    <!-- HEADER -->
+    <div class="header row items-center">
+
+      <q-btn
+        flat
+        round
+        dense
+        icon="chevron_left"
+        class="back-btn"
+        @click="router.back()"
+      />
+
+      <div class="header-title">
+        {{ $t('notifications.title') }}
+      </div>
+
+    </div>
+
+    <!-- ACTIONS -->
+    <div class="actions row justify-between items-center">
+
+      <div class="unread-text">
+        {{ $t('notifications.unread') }} {{ unreadCount }}
+      </div>
+
+      <q-btn
+        flat
+        no-caps
+        class="mark-read-btn"
+        label="✓ Marcar todas como lidas"
+        @click="markAllAsRead"
+      />
+
+    </div>
+
+    <!-- LIST -->
+    <div class="notifications-list">
+
+       <q-card
+    v-for="item in notifications"
+    :key="item.id"
+    flat
+    clickable
+    class="notification-card"
+    :class="{ unread: !item.read }"
+    @click="markAsRead(item)"
+  >
+
+    <div class="row no-wrap items-start">
+
+      <!-- AVATAR -->
+      <q-avatar size="42px" class="q-mr-md">
+        <img :src="logoDiaria" />
+      </q-avatar>
+
+      <!-- CONTENT -->
+      <div class="col">
+
+        <div class="row justify-between items-start">
+
+          <div class="notification-title">
+            {{ item.title }}
+          </div>
+
+          <!-- STATUS -->
+          <div
+            class="status-dot"
+            :class="{ active: !item.read }"
+          />
+
+        </div>
+
+        <div class="notification-description">
+          {{ item.description }}
+        </div>
+
+        <div class="notification-time">
+          {{ item.time }}
+        </div>
+
+      </div>
+
+    </div>
+
+  </q-card>
+
+</div>
+
+  </q-page>
+</template>
+
+<script setup>
+
+import { computed, ref, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+
+import { api } from 'boot/axios'
+
+import logoDiaria from 'src/assets/logo_diaria_colorido_sem_texto.svg'
+
+const router = useRouter()
+
+const notifications = ref([])
+
+onMounted(() => {
+  loadNotifications()
+})
+
+const unreadCount = computed(() => {
+  return notifications.value.filter((n) => !n.read).length
+})
+
+const loadNotifications = async () => {
+  try {
+
+    const response = await api.get('/notifications')
+
+    notifications.value = response.data.payload || []
+
+  } catch (error) {
+    console.error(error)
+  }
+}
+
+const markAsRead = async (notification) => {
+  try {
+
+    await api.put(`/notifications/${notification.id}/read`)
+
+    notifications.value = notifications.value.map((item) => {
+
+      if (item.id === notification.id) {
+        return {
+          ...item,
+          read: true
+        }
+      }
+
+      return item
+    })
+
+    if (
+      notification.origin === 'schedule'
+      && notification.origin_id
+    ) {
+      router.push(`/schedule/${notification.origin_id}`)
+    }
+
+  } catch (error) {
+    console.error(error)
+  }
+}
+
+const markAllAsRead = async () => {
+  try {
+
+    await api.put('/notifications/read-all')
+
+    notifications.value = notifications.value.map((notification) => ({
+      ...notification,
+      read: true
+    }))
+
+  } catch (error) {
+    console.error(error)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.notifications-page {
+  background: #f5f5f7;
+  min-height: 100vh;
+}
+
+/* HEADER */
+.header {
+  position: relative;
+  padding: 18px 16px 12px;
+  background: white;
+}
+
+.back-btn {
+  position: absolute;
+  left: 10px;
+}
+
+.header-title {
+  width: 100%;
+  text-align: center;
+
+  font-size: 16px;
+  font-weight: 700;
+
+  color: #8B5CF6;
+}
+
+/* ACTIONS */
+.actions {
+  padding: 14px 16px;
+}
+
+.unread-text {
+  font-size: 13px;
+  color: #666;
+}
+
+.mark-read-btn {
+  font-size: 12px;
+  color: #ff4fd8;
+}
+
+/* LIST */
+.notifications-list {
+  display: flex;
+  flex-direction: column;
+}
+
+/* CARD */
+.notification-card {
+  border-radius: 0;
+  padding: 16px;
+  background: white;
+
+  border-bottom: 1px solid #ececec;
+
+  transition: 0.2s ease;
+}
+
+.notification-card.unread {
+  background: #f8eff7;
+}
+
+/* TITLE */
+.notification-title {
+  font-size: 14px;
+  font-weight: 700;
+  color: #555;
+}
+
+/* DESCRIPTION */
+.notification-description {
+  margin-top: 4px;
+
+  font-size: 12px;
+  line-height: 1.4;
+
+  color: #777;
+}
+
+/* TIME */
+.notification-time {
+  margin-top: 10px;
+
+  font-size: 11px;
+  color: #aaa;
+}
+
+/* STATUS */
+.status-dot {
+  width: 10px;
+  height: 10px;
+
+  border-radius: 50%;
+  background: #ddd;
+}
+
+.status-dot.active {
+  background: #ff5be1;
+}
+</style>

+ 213 - 182
src/pages/payments/PaymentsPage.vue

@@ -72,6 +72,9 @@
               color="secondary"
               class="btn-withdraw"
               :label="$t('provider.payments.btn_withdraw')"
+              :loading="withdrawLoading"
+              :disable="saldoDisponivel <= 0 || withdrawLoading"
+              @click="onSacar"
             />
           </div>
           <div class="text-caption text-grey-6">
@@ -114,77 +117,64 @@
         />
       </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 v-if="item.client_photo" :src="item.client_photo" style="object-fit:cover" />
-              <span v-else class="text-weight-bold full-width full-height flex flex-center" :style="avatarColors[item.id % avatarColors.length]" style="font-size:13px;border-radius:50%">{{ item.client_name?.slice(0,2).toUpperCase() ?? '??' }}</span>
-            </q-avatar>
+      <div v-if="servicesLoading" class="flex flex-center q-py-xl">
+        <q-spinner color="primary" size="40px" />
+      </div>
 
-            <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) }}
+      <template v-for="item in services" :key="item.id">
+        <q-card
+          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 v-if="item.client_photo" :src="item.client_photo" style="object-fit:cover" />
+              <span v-else class="text-weight-bold full-width full-height flex flex-center" :style="avatarColors[item.id % avatarColors.length]" style="font-size:13px;border-radius:50%">{{ item.client_name?.slice(0,2).toUpperCase() ?? '??' }}</span>
+              </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>
+                  {{ ' ' + item.date }}
+                </div>
+                <div class="text-date-regular">
+                  <span class="text-date-bold">{{ $t('provider.payments.services_date_payment') }}</span>
+                  {{ ' ' + item.payment_date }}
+                </div>
               </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 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">{{ item.period_label }}</span>
+              </div>
             </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>
+          </q-card-section>
+        </q-card>
+      </template>
     </div>
   </q-page>
 </template>
 
 <script setup>
-import { ref } from 'vue';
+import { ref, onMounted } 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 { getBalance, requestWithdrawal } from 'src/api/providerWithdrawal';
+import { getPaymentSplits } from 'src/api/paymentSplit';
 import MovimentacoesDialog from 'src/components/payments/MovimentacoesDialog.vue';
-import AntecipacaoDialog from 'src/components/payments/AntecipacaoDialog.vue';
-import AntecipacaoConfirmDialog from 'src/components/payments/AntecipacaoConfirmDialog.vue';
+import WithdrawConfirmDialog from 'src/components/payments/WithdrawConfirmDialog.vue';
 
 const $q = useQuasar();
 const { t } = useI18n();
@@ -197,6 +187,8 @@ const avatarColors = [
 ];
 const earningsExpanded = ref(false);
 const selectedPeriod = ref('week');
+const servicesLoading = ref(true);
+const withdrawLoading = ref(false);
 
 const periods = [
   { key: 'week',  labelKey: 'provider.payments.period_week'  },
@@ -206,125 +198,187 @@ const periods = [
 
 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 earningsByPeriod = ref({
+  week:  { value: 0, count: 0 },
+  month: { value: 0, count: 0 },
+  year:  { value: 0, count: 0 },
+});
+
+const saldoDisponivel = ref(0);
+const saldoALiberar = ref(0);
+const services = ref([]);
+
+const parseAmount = (value) => {
+  const amount = Number.parseFloat(value);
+  return Number.isFinite(amount) ? amount : 0;
+};
+
+const splitAmount = (split) => (
+  parseAmount(split.provider_amount ?? split.net_amount ?? split.gross_amount)
+);
+
+const splitPaymentStatus = (split) => (
+  split.payment_status ?? split.payment?.status ?? 'pending'
+);
+
+const splitScheduleStatus = (split) => (
+  split.schedule_status ?? split.schedule?.status ?? split.scheduleStatus ?? ''
+);
+
+const splitCodeVerified = (split) => (
+  split.code_verified ?? split.schedule?.code_verified ?? false
+);
+
+const isTruthyFlag = (value) => (
+  value === true || value === 1 || value === '1' || value === 'true'
+);
+
+const isSplitPaidAndFinished = (split) => (
+  splitPaymentStatus(split) === 'paid'
+  && (!splitScheduleStatus(split) || splitScheduleStatus(split) === 'finished')
+  && isTruthyFlag(splitCodeVerified(split))
+);
+
+const splitReleaseDate = (split) => (
+  split.available_at
+  ?? split.release_at
+  ?? split.released_at
+  ?? split.withdraw_available_at
+  ?? null
+);
+
+const isSplitAvailable = (split) => {
+  if (!isSplitPaidAndFinished(split)) return false;
+
+  const releaseAt = splitReleaseDate(split);
+  if (!releaseAt) return true;
+
+  const releaseDate = parseDateSafe(releaseAt);
+  return !releaseDate || releaseDate <= new Date();
 };
 
-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 sumSplits = (splits, predicate) => (
+  splits.reduce((total, split) => total + (predicate(split) ? splitAmount(split) : 0), 0)
+);
+
+const loadData = async () => {
+  servicesLoading.value = true;
+  try {
+    const [balance, splits] = await Promise.all([
+      getBalance(),
+      getPaymentSplits(),
+    ]);
+    const paymentSplits = splits || [];
+    const availableFromSplits = sumSplits(paymentSplits, isSplitAvailable);
+    const pendingFromSplits = sumSplits(
+      paymentSplits,
+      (split) => isSplitPaidAndFinished(split) && !isSplitAvailable(split)
+    );
+
+    saldoDisponivel.value = parseAmount(balance?.available) || availableFromSplits;
+    saldoALiberar.value = parseAmount(balance?.pending) || pendingFromSplits;
+
+    services.value = paymentSplits.map((s) => ({
+      id: s.id,
+      client_name: s.client_name ?? t('provider.payments.default_client_name'),
+      client_photo: null,
+      date: formatServiceDate(s.schedule_date),
+      payment_date: formatIsoDate(s.payment_paid_at),
+      total_amount: splitAmount(s),
+      period_label: s.schedule_period_type
+        ? t(labelsPeriodTypes.find(l => l.value == s.schedule_period_type)?.label)
+        : '',
+      schedule_type: 'default',
+      payment_status: splitPaymentStatus(s),
+    }));
+  } catch (error) {
+    $q.notify({ type: 'negative', message: error?.response?.data?.message || error.message });
+  } finally {
+    servicesLoading.value = false;
+  }
 };
 
-const formatShortDate = (dateStr) => {
-  const d = parseLocalDate(dateStr);
-  if (!d) return '';
-  return d.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
+const onSacar = () => {
+  $q.dialog({
+    component: WithdrawConfirmDialog,
+    componentProps: { amount: saldoDisponivel.value },
+  }).onOk(async () => {
+    withdrawLoading.value = true;
+    try {
+      await requestWithdrawal();
+      saldoDisponivel.value = 0;
+      // notification handled by axios interceptor
+    } catch (error) {
+      $q.notify({ type: 'negative', message: error?.response?.data?.message || error.message });
+    } finally {
+      withdrawLoading.value = false;
+    }
+  });
+};
+
+const parseDateSafe = (value) => {
+  if (!value) return null;
+  const raw = String(value).trim();
+
+  // yyyy-mm-dd (from API date fields)
+  if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
+    const date = new Date(`${raw}T12:00:00`);
+    return Number.isNaN(date.getTime()) ? null : date;
+  }
+
+  // dd/mm/yyyy
+  const brMatch = raw.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
+  if (brMatch) {
+    const [, day, month, year] = brMatch;
+    const date = new Date(`${year}-${month}-${day}T12:00:00`);
+    return Number.isNaN(date.getTime()) ? null : date;
+  }
+
+  const date = new Date(raw);
+  return Number.isNaN(date.getTime()) ? null : date;
 };
 
-const periodLabel = (periodType) => {
-  const found = labelsPeriodTypes.find(l => l.value == periodType);
-  return found ? t(found.label) : '';
+const formatShortDate = (dateStr) => {
+  const date = parseDateSafe(dateStr);
+  if (!date) return '';
+  return date.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
 };
 
+const formatServiceDate = (dateStr) => formatShortDate(dateStr);
+const formatIsoDate = (dateStr) => formatShortDate(dateStr);
+
 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'),
+    pending:    t('provider.payments.pay_status_pending'),
+    paid:       t('provider.payments.pay_status_paid'),
+    authorized: t('provider.payments.pay_status_authorized'),
+    processing: t('provider.payments.pay_status_processing'),
+    failed:     t('provider.payments.pay_status_failed'),
+    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',
+    pending:    'warning-bg',
+    paid:       'success-bg',
+    authorized: 'teal-bg',
+    processing: 'info-bg',
+    failed:     'error-bg',
+    cancelled:  'neutral-bg',
   };
   return map[status] ?? 'neutral-bg';
 };
 
 const payStatusTextColor = (status) => {
   const map = {
-    pending:     'warning',
-    paid:        'success',
-    anticipated: 'teal',
-    cancelled:   'status-finished',
+    pending:    'warning',
+    paid:       'success',
+    authorized: 'teal',
+    processing: 'info',
+    failed:     'error-dark',
+    cancelled:  'status-finished',
   };
   return map[status] ?? 'text';
 };
@@ -333,17 +387,9 @@ 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';
-    });
-  });
-};
+onMounted(() => {
+  loadData();
+});
 </script>
 
 <style scoped lang="scss">
@@ -389,20 +435,6 @@ const openAntecipacaoDialog = (item) => {
   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;
@@ -448,9 +480,8 @@ const openAntecipacaoDialog = (item) => {
   padding: 2px 2px;
 }
 
-.btn-anticipate {
-  font-size: 11px;
+.section-title {
+  font-size: 15px;
   font-weight: 700;
-  padding: 3px 10px;
 }
 </style>

+ 11 - 0
src/router/routes/notifications.route.js

@@ -0,0 +1,11 @@
+export default [
+  {
+    path: '/notifications',
+    name: 'NotificationsPage',
+    component: () => import('src/pages/notifications/NotificationsPage.vue'),
+    meta: {
+      title: 'Notificações',
+      requireAuth: true
+    }
+  }
+]