Explorar o código

refactor: ajuste para mostrar erros dos campos + q-scroll-area nos forms em dialogs

Gustavo Mantovani hai 1 semana
pai
achega
20a16caff8
Modificáronse 32 ficheiros con 2116 adicións e 1669 borrados
  1. 3 2
      src/components/defaults/DefaultForm.vue
  2. 104 65
      src/components/financial/AddAccountReceivableDialog.vue
  3. 69 47
      src/components/financial/AddCompanyPayableDialog.vue
  4. 69 50
      src/components/financial/AddEditTreasuryAccountDialog.vue
  5. 86 65
      src/components/financial/AddFinancialPlanAccountDialog.vue
  6. 74 49
      src/components/financial/AddTreasuryLaunchDialog.vue
  7. 34 25
      src/components/financial/FinancialAuthDialog.vue
  8. 55 24
      src/components/financial/SettleAccountReceivableDialog.vue
  9. 52 30
      src/components/financial/SettleCompanyPayableDialog.vue
  10. 6 23
      src/composables/useSubmitHandler.js
  11. 40 6
      src/css/app.scss
  12. 27 25
      src/pages/cadastros/components/AddEditGroupDialog.vue
  13. 1 0
      src/pages/financial/AsaasIntegrationPage.vue
  14. 233 213
      src/pages/kanban/components/AddEditKanbanDialog.vue
  15. 2 0
      src/pages/login/LoginPage.vue
  16. 407 394
      src/pages/packages/components/AddEditPackageDialog.vue
  17. 26 20
      src/pages/permissions/components/AddEditPermissionGroupDialog.vue
  18. 55 49
      src/pages/products/components/AddEditProductDialog.vue
  19. 77 72
      src/pages/products/components/AddEditStockProductDialog.vue
  20. 48 44
      src/pages/products/components/AddProductDialog.vue
  21. 13 9
      src/pages/support/components/AddEditReplyDialog.vue
  22. 117 108
      src/pages/support/components/AddEditTicketDialog.vue
  23. 14 9
      src/pages/support/components/AddReplyDialog.vue
  24. 50 48
      src/pages/tbr/components/AddEditInhabitantClassificationDialog.vue
  25. 69 57
      src/pages/tbr/components/AddEditTbrDialog.vue
  26. 14 9
      src/pages/tbr/components/GenerateBillingsDialog.vue
  27. 51 29
      src/pages/unit/components/AddEditHistoryDialog.vue
  28. 193 150
      src/pages/unit/components/AddEditPartnerDialog.vue
  29. 56 33
      src/pages/unit/components/AddMediaDialog.vue
  30. 36 2
      src/pages/unit/tabs/UnitDataTab.vue
  31. 18 11
      src/pages/user_types/components/AddUserTypeDialog.vue
  32. 17 1
      src/pages/users/UserActionPage.vue

+ 3 - 2
src/components/defaults/DefaultForm.vue

@@ -1,11 +1,12 @@
 <template>
-  <q-form ref="formRef" v-bind="$attrs" @validation-error="onValidationError">
+  <QForm ref="formRef" v-bind="$attrs" @validation-error="onValidationError">
     <slot />
-  </q-form>
+  </QForm>
 </template>
 
 <script setup>
 import { useTemplateRef } from "vue";
+import { QForm } from "quasar";
 import { useFormUpdateTracker } from "src/composables/useFormUpdateTracker";
 
 defineOptions({ inheritAttrs: false });

+ 104 - 65
src/components/financial/AddAccountReceivableDialog.vue

@@ -1,78 +1,111 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <q-card class="q-dialog-plugin overflow-hidden" style="width: 520px; max-width: 90vw">
-      <DefaultDialogHeader title="Nova conta a receber" @close="onDialogCancel" />
+    <q-card
+      class="q-dialog-plugin overflow-hidden"
+      style="width: 520px; max-width: 90vw"
+    >
+      <DefaultDialogHeader
+        title="Nova conta a receber"
+        @close="onDialogCancel"
+      />
 
       <DefaultForm ref="formRef" @submit.prevent="onSubmit">
-        <q-card-section class="row q-col-gutter-md q-pt-none">
-          <DefaultInput
-            v-model="form.history"
-            label="Descrição"
-            class="col-12"
-            lazy-rules="ondemand"
-            :rules="[(value) => !!value || 'Informe a descrição']"
-          />
+        <q-scroll-area class="form-dialog-scroll-area">
+          <q-card-section class="row q-col-gutter-md q-pt-none">
+            <DefaultInput
+              v-model="form.history"
+              v-model:error="validationErrors.history"
+              :error-message="validationErrors.history"
+              label="Descrição"
+              class="col-12"
+              lazy-rules="ondemand"
+              :rules="[(value) => !!value || 'Informe a descrição']"
+            />
 
-          <div class="col-12">
-            <div class="text-caption text-grey-8 q-mb-xs">Tipo da conta</div>
-            <q-option-group
-              v-model="form.account_type"
-              :options="accountTypeOptions"
-              color="primary"
-              inline
-              type="radio"
+            <div class="col-12">
+              <div class="text-caption text-grey-8 q-mb-xs">Tipo da conta</div>
+              <q-option-group
+                v-model="form.account_type"
+                :options="accountTypeOptions"
+                color="primary"
+                inline
+                type="radio"
+              />
+            </div>
+
+            <UnitSelect
+              v-if="form.account_type === 'unit'"
+              v-model="form.unit"
+              v-model:error="validationErrors.unit_id"
+              :error-message="validationErrors.unit_id"
+              label="Unidade"
+              class="col-12"
+              :rules="[(value) => !!value || 'Selecione a unidade']"
             />
-          </div>
-
-          <UnitSelect
-            v-if="form.account_type === 'unit'"
-            v-model="form.unit"
-            label="Unidade"
-            class="col-12"
-            :rules="[(value) => !!value || 'Selecione a unidade']"
-          />
 
-          <DefaultCurrencyInput
-            v-model="form.value"
-            label="Valor"
-            class="col-12 col-sm-6"
-            lazy-rules="ondemand"
-            :rules="[() => Number(form.value) > 0 || 'Informe o valor']"
-          />
+            <DefaultCurrencyInput
+              v-model="form.value"
+              v-model:error="validationErrors.value"
+              :error-message="validationErrors.value"
+              label="Valor"
+              class="col-12 col-sm-6"
+              lazy-rules="ondemand"
+              :rules="[() => Number(form.value) > 0 || 'Informe o valor']"
+            />
 
-          <DefaultInputDatePicker
-            v-model="form.due_date"
-            v-model:untreated-date="form.due_date_iso"
-            label="Data de vencimento"
-            class="col-12 col-sm-6"
-            lazy-rules="ondemand"
-            :rules="[(value) => !!value || 'Informe o vencimento']"
-          />
+            <DefaultInputDatePicker
+              v-model="form.due_date"
+              v-model:untreated-date="form.due_date_iso"
+              v-model:error="validationErrors.due_date"
+              :error-message="validationErrors.due_date"
+              label="Data de vencimento"
+              class="col-12 col-sm-6"
+              lazy-rules="ondemand"
+              :rules="[(value) => !!value || 'Informe o vencimento']"
+            />
 
-          <DefaultSelect
-            v-model="form.financial_plan_account_id"
-            label="Plano de contas"
-            :options="planAccountOptions"
-            class="col-12"
-            clearable
-            emit-value
-            map-options
-            :loading="loadingPlanAccounts"
-          />
+            <DefaultSelect
+              v-model="form.financial_plan_account_id"
+              v-model:error="validationErrors.financial_plan_account_id"
+              :error-message="validationErrors.financial_plan_account_id"
+              label="Plano de contas"
+              :options="planAccountOptions"
+              class="col-12"
+              clearable
+              emit-value
+              map-options
+              :loading="loadingPlanAccounts"
+            />
 
-          <DefaultInput
-            v-model="form.obs"
-            label="Observação"
-            type="textarea"
-            class="col-12"
-            rows="3"
-          />
-        </q-card-section>
+            <DefaultInput
+              v-model="form.obs"
+              v-model:error="validationErrors.obs"
+              :error-message="validationErrors.obs"
+              label="Observação"
+              type="textarea"
+              class="col-12"
+              rows="3"
+            />
+          </q-card-section>
+        </q-scroll-area>
 
         <q-separator />
         <q-card-actions align="right" class="q-pa-md q-gutter-sm">
-          <q-btn outline label="Cancelar" color="negative" no-caps @click="onDialogCancel" />
-          <q-btn color="primary" label="Salvar" type="submit" no-caps unelevated :loading="loading" />
+          <q-btn
+            outline
+            label="Cancelar"
+            color="negative"
+            no-caps
+            @click="onDialogCancel"
+          />
+          <q-btn
+            color="primary"
+            label="Salvar"
+            type="submit"
+            no-caps
+            unelevated
+            :loading="loading"
+          />
         </q-card-actions>
       </DefaultForm>
     </q-card>
@@ -95,7 +128,8 @@ import UnitSelect from "src/components/selects/UnitSelect.vue";
 
 defineEmits([...useDialogPluginComponent.emits]);
 
-const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
 const today = new Date();
 const dueDateIso = [
   today.getFullYear(),
@@ -129,7 +163,11 @@ watch(
   },
 );
 
-const { loading, execute: submitForm } = useSubmitHandler({
+const {
+  loading,
+  validationErrors,
+  execute: submitForm,
+} = useSubmitHandler({
   formRef,
   onSuccess: (response) => onDialogOK(response),
 });
@@ -151,7 +189,8 @@ const onSubmit = async () => {
   await submitForm(() =>
     createFranchiseeReceivable({
       account_type: form.value.account_type,
-      unit_id: form.value.account_type === "unit" ? form.value.unit?.value : null,
+      unit_id:
+        form.value.account_type === "unit" ? form.value.unit?.value : null,
       history: form.value.history,
       value: form.value.value,
       due_date: form.value.due_date_iso,

+ 69 - 47
src/components/financial/AddCompanyPayableDialog.vue

@@ -1,58 +1,79 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <q-card class="q-dialog-plugin overflow-hidden" style="width: 500px; max-width: 90vw">
+    <q-card
+      class="q-dialog-plugin overflow-hidden"
+      style="width: 500px; max-width: 90vw"
+    >
       <DefaultDialogHeader title="Nova conta a pagar" @close="onDialogCancel" />
 
       <DefaultForm ref="formRef" @submit.prevent="onSubmit">
-        <q-card-section class="row q-col-gutter-md q-pt-none">
-          <DefaultInput
-            v-model="form.history"
-            label="Descrição"
-            class="col-12"
-            lazy-rules="ondemand"
-            :rules="[(value) => !!value || 'Informe a descrição']"
-          />
-
-          <DefaultCurrencyInput
-            v-model="form.value"
-            label="Valor"
-            class="col-12 col-sm-6"
-            lazy-rules="ondemand"
-            :rules="[() => Number(form.value) > 0 || 'Informe o valor']"
-          />
-
-          <DefaultInputDatePicker
-            v-model="form.due_date"
-            v-model:untreated-date="form.due_date_iso"
-            label="Vencimento"
-            class="col-12 col-sm-6"
-            lazy-rules="ondemand"
-            :rules="[(value) => !!value || 'Informe o vencimento']"
-          />
-
-          <DefaultSelect
-            v-model="form.financial_plan_account_id"
-            label="Plano de contas (opcional)"
-            :options="planAccountOptions"
-            class="col-12"
-            clearable
-            emit-value
-            map-options
-            :loading="loadingPlanAccounts"
-          />
-
-          <DefaultInput
-            v-model="form.obs"
-            label="Observação (opcional)"
-            type="textarea"
-            class="col-12"
-            rows="3"
-          />
-        </q-card-section>
+        <q-scroll-area class="form-dialog-scroll-area">
+          <q-card-section class="row q-col-gutter-md q-pt-none">
+            <DefaultInput
+              v-model="form.history"
+              v-model:error="validationErrors.history"
+              :error-message="validationErrors.history"
+              label="Descrição"
+              class="col-12"
+              lazy-rules="ondemand"
+              :rules="[(value) => !!value || 'Informe a descrição']"
+            />
+
+            <DefaultCurrencyInput
+              v-model="form.value"
+              v-model:error="validationErrors.value"
+              :error-message="validationErrors.value"
+              label="Valor"
+              class="col-12 col-sm-6"
+              lazy-rules="ondemand"
+              :rules="[() => Number(form.value) > 0 || 'Informe o valor']"
+            />
+
+            <DefaultInputDatePicker
+              v-model="form.due_date"
+              v-model:untreated-date="form.due_date_iso"
+              v-model:error="validationErrors.due_date"
+              :error-message="validationErrors.due_date"
+              label="Vencimento"
+              class="col-12 col-sm-6"
+              lazy-rules="ondemand"
+              :rules="[(value) => !!value || 'Informe o vencimento']"
+            />
+
+            <DefaultSelect
+              v-model="form.financial_plan_account_id"
+              v-model:error="validationErrors.financial_plan_account_id"
+              :error-message="validationErrors.financial_plan_account_id"
+              label="Plano de contas (opcional)"
+              :options="planAccountOptions"
+              class="col-12"
+              clearable
+              emit-value
+              map-options
+              :loading="loadingPlanAccounts"
+            />
+
+            <DefaultInput
+              v-model="form.obs"
+              v-model:error="validationErrors.obs"
+              :error-message="validationErrors.obs"
+              label="Observação (opcional)"
+              type="textarea"
+              class="col-12"
+              rows="3"
+            />
+          </q-card-section>
+        </q-scroll-area>
 
         <q-separator />
         <q-card-actions align="right" class="q-pa-md q-gutter-sm">
-          <q-btn outline label="Cancelar" color="negative" no-caps @click="onDialogCancel" />
+          <q-btn
+            outline
+            label="Cancelar"
+            color="negative"
+            no-caps
+            @click="onDialogCancel"
+          />
           <q-btn
             color="primary"
             label="Salvar"
@@ -107,6 +128,7 @@ const loadingPlanAccounts = ref(false);
 
 const {
   loading,
+  validationErrors,
   execute: submitForm,
 } = useSubmitHandler({
   formRef,

+ 69 - 50
src/components/financial/AddEditTreasuryAccountDialog.vue

@@ -1,58 +1,73 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <q-card class="q-dialog-plugin overflow-hidden" style="width: 600px; max-width: 90vw">
+    <q-card
+      class="q-dialog-plugin overflow-hidden"
+      style="width: 600px; max-width: 90vw"
+    >
       <DefaultDialogHeader :title="title" @close="onDialogCancel" />
       <DefaultForm ref="formRef" @submit="onOKClick">
-        <q-card-section class="row q-col-gutter-sm q-pt-none">
-          <DefaultInput
-            v-model="form.name"
-            v-model:error="validationErrors.name"
-            :rules="[inputRules.required]"
-            label="Nome da conta"
-            placeholder="Ex. Conta Principal"
-            class="col-12"
-          />
-
-          <DefaultInput
-            v-model="form.bank_name"
-            v-model:error="validationErrors.bank_name"
-            :rules="[inputRules.required]"
-            label="Banco"
-            placeholder="Ex. Banco do Brasil"
-            class="col-12 col-md-6"
-          />
-
-          <DefaultSelect
-            v-model="form.bank_type_account"
-            v-model:error="validationErrors.bank_type_account"
-            :options="accountTypeOptions"
-            emit-value
-            map-options
-            label="Tipo de conta"
-            placeholder="Selecione"
-            class="col-12 col-md-6"
-          />
-
-          <DefaultInput
-            v-model="form.bank_agency"
-            v-model:error="validationErrors.bank_agency"
-            label="Agência"
-            placeholder="Ex. 1234"
-            class="col-12 col-md-6"
-          />
-
-          <DefaultInput
-            v-model="form.bank_account"
-            v-model:error="validationErrors.bank_account"
-            label="Conta"
-            placeholder="Ex. 56789-0"
-            class="col-12 col-md-6"
-          />
-        </q-card-section>
+        <q-scroll-area class="form-dialog-scroll-area">
+          <q-card-section class="row q-col-gutter-sm q-pt-none">
+            <DefaultInput
+              v-model="form.name"
+              v-model:error="validationErrors.name"
+              :error-message="validationErrors.name"
+              :rules="[inputRules.required]"
+              label="Nome da conta"
+              placeholder="Ex. Conta Principal"
+              class="col-12"
+            />
+
+            <DefaultInput
+              v-model="form.bank_name"
+              v-model:error="validationErrors.bank_name"
+              :error-message="validationErrors.bank_name"
+              :rules="[inputRules.required]"
+              label="Banco"
+              placeholder="Ex. Banco do Brasil"
+              class="col-12 col-md-6"
+            />
+
+            <DefaultSelect
+              v-model="form.bank_type_account"
+              v-model:error="validationErrors.bank_type_account"
+              :error-message="validationErrors.bank_type_account"
+              :options="accountTypeOptions"
+              emit-value
+              map-options
+              label="Tipo de conta"
+              placeholder="Selecione"
+              class="col-12 col-md-6"
+            />
+
+            <DefaultInput
+              v-model="form.bank_agency"
+              v-model:error="validationErrors.bank_agency"
+              :error-message="validationErrors.bank_agency"
+              label="Agência"
+              placeholder="Ex. 1234"
+              class="col-12 col-md-6"
+            />
+
+            <DefaultInput
+              v-model="form.bank_account"
+              v-model:error="validationErrors.bank_account"
+              :error-message="validationErrors.bank_account"
+              label="Conta"
+              placeholder="Ex. 56789-0"
+              class="col-12 col-md-6"
+            />
+          </q-card-section>
+        </q-scroll-area>
 
         <q-card-actions>
           <q-space />
-          <q-btn outline color="negative" label="Cancelar" @click="onDialogCancel" />
+          <q-btn
+            outline
+            color="negative"
+            label="Cancelar"
+            @click="onDialogCancel"
+          />
           <q-btn
             color="primary"
             :label="isEdit ? 'Salvar' : 'Cadastrar'"
@@ -70,7 +85,10 @@ import { ref, computed } from "vue";
 import { useDialogPluginComponent } from "quasar";
 import { useInputRules } from "src/composables/useInputRules";
 import { useSubmitHandler } from "src/composables/useSubmitHandler";
-import { createTreasuryAccount, updateTreasuryAccount } from "src/api/treasury_account";
+import {
+  createTreasuryAccount,
+  updateTreasuryAccount,
+} from "src/api/treasury_account";
 
 import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
 import DefaultInput from "src/components/defaults/DefaultInput.vue";
@@ -87,7 +105,8 @@ const { account } = defineProps({
 
 const { inputRules } = useInputRules();
 
-const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
 
 const formRef = ref(null);
 const isEdit = computed(() => !!account);

+ 86 - 65
src/components/financial/AddFinancialPlanAccountDialog.vue

@@ -1,75 +1,95 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <q-card class="q-dialog-plugin overflow-hidden" style="width: 520px; max-width: 90vw">
+    <q-card
+      class="q-dialog-plugin overflow-hidden"
+      style="width: 520px; max-width: 90vw"
+    >
       <DefaultDialogHeader title="Nova Conta" @close="onDialogCancel" />
       <DefaultForm ref="formRef" @submit="onOKClick">
-        <q-card-section class="q-pt-none">
-          <!-- Tipo: conta pai (raiz) ou conta filha (vinculada a um pai) -->
-          <q-option-group
-            v-model="accountKind"
-            :options="kindOptions"
-            color="primary"
-            inline
-            class="q-mb-sm"
-          />
-
-          <div class="row q-col-gutter-sm">
-            <DefaultInput
-              v-model="form.code"
-              v-model:error="validationErrors.code"
-              :rules="[inputRules.required]"
-              label="Código"
-              :placeholder="accountKind === 'child' ? '3.1.1' : '3'"
-              class="col-12 col-md-4"
-            />
-
-            <DefaultInput
-              v-model="form.description"
-              v-model:error="validationErrors.description"
-              :rules="[inputRules.required]"
-              label="Nome da Conta"
-              placeholder="Ex. Contas a Pagar"
-              class="col-12 col-md-8"
-            />
-
-            <!-- Conta pai: define o tipo -->
-            <DefaultInput
-              v-if="accountKind === 'parent'"
-              v-model="form.chart_type"
-              v-model:error="validationErrors.chart_type"
-              :rules="[inputRules.required]"
-              label="Tipo de Conta"
-              placeholder="Ex. Ativo, Passivo, Receitas, Despesas"
-              class="col-12"
-            />
-
-            <!-- Conta filha: escolhe o pai (herda o tipo) -->
-            <DefaultSelect
-              v-else
-              v-model="form.parent_id"
-              :options="parentOptions"
-              option-value="id"
-              option-label="label"
-              emit-value
-              map-options
-              :loading="loadingParents"
-              :rules="[inputRules.required]"
-              label="Conta Pai"
-              class="col-12"
+        <q-scroll-area class="form-dialog-scroll-area">
+          <q-card-section class="q-pt-none">
+            <!-- Tipo: conta pai (raiz) ou conta filha (vinculada a um pai) -->
+            <q-option-group
+              v-model="accountKind"
+              :options="kindOptions"
+              color="primary"
+              inline
+              class="q-mb-sm"
             />
-          </div>
 
-          <div
-            v-if="accountKind === 'child' && selectedParent"
-            class="text-caption text-grey-7 q-mt-xs"
-          >
-            Tipo herdado do pai: <b>{{ selectedParent.chart_type }}</b>
-          </div>
-        </q-card-section>
+            <div class="row q-col-gutter-sm">
+              <DefaultInput
+                v-model="form.code"
+                v-model:error="validationErrors.code"
+                :error-message="validationErrors.code"
+                :rules="[inputRules.required]"
+                label="Código"
+                :placeholder="accountKind === 'child' ? '3.1.1' : '3'"
+                class="col-12 col-md-4"
+              />
+
+              <DefaultInput
+                v-model="form.description"
+                v-model:error="validationErrors.description"
+                :error-message="validationErrors.description"
+                :rules="[inputRules.required]"
+                label="Nome da Conta"
+                placeholder="Ex. Contas a Pagar"
+                class="col-12 col-md-8"
+              />
+
+              <!-- Conta pai: define o tipo -->
+              <DefaultInput
+                v-if="accountKind === 'parent'"
+                v-model="form.chart_type"
+                v-model:error="validationErrors.chart_type"
+                :error-message="validationErrors.chart_type"
+                :rules="[inputRules.required]"
+                label="Tipo de Conta"
+                placeholder="Ex. Ativo, Passivo, Receitas, Despesas"
+                class="col-12"
+              />
+
+              <!-- Conta filha: escolhe o pai (herda o tipo) -->
+              <DefaultSelect
+                v-else
+                v-model="form.parent_id"
+                v-model:error="validationErrors.parent_id"
+                :error-message="validationErrors.parent_id"
+                :options="parentOptions"
+                option-value="id"
+                option-label="label"
+                emit-value
+                map-options
+                :loading="loadingParents"
+                :rules="[inputRules.required]"
+                label="Conta Pai"
+                class="col-12"
+              />
+            </div>
+
+            <div
+              v-if="accountKind === 'child' && selectedParent"
+              class="text-caption text-grey-7 q-mt-xs"
+            >
+              Tipo herdado do pai: <b>{{ selectedParent.chart_type }}</b>
+            </div>
+          </q-card-section>
+        </q-scroll-area>
 
         <q-card-actions align="right">
-          <q-btn outline color="negative" label="Cancelar" @click="onDialogCancel" />
-          <q-btn color="primary" label="Salvar" type="submit" :loading="loading" />
+          <q-btn
+            outline
+            color="negative"
+            label="Cancelar"
+            @click="onDialogCancel"
+          />
+          <q-btn
+            color="primary"
+            label="Salvar"
+            type="submit"
+            :loading="loading"
+          />
         </q-card-actions>
       </DefaultForm>
     </q-card>
@@ -94,7 +114,8 @@ defineEmits([...useDialogPluginComponent.emits]);
 
 const { inputRules } = useInputRules();
 
-const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
 
 const formRef = ref(null);
 

+ 74 - 49
src/components/financial/AddTreasuryLaunchDialog.vue

@@ -1,56 +1,75 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <q-card class="q-dialog-plugin overflow-hidden" style="width: 520px; max-width: 90vw">
+    <q-card
+      class="q-dialog-plugin overflow-hidden"
+      style="width: 520px; max-width: 90vw"
+    >
       <DefaultDialogHeader :title="title" @close="onDialogCancel" />
       <DefaultForm ref="formRef" @submit="onOKClick">
-        <q-card-section class="row q-col-gutter-sm q-pt-none">
-          <DefaultSelect
-            v-model="form.transaction_type"
-            v-model:error="validationErrors.transaction_type"
-            :options="typeOptions"
-            emit-value
-            map-options
-            :rules="[inputRules.required]"
-            label="Tipo"
-            placeholder="Entrada ou Saída"
-            class="col-12"
-          />
+        <q-scroll-area class="form-dialog-scroll-area">
+          <q-card-section class="row q-col-gutter-sm q-pt-none">
+            <DefaultSelect
+              v-model="form.transaction_type"
+              v-model:error="validationErrors.transaction_type"
+              :error-message="validationErrors.transaction_type"
+              :options="typeOptions"
+              emit-value
+              map-options
+              :rules="[inputRules.required]"
+              label="Tipo"
+              placeholder="Entrada ou Saída"
+              class="col-12"
+            />
+
+            <DefaultInput
+              v-model="form.description"
+              v-model:error="validationErrors.description"
+              :error-message="validationErrors.description"
+              :rules="[inputRules.required]"
+              label="Descrição"
+              placeholder="Descreva a movimentação"
+              class="col-12"
+            />
+
+            <DefaultSelect
+              v-model="form.financial_plan_account_id"
+              v-model:error="validationErrors.financial_plan_account_id"
+              :error-message="validationErrors.financial_plan_account_id"
+              :options="planAccountOptions"
+              :loading="loadingPlanAccounts"
+              emit-value
+              map-options
+              use-input
+              input-debounce="0"
+              :rules="[inputRules.required]"
+              label="Plano de Contas"
+              class="col-12"
+              @filter="filterPlanAccounts"
+            />
+
+            <DefaultCurrencyInput
+              v-model="form.amount"
+              v-model:error="validationErrors.amount"
+              :error-message="validationErrors.amount"
+              label="Valor"
+              class="col-12"
+            />
+          </q-card-section>
+        </q-scroll-area>
 
-          <DefaultInput
-            v-model="form.description"
-            v-model:error="validationErrors.description"
-            :rules="[inputRules.required]"
-            label="Descrição"
-            placeholder="Descreva a movimentação"
-            class="col-12"
+        <q-card-actions align="right">
+          <q-btn
+            outline
+            color="negative"
+            label="Cancelar"
+            @click="onDialogCancel"
           />
-
-          <DefaultSelect
-            v-model="form.financial_plan_account_id"
-            v-model:error="validationErrors.financial_plan_account_id"
-            :options="planAccountOptions"
-            :loading="loadingPlanAccounts"
-            emit-value
-            map-options
-            use-input
-            input-debounce="0"
-            :rules="[inputRules.required]"
-            label="Plano de Contas"
-            class="col-12"
-            @filter="filterPlanAccounts"
+          <q-btn
+            color="primary"
+            label="Salvar"
+            type="submit"
+            :loading="loading"
           />
-
-          <DefaultCurrencyInput
-            v-model="form.amount"
-            v-model:error="validationErrors.amount"
-            label="Valor"
-            class="col-12"
-          />
-        </q-card-section>
-
-        <q-card-actions align="right">
-          <q-btn outline color="negative" label="Cancelar" @click="onDialogCancel" />
-          <q-btn color="primary" label="Salvar" type="submit" :loading="loading" />
         </q-card-actions>
       </DefaultForm>
     </q-card>
@@ -62,7 +81,10 @@ import { ref, onMounted } from "vue";
 import { useDialogPluginComponent } from "quasar";
 import { useInputRules } from "src/composables/useInputRules";
 import { useSubmitHandler } from "src/composables/useSubmitHandler";
-import { createTreasuryLaunch, updateTreasuryLaunch } from "src/api/treasury_launch";
+import {
+  createTreasuryLaunch,
+  updateTreasuryLaunch,
+} from "src/api/treasury_launch";
 import { getFinancialPlanAccountsForSelect } from "src/api/financial_plan_account";
 
 import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
@@ -85,7 +107,8 @@ const { account, launch } = defineProps({
 
 const { inputRules } = useInputRules();
 
-const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
 
 const formRef = ref(null);
 const isEdit = !!launch;
@@ -111,7 +134,9 @@ const filterPlanAccounts = (needle, update) => {
   update(() => {
     const term = needle.toLowerCase();
     planAccountOptions.value = term
-      ? allPlanAccounts.value.filter((o) => o.label.toLowerCase().includes(term))
+      ? allPlanAccounts.value.filter((o) =>
+          o.label.toLowerCase().includes(term),
+        )
       : allPlanAccounts.value;
   });
 };

+ 34 - 25
src/components/financial/FinancialAuthDialog.vue

@@ -3,32 +3,40 @@
     <q-card class="q-dialog-plugin overflow-hidden" style="min-width: 400px">
       <DefaultDialogHeader title="Acesso ao Financeiro" @close="onCancel" />
 
-      <q-card-section class="q-pt-none q-pb-sm">
-        <div class="text-caption text-grey-6 q-mb-sm">
-          Confirme sua senha para acessar o módulo Financeiro.
-        </div>
-        <DefaultForm @submit.prevent="onConfirm">
-          <q-input
-            ref="passwordInput"
-            v-model="password"
-            type="password"
-            outlined
-            dense
-            autofocus
-            label="Senha"
-            :error="hasError"
-            :error-message="errorMessage"
-            @update:model-value="hasError = false"
-          >
-            <template #prepend>
-              <q-icon name="mdi-lock-outline" />
-            </template>
-          </q-input>
-        </DefaultForm>
-      </q-card-section>
+      <q-scroll-area class="form-dialog-scroll-area">
+        <q-card-section class="q-pt-none q-pb-sm">
+          <div class="text-caption text-grey-6 q-mb-sm">
+            Confirme sua senha para acessar o módulo Financeiro.
+          </div>
+          <DefaultForm @submit.prevent="onConfirm">
+            <q-input
+              ref="passwordInput"
+              v-model="password"
+              type="password"
+              outlined
+              dense
+              autofocus
+              label="Senha"
+              :error="hasError"
+              :error-message="errorMessage"
+              @update:model-value="hasError = false"
+            >
+              <template #prepend>
+                <q-icon name="mdi-lock-outline" />
+              </template>
+            </q-input>
+          </DefaultForm>
+        </q-card-section>
+      </q-scroll-area>
 
       <q-card-actions align="right">
-        <q-btn outline color="primary" label="Cancelar" :disable="loading" @click="onCancel" />
+        <q-btn
+          outline
+          color="primary"
+          label="Cancelar"
+          :disable="loading"
+          @click="onCancel"
+        />
         <q-btn
           color="primary"
           label="Confirmar"
@@ -49,7 +57,8 @@ import { confirmPassword } from "src/api/auth";
 
 defineEmits([...useDialogPluginComponent.emits]);
 
-const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
 
 const password = ref("");
 const loading = ref(false);

+ 55 - 24
src/components/financial/SettleAccountReceivableDialog.vue

@@ -1,22 +1,41 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <q-card class="q-dialog-plugin overflow-hidden" style="width: 420px; max-width: 90vw">
+    <q-card
+      class="q-dialog-plugin overflow-hidden"
+      style="width: 420px; max-width: 90vw"
+    >
       <DefaultDialogHeader title="Marcar como pago" @close="onDialogCancel" />
       <DefaultForm ref="formRef" @submit.prevent="onSubmit">
-        <q-card-section class="q-pt-none">
-          <div class="text-body2 q-mb-md">{{ receivable.history }}</div>
-          <DefaultInputDatePicker
-            v-model="paymentDate"
-            v-model:untreated-date="paymentDateIso"
-            label="Data do pagamento"
-            lazy-rules="ondemand"
-            :rules="[(value) => !!value || 'Informe a data do pagamento']"
-          />
-        </q-card-section>
+        <q-scroll-area class="form-dialog-scroll-area">
+          <q-card-section class="q-pt-none">
+            <div class="text-body2 q-mb-md">{{ receivable.history }}</div>
+            <DefaultInputDatePicker
+              v-model="paymentDate"
+              v-model:untreated-date="paymentDateIso"
+              v-model:error="validationErrors.payment_date"
+              :error-message="validationErrors.payment_date"
+              label="Data do pagamento"
+              lazy-rules="ondemand"
+              :rules="[(value) => !!value || 'Informe a data do pagamento']"
+            />
+          </q-card-section>
+        </q-scroll-area>
         <q-separator />
         <q-card-actions align="right" class="q-pa-md q-gutter-sm">
-          <q-btn outline color="negative" label="Cancelar" no-caps @click="onDialogCancel" />
-          <q-btn color="primary" label="Confirmar" type="submit" no-caps :loading="loading" />
+          <q-btn
+            outline
+            color="negative"
+            label="Cancelar"
+            no-caps
+            @click="onDialogCancel"
+          />
+          <q-btn
+            color="primary"
+            label="Confirmar"
+            type="submit"
+            no-caps
+            :loading="loading"
+          />
         </q-card-actions>
       </DefaultForm>
     </q-card>
@@ -32,21 +51,33 @@ import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue
 import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
 
 defineEmits([...useDialogPluginComponent.emits]);
-const { receivable } = defineProps({ receivable: { type: Object, required: true } });
-const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+const { receivable } = defineProps({
+  receivable: { type: Object, required: true },
+});
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
 const today = new Date();
 const paymentDate = ref(today.toLocaleDateString("pt-BR"));
-const paymentDateIso = ref([
-  today.getFullYear(),
-  String(today.getMonth() + 1).padStart(2, "0"),
-  String(today.getDate()).padStart(2, "0"),
-].join("-"));
+const paymentDateIso = ref(
+  [
+    today.getFullYear(),
+    String(today.getMonth() + 1).padStart(2, "0"),
+    String(today.getDate()).padStart(2, "0"),
+  ].join("-"),
+);
 const formRef = ref(null);
-const { loading, execute: submitForm } = useSubmitHandler({
+const {
+  loading,
+  validationErrors,
+  execute: submitForm,
+} = useSubmitHandler({
   formRef,
   onSuccess: (response) => onDialogOK(response),
 });
-const onSubmit = () => submitForm(() =>
-  settleFranchiseeReceivable(receivable.id, { payment_date: paymentDateIso.value }),
-);
+const onSubmit = () =>
+  submitForm(() =>
+    settleFranchiseeReceivable(receivable.id, {
+      payment_date: paymentDateIso.value,
+    }),
+  );
 </script>

+ 52 - 30
src/components/financial/SettleCompanyPayableDialog.vue

@@ -1,41 +1,62 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <q-card class="q-dialog-plugin overflow-hidden" style="width: 460px; max-width: 90vw">
+    <q-card
+      class="q-dialog-plugin overflow-hidden"
+      style="width: 460px; max-width: 90vw"
+    >
       <DefaultDialogHeader title="Dar baixa" @close="onDialogCancel" />
 
       <DefaultForm ref="formRef" @submit.prevent="onSubmit">
-        <q-card-section class="row q-col-gutter-md q-pt-none">
-          <div class="col-12 text-body2 text-grey-8">{{ payable.history }}</div>
-
-          <DefaultInputDatePicker
-            v-model="form.payment_date"
-            v-model:untreated-date="form.payment_date_iso"
-            label="Data do pagamento"
-            class="col-12"
-            lazy-rules="ondemand"
-            :rules="[(value) => !!value || 'Informe a data do pagamento']"
-          />
-
-          <DefaultCurrencyInput
-            v-model="form.discount"
-            label="Desconto"
-            class="col-12 col-sm-6"
-          />
-          <DefaultCurrencyInput
-            v-model="form.fine"
-            label="Multa/Juros"
-            class="col-12 col-sm-6"
-          />
-
-          <div class="col-12 text-caption text-grey-7">
-            Valor da conta: {{ formatCurrency(payable.value) }} ·
-            <span class="text-weight-medium">Pago: {{ formatCurrency(netValue) }}</span>
-          </div>
-        </q-card-section>
+        <q-scroll-area class="form-dialog-scroll-area">
+          <q-card-section class="row q-col-gutter-md q-pt-none">
+            <div class="col-12 text-body2 text-grey-8">
+              {{ payable.history }}
+            </div>
+
+            <DefaultInputDatePicker
+              v-model="form.payment_date"
+              v-model:untreated-date="form.payment_date_iso"
+              v-model:error="validationErrors.payment_date"
+              :error-message="validationErrors.payment_date"
+              label="Data do pagamento"
+              class="col-12"
+              lazy-rules="ondemand"
+              :rules="[(value) => !!value || 'Informe a data do pagamento']"
+            />
+
+            <DefaultCurrencyInput
+              v-model="form.discount"
+              v-model:error="validationErrors.discount"
+              :error-message="validationErrors.discount"
+              label="Desconto"
+              class="col-12 col-sm-6"
+            />
+            <DefaultCurrencyInput
+              v-model="form.fine"
+              v-model:error="validationErrors.fine"
+              :error-message="validationErrors.fine"
+              label="Multa/Juros"
+              class="col-12 col-sm-6"
+            />
+
+            <div class="col-12 text-caption text-grey-7">
+              Valor da conta: {{ formatCurrency(payable.value) }} ·
+              <span class="text-weight-medium"
+                >Pago: {{ formatCurrency(netValue) }}</span
+              >
+            </div>
+          </q-card-section>
+        </q-scroll-area>
 
         <q-separator />
         <q-card-actions align="right" class="q-pa-md q-gutter-sm">
-          <q-btn outline label="Cancelar" color="negative" no-caps @click="onDialogCancel" />
+          <q-btn
+            outline
+            label="Cancelar"
+            color="negative"
+            no-caps
+            @click="onDialogCancel"
+          />
           <q-btn
             color="primary"
             label="Confirmar baixa"
@@ -102,6 +123,7 @@ const formatCurrency = (value) =>
 
 const {
   loading,
+  validationErrors,
   execute: submitForm,
 } = useSubmitHandler({
   formRef,

+ 6 - 23
src/composables/useSubmitHandler.js

@@ -1,7 +1,9 @@
 import { ref, nextTick } from "vue";
+import { useScroll } from "src/composables/useScroll";
 
 export function useSubmitHandler(options = {}) {
   const { onSuccess, onError, formRef, scrollFn, containerRef } = options;
+  const { scrollToFirstError } = useScroll();
 
   const loading = ref(false);
   const validationErrors = ref({});
@@ -12,27 +14,8 @@ export function useSubmitHandler(options = {}) {
     return Array.isArray(refs) ? refs : [refs];
   };
 
-  const scrollToFirstError = async () => {
-    if (!formRef?.value || !scrollFn) return;
-    await nextTick();
-
-    const refsToSearch = getFormRefs();
-
-    for (const ref of refsToSearch) {
-      if (!ref) continue;
-      const components = ref.getValidationComponents();
-      if (!components) continue;
-
-      const firstErrorComponent = components.find((c) => c.hasError);
-
-      if (firstErrorComponent) {
-        const container = containerRef?.value || containerRef;
-        firstErrorComponent.focus();
-        scrollFn(firstErrorComponent, container);
-        return;
-      }
-    }
-  };
+  const showFirstError = () =>
+    scrollToFirstError(formRef, containerRef, { scrollFn });
 
   const execute = async (apiCallThunk) => {
     loading.value = true;
@@ -54,7 +37,7 @@ export function useSubmitHandler(options = {}) {
 
     if (!allValid) {
       loading.value = false;
-      await scrollToFirstError();
+      await showFirstError();
       throw new Error("Frontend validation failed.");
     }
 
@@ -79,7 +62,7 @@ export function useSubmitHandler(options = {}) {
         const message = errors[key][0];
         validationErrors.value[key] = message;
       }
-      await scrollToFirstError();
+      await showFirstError();
     }
 
     await nextTick();

+ 40 - 6
src/css/app.scss

@@ -1,6 +1,6 @@
 @use "sass:map";
 @use "src/css/quasar.variables.scss";
-@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Rethink+Sans:ital,wght@0,400..800;1,400..800&display=swap');
+@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Rethink+Sans:ital,wght@0,400..800;1,400..800&display=swap");
 
 .required-field .q-field__label::after {
   color: var(--q-negative);
@@ -78,11 +78,49 @@ body.body--light {
   }
 }
 
-
 .q-card__actions .q-btn {
   padding: 10px 16px;
 }
 
+// Keep dialog headers and actions visible while only the form content scrolls.
+.q-dialog .q-card:has(.q-form) {
+  display: flex;
+  flex-direction: column;
+  max-height: calc(100vh - 48px);
+  overflow: hidden;
+
+  > .q-bar,
+  > .q-card__actions {
+    flex: 0 0 auto;
+  }
+
+  > .q-form {
+    display: flex;
+    flex: 1 1 auto;
+    flex-direction: column;
+    min-height: 0;
+    overflow: hidden;
+
+    > .form-dialog-scroll-area {
+      flex: 1 1 auto;
+      height: min(70vh, 640px);
+      min-height: 0;
+    }
+
+    > .q-card__actions,
+    > .q-separator {
+      flex: 0 0 auto;
+    }
+  }
+
+  // Supports dialogs whose form is wrapped by the scroll area.
+  > .form-dialog-scroll-area {
+    flex: 1 1 auto;
+    height: min(70vh, 640px);
+    min-height: 0;
+  }
+}
+
 input[type="number"]::-webkit-inner-spin-button,
 input[type="number"]::-webkit-outer-spin-button {
   -webkit-appearance: none;
@@ -109,12 +147,8 @@ input[type="number"]::-webkit-outer-spin-button {
   }
 }
 
-
-
-
 .q-field--standout.q-field--rounded .q-field__control {
   border-radius: 8px;
-  
 }
 
 .q-btn--rectangle {

+ 27 - 25
src/pages/cadastros/components/AddEditGroupDialog.vue

@@ -8,32 +8,34 @@
         />
 
         <DefaultForm ref="formRef" @submit="onOKClick">
-          <q-card-section class="q-pt-sm column q-gutter-y-sm">
-            <DefaultInput
-              v-model="form.name"
-              label="Nome do Grupo"
-              outlined
-              :rules="[inputRules.required]"
-            />
+          <q-scroll-area class="form-dialog-scroll-area">
+            <q-card-section class="q-pt-sm column q-gutter-y-sm">
+              <DefaultInput
+                v-model="form.name"
+                label="Nome do Grupo"
+                outlined
+                :rules="[inputRules.required]"
+              />
 
-            <q-select
-              v-model="form.unit_ids"
-              :options="unitOptions"
-              label="Unidades"
-              label-color="secondary"
-              outlined
-              multiple
-              use-chips
-              emit-value
-              map-options
-              hide-dropdown-icon
-              :loading="loadingUnits"
-            >
-              <template #append>
-                <q-icon name="mdi-chevron-down" color="secondary" />
-              </template>
-            </q-select>
-          </q-card-section>
+              <q-select
+                v-model="form.unit_ids"
+                :options="unitOptions"
+                label="Unidades"
+                label-color="secondary"
+                outlined
+                multiple
+                use-chips
+                emit-value
+                map-options
+                hide-dropdown-icon
+                :loading="loadingUnits"
+              >
+                <template #append>
+                  <q-icon name="mdi-chevron-down" color="secondary" />
+                </template>
+              </q-select>
+            </q-card-section>
+          </q-scroll-area>
 
           <q-card-actions align="right" class="q-px-md q-pb-md">
             <q-btn

+ 1 - 0
src/pages/financial/AsaasIntegrationPage.vue

@@ -26,6 +26,7 @@
           <DefaultPasswordInput
             v-model="apiKey"
             v-model:error="validationErrors.asaas_api_key"
+            :error-message="validationErrors.asaas_api_key"
             :label="
               account.asaas_configured
                 ? 'Nova chave da API (deixe em branco para manter a atual)'

+ 233 - 213
src/pages/kanban/components/AddEditKanbanDialog.vue

@@ -1,7 +1,14 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
     <div style="width: 100%; max-width: 1100px">
-      <q-card style="height: 560px; display: flex; flex-direction: column; overflow: hidden">
+      <q-card
+        style="
+          height: 560px;
+          display: flex;
+          flex-direction: column;
+          overflow: hidden;
+        "
+      >
         <DefaultDialogHeader
           :title="card ? 'Editar Atividade' : 'Adicionar Atividade'"
           @close="onDialogCancel"
@@ -9,185 +16,195 @@
 
         <DefaultForm
           ref="formRef"
-          style="flex: 1; display: flex; flex-direction: column; overflow: hidden"
+          style="
+            flex: 1;
+            display: flex;
+            flex-direction: column;
+            overflow: hidden;
+          "
           @submit="onOKClick"
         >
-          <q-card-section class="q-pt-sm" style="flex: 1; overflow-y: auto; min-height: 0">
-            <CustomTabComponent
-              v-if="card?.id"
-              v-model:active-tab="currentTab"
-              :tabs="tabs"
-              class="q-mb-md"
-            />
+          <q-scroll-area class="form-dialog-scroll-area">
+            <q-card-section class="q-pt-sm">
+              <CustomTabComponent
+                v-if="card?.id"
+                v-model:active-tab="currentTab"
+                :tabs="tabs"
+                class="q-mb-md"
+              />
+
+              <!-- Tab: Atividade -->
+              <div v-show="currentTab === 'atividade'">
+                <div class="row q-col-gutter-sm">
+                  <!-- 1. Título -->
+                  <DefaultInput
+                    v-model="form.title"
+                    label="Título da Tarefa"
+                    class="col-12"
+                    :rules="[(val) => !!val || 'Campo obrigatório']"
+                  />
 
-            <!-- Tab: Atividade -->
-            <div v-show="currentTab === 'atividade'">
-              <div class="row q-col-gutter-sm">
-                <!-- 1. Título -->
-                <DefaultInput
-                  v-model="form.title"
-                  label="Título da Tarefa"
-                  class="col-12"
-                  :rules="[val => !!val || 'Campo obrigatório']"
-                />
-
-                <!-- 2. Destino (primeiro campo de configuração) -->
-                <DefaultSelect
-                  v-model="form.scope"
-                  label="Destino"
-                  :options="scopeOptions"
-                  emit-value
-                  map-options
-                  class="col-6"
-                  :rules="[val => !!val || 'Campo obrigatório']"
-                />
-
-                <!-- Prazo ao lado do Destino -->
-                <DefaultInputDatePicker
-                  v-model="form.due_date_display"
-                  v-model:untreated-date="form.due_date"
-                  label="Prazo de Entrega"
-                  class="col-6"
-                />
-
-                <!-- Unidade específica — só aparece quando scope = specific -->
-                <UnitSelect
-                  v-if="form.scope === 'specific'"
-                  v-model="selectedUnit"
-                  class="col-6"
-                />
-
-                <!-- 3. Prioridade + Fase -->
-                <DefaultSelect
-                  v-model="form.priority"
-                  label="Prioridade"
-                  :options="priorityOptions"
-                  emit-value
-                  map-options
-                  class="col-6"
-                  :rules="[val => !!val || 'Campo obrigatório']"
-                />
-
-                <DefaultSelect
-                  v-model="form.phase"
-                  label="Fase"
-                  :options="phaseOptions"
-                  emit-value
-                  map-options
-                  class="col-6"
-                  :rules="[val => !!val || 'Campo obrigatório']"
-                />
-
-                <!-- 4. Setor -->
-                <DefaultInput
-                  v-model="form.sector"
-                  label="Setor"
-                  class="col-6"
-                />
-
-                <!-- 5. Descrição -->
-                <DefaultInput
-                  v-model="form.description"
-                  label="Descrição"
-                  type="textarea"
-                  class="col-12"
-                />
-              </div>
-            </div>
-
-            <!-- Tab: Comentários -->
-            <div v-show="currentTab === 'comentarios'">
-              <div class="flex justify-end q-mb-sm">
-                <q-btn
-                  v-if="canAdd"
-                  color="primary"
-                  icon="mdi-plus"
-                  unelevated
-                  style="width: 40px; height: 40px"
-                  @click="onAddComment"
-                />
-              </div>
-              <div
-                style="
-                  height: 100%;
-                  max-height: 380px;
-                  overflow-y: auto;
-                  display: flex;
-                  flex-direction: column;
-                  gap: 8px;
-                "
-              >
-                <template v-if="replies.length">
-                  <KanbanCommentCard
-                    v-for="reply in replies"
-                    :key="reply.id"
-                    :reply="reply.reply"
-                    :created-at="reply.created_at"
-                    :user-name="reply.user_name"
-                    :can-edit="canEdit"
-                    :can-delete="canDelete"
-                    @edit="onEditComment(reply)"
-                    @delete="onDeleteComment(reply)"
+                  <!-- 2. Destino (primeiro campo de configuração) -->
+                  <DefaultSelect
+                    v-model="form.scope"
+                    label="Destino"
+                    :options="scopeOptions"
+                    emit-value
+                    map-options
+                    class="col-6"
+                    :rules="[(val) => !!val || 'Campo obrigatório']"
+                  />
+
+                  <!-- Prazo ao lado do Destino -->
+                  <DefaultInputDatePicker
+                    v-model="form.due_date_display"
+                    v-model:untreated-date="form.due_date"
+                    label="Prazo de Entrega"
+                    class="col-6"
+                  />
+
+                  <!-- Unidade específica — só aparece quando scope = specific -->
+                  <UnitSelect
+                    v-if="form.scope === 'specific'"
+                    v-model="selectedUnit"
+                    class="col-6"
+                  />
+
+                  <!-- 3. Prioridade + Fase -->
+                  <DefaultSelect
+                    v-model="form.priority"
+                    label="Prioridade"
+                    :options="priorityOptions"
+                    emit-value
+                    map-options
+                    class="col-6"
+                    :rules="[(val) => !!val || 'Campo obrigatório']"
+                  />
+
+                  <DefaultSelect
+                    v-model="form.phase"
+                    label="Fase"
+                    :options="phaseOptions"
+                    emit-value
+                    map-options
+                    class="col-6"
+                    :rules="[(val) => !!val || 'Campo obrigatório']"
+                  />
+
+                  <!-- 4. Setor -->
+                  <DefaultInput
+                    v-model="form.sector"
+                    label="Setor"
+                    class="col-6"
+                  />
+
+                  <!-- 5. Descrição -->
+                  <DefaultInput
+                    v-model="form.description"
+                    label="Descrição"
+                    type="textarea"
+                    class="col-12"
                   />
-                </template>
-                <div v-else class="flex flex-center full-height text-grey-5 text-body2">
-                  Nenhum comentário registrado.
                 </div>
               </div>
-            </div>
-
-            <!-- Tab: Mídias -->
-            <div v-show="currentTab === 'midias'">
-              <div class="flex justify-end q-mb-sm">
-                <q-btn
-                  v-if="canAdd"
-                  color="primary"
-                  icon="mdi-upload-outline"
-                  unelevated
-                  style="width: 40px; height: 40px"
-                  :loading="uploadingMedia"
-                  @click="triggerFileInput"
-                />
-                <input
-                  ref="fileInputRef"
-                  type="file"
-                  style="display: none"
-                  @change="onFileSelected"
-                />
-              </div>
-              <div
-                style="
-                  height: 100%;
-                  max-height: 380px;
-                  overflow-y: auto;
-                  display: flex;
-                  flex-direction: column;
-                  gap: 8px;
-                "
-              >
-                <template v-if="medias.length">
-                  <KanbanMediaCard
-                    v-for="media in medias"
-                    :key="media.id"
-                    :file-name="media.file_name"
-                    :file-url="media.file_url"
-                    :mime-type="media.mime_type"
-                    :created-at="media.created_at"
-                    :user-name="media.user_name"
-                    :can-delete="canDelete"
-                    @delete="onDeleteMedia(media)"
+
+              <!-- Tab: Comentários -->
+              <div v-show="currentTab === 'comentarios'">
+                <div class="flex justify-end q-mb-sm">
+                  <q-btn
+                    v-if="canAdd"
+                    color="primary"
+                    icon="mdi-plus"
+                    unelevated
+                    style="width: 40px; height: 40px"
+                    @click="onAddComment"
                   />
-                </template>
-                <div v-else class="flex flex-center full-height text-grey-5 text-body2">
-                  Nenhuma mídia anexada.
+                </div>
+                <div style="display: flex; flex-direction: column; gap: 8px">
+                  <template v-if="replies.length">
+                    <KanbanCommentCard
+                      v-for="reply in replies"
+                      :key="reply.id"
+                      :reply="reply.reply"
+                      :created-at="reply.created_at"
+                      :user-name="reply.user_name"
+                      :can-edit="canEdit"
+                      :can-delete="canDelete"
+                      @edit="onEditComment(reply)"
+                      @delete="onDeleteComment(reply)"
+                    />
+                  </template>
+                  <div
+                    v-else
+                    class="flex flex-center full-height text-grey-5 text-body2"
+                  >
+                    Nenhum comentário registrado.
+                  </div>
                 </div>
               </div>
-            </div>
-          </q-card-section>
 
-          <q-card-actions align="right" class="q-px-md q-pb-md" style="flex-shrink: 0">
-            <q-btn outline color="primary" label="Cancelar" @click="onDialogCancel" />
-            <q-btn v-if="card?.id ? canEdit : canAdd" color="primary" label="Salvar" type="submit" :loading="loading" />
+              <!-- Tab: Mídias -->
+              <div v-show="currentTab === 'midias'">
+                <div class="flex justify-end q-mb-sm">
+                  <q-btn
+                    v-if="canAdd"
+                    color="primary"
+                    icon="mdi-upload-outline"
+                    unelevated
+                    style="width: 40px; height: 40px"
+                    :loading="uploadingMedia"
+                    @click="triggerFileInput"
+                  />
+                  <input
+                    ref="fileInputRef"
+                    type="file"
+                    style="display: none"
+                    @change="onFileSelected"
+                  />
+                </div>
+                <div style="display: flex; flex-direction: column; gap: 8px">
+                  <template v-if="medias.length">
+                    <KanbanMediaCard
+                      v-for="media in medias"
+                      :key="media.id"
+                      :file-name="media.file_name"
+                      :file-url="media.file_url"
+                      :mime-type="media.mime_type"
+                      :created-at="media.created_at"
+                      :user-name="media.user_name"
+                      :can-delete="canDelete"
+                      @delete="onDeleteMedia(media)"
+                    />
+                  </template>
+                  <div
+                    v-else
+                    class="flex flex-center full-height text-grey-5 text-body2"
+                  >
+                    Nenhuma mídia anexada.
+                  </div>
+                </div>
+              </div>
+            </q-card-section>
+          </q-scroll-area>
+
+          <q-card-actions
+            align="right"
+            class="q-px-md q-pb-md"
+            style="flex-shrink: 0"
+          >
+            <q-btn
+              outline
+              color="primary"
+              label="Cancelar"
+              @click="onDialogCancel"
+            />
+            <q-btn
+              v-if="card?.id ? canEdit : canAdd"
+              color="primary"
+              label="Salvar"
+              type="submit"
+              :loading="loading"
+            />
           </q-card-actions>
         </DefaultForm>
       </q-card>
@@ -225,7 +242,7 @@ import { permissionStore } from "src/stores/permission";
 defineEmits([...useDialogPluginComponent.emits]);
 
 const { card, initialPhase } = defineProps({
-  card:         { type: Object, default: null },
+  card: { type: Object, default: null },
   initialPhase: { type: String, default: "a_fazer" },
 });
 
@@ -238,38 +255,40 @@ const canAdd = permissions.getAccess("franchisor_activities", "add");
 const canEdit = permissions.getAccess("franchisor_activities", "edit");
 const canDelete = permissions.getAccess("franchisor_activities", "delete");
 
-const formRef      = ref(null);
-const loading      = ref(false);
-const currentTab   = ref("atividade");
-const replies      = ref([]);
-const medias       = ref([]);
+const formRef = ref(null);
+const loading = ref(false);
+const currentTab = ref("atividade");
+const replies = ref([]);
+const medias = ref([]);
 const uploadingMedia = ref(false);
 const fileInputRef = ref(null);
-const selectedUnit = ref(card?.target_unit_id ? { value: card.target_unit_id } : null);
+const selectedUnit = ref(
+  card?.target_unit_id ? { value: card.target_unit_id } : null,
+);
 
 const tabs = computed(() => [
-  { name: "atividade",   label: "Atividade"   },
+  { name: "atividade", label: "Atividade" },
   { name: "comentarios", label: "Comentários" },
-  { name: "midias",      label: "Mídias"      },
+  { name: "midias", label: "Mídias" },
 ]);
 
 const priorityOptions = [
-  { label: "Alta",   value: "alta"   },
+  { label: "Alta", value: "alta" },
   { label: "Normal", value: "normal" },
-  { label: "Baixa",  value: "baixa"  },
+  { label: "Baixa", value: "baixa" },
 ];
 
 const phaseOptions = [
-  { label: "A Fazer",            value: "a_fazer"            },
-  { label: "Em Progresso",       value: "em_progresso"       },
-  { label: "Em Revisão",         value: "em_revisao"         },
-  { label: "Concluído",          value: "concluido"          },
+  { label: "A Fazer", value: "a_fazer" },
+  { label: "Em Progresso", value: "em_progresso" },
+  { label: "Em Revisão", value: "em_revisao" },
+  { label: "Concluído", value: "concluido" },
   { label: "Demandas Especiais", value: "demandas_especiais" },
 ];
 
 const scopeOptions = [
-  { label: "Interno",            value: "internal" },
-  { label: "Todas as Unidades",  value: "all"      },
+  { label: "Interno", value: "internal" },
+  { label: "Todas as Unidades", value: "all" },
   { label: "Unidade Específica", value: "specific" },
 ];
 
@@ -280,28 +299,29 @@ const formatDisplayDate = (rawDate) => {
 };
 
 const form = ref({
-  title:            card?.title         ?? null,
-  priority:         card?.priority      ?? "normal",
-  phase:            card?.phase         ?? initialPhase,
-  scope:            card?.scope         ?? "internal",
-  sector:           card?.sector        ?? null,
-  description:      card?.description   ?? null,
-  due_date:         card?.due_date      ?? null,
+  title: card?.title ?? null,
+  priority: card?.priority ?? "normal",
+  phase: card?.phase ?? initialPhase,
+  scope: card?.scope ?? "internal",
+  sector: card?.sector ?? null,
+  description: card?.description ?? null,
+  due_date: card?.due_date ?? null,
   due_date_display: formatDisplayDate(card?.due_date),
 });
 
 const buildPayload = () => ({
-  title:               form.value.title,
-  priority:            form.value.priority,
-  phase:               form.value.phase,
-  scope:               form.value.scope,
-  sector:              form.value.sector       || null,
-  description:         form.value.description  || null,
-  due_date:            form.value.due_date      || null,
-  responsible_user_id: null,   // Matriz never assigns a responsible
-  target_unit_id: form.value.scope === "specific"
-    ? (selectedUnit.value?.value ?? null)
-    : null,
+  title: form.value.title,
+  priority: form.value.priority,
+  phase: form.value.phase,
+  scope: form.value.scope,
+  sector: form.value.sector || null,
+  description: form.value.description || null,
+  due_date: form.value.due_date || null,
+  responsible_user_id: null, // Matriz never assigns a responsible
+  target_unit_id:
+    form.value.scope === "specific"
+      ? (selectedUnit.value?.value ?? null)
+      : null,
 });
 
 const loadReplies = async () => {
@@ -333,10 +353,10 @@ const onFileSelected = async (event) => {
 
 const onDeleteMedia = (media) => {
   $q.dialog({
-    title:   "Excluir Mídia",
+    title: "Excluir Mídia",
     message: `Deseja excluir "${media.file_name}"?`,
-    cancel:  { outline: true, color: "primary",  label: "Cancelar" },
-    ok:      {               color: "negative",  label: "Excluir"  },
+    cancel: { outline: true, color: "primary", label: "Cancelar" },
+    ok: { color: "negative", label: "Excluir" },
   }).onOk(async () => {
     await deleteKanbanMedia(card.id, media.id);
     loadMedias();
@@ -347,7 +367,7 @@ const onAddComment = () => {
   $q.dialog({
     title: "Adicionar Comentário",
     prompt: { model: "", type: "textarea", label: "Comentário" },
-    ok:     { label: "Salvar",   color: "primary" },
+    ok: { label: "Salvar", color: "primary" },
     cancel: { label: "Cancelar", color: "primary", outline: true },
   }).onOk(async (text) => {
     if (!text?.trim()) return;
@@ -360,7 +380,7 @@ const onEditComment = (reply) => {
   $q.dialog({
     title: "Editar Comentário",
     prompt: { model: reply.reply, type: "textarea", label: "Comentário" },
-    ok:     { label: "Salvar",   color: "primary" },
+    ok: { label: "Salvar", color: "primary" },
     cancel: { label: "Cancelar", color: "primary", outline: true },
   }).onOk(async (text) => {
     if (!text?.trim()) return;
@@ -371,10 +391,10 @@ const onEditComment = (reply) => {
 
 const onDeleteComment = (reply) => {
   $q.dialog({
-    title:   "Excluir Comentário",
+    title: "Excluir Comentário",
     message: "Tem certeza que deseja excluir este comentário?",
-    cancel:  { outline: true, color: "primary",  label: "Cancelar" },
-    ok:      {               color: "negative",  label: "Excluir"  },
+    cancel: { outline: true, color: "primary", label: "Cancelar" },
+    ok: { color: "negative", label: "Excluir" },
   }).onOk(async () => {
     await deleteKanbanReply(card.id, reply.id);
     loadReplies();

+ 2 - 0
src/pages/login/LoginPage.vue

@@ -42,6 +42,7 @@
         <DefaultInput
           v-model="form.email"
           v-model:error="validationErrors.email"
+          :error-message="validationErrors.email"
           type="email"
           lazy-rules
           icon="mdi-account"
@@ -52,6 +53,7 @@
         <DefaultPasswordInput
           v-model="form.password"
           v-model:error="validationErrors.password"
+          :error-message="validationErrors.password"
           :rules="[inputRules.required, inputRules.min(6)]"
           :label="$t('common.terms.password')"
         />

+ 407 - 394
src/pages/packages/components/AddEditPackageDialog.vue

@@ -8,434 +8,453 @@
         />
 
         <DefaultForm ref="formRef" @submit="onOKClick">
-          <q-card-section class="q-pt-sm">
-            <CustomTabComponent
-              v-model:active-tab="currentTab"
-              :tabs="tabs"
-              class="q-mb-md"
-            />
+          <q-scroll-area class="form-dialog-scroll-area">
+            <q-card-section class="q-pt-sm">
+              <CustomTabComponent
+                v-model:active-tab="currentTab"
+                :tabs="tabs"
+                class="q-mb-md"
+              />
+
+              <!-- Tab: Informações -->
+              <div v-show="currentTab === 'informacoes'">
+                <div class="row q-col-gutter-sm">
+                  <DefaultInput
+                    v-model="form.name"
+                    label="Nome do Pacote"
+                    class="col-12"
+                  />
 
-            <!-- Tab: Informações -->
-            <div v-show="currentTab === 'informacoes'">
-              <div class="row q-col-gutter-sm">
-                <DefaultInput
-                  v-model="form.name"
-                  label="Nome do Pacote"
-                  class="col-12"
-                />
-
-                <DefaultInput
-                  v-model="form.quantity_classes"
-                  label="Quantidade de Aulas"
-                  class="col-12"
-                  type="number"
-                  min="0"
-                />
-
-                <DefaultInput
-                  v-model="form.class_duration_hours"
-                  label="Duração da Aula (horas)"
-                  class="col-12"
-                  type="number"
-                  min="0.5"
-                  max="24"
-                  step="0.5"
-                />
-
-                <div class="col-12 package-duration-hint">
-                  A duração padrão das aulas é de 2 horas e pode ser alterada se
-                  necessário. É possível configurar no máximo dois dias.
-                </div>
+                  <DefaultInput
+                    v-model="form.quantity_classes"
+                    label="Quantidade de Aulas"
+                    class="col-12"
+                    type="number"
+                    min="0"
+                  />
 
-                <DefaultSelect
-                  v-model="form.weekday"
-                  label="1º Dia da Semana"
-                  class="col-4"
-                  :options="weekdays"
-                  option-value="value"
-                  option-label="label"
-                  emit-value
-                  map-options
-                  clearable
-                />
-
-                <DefaultInput
-                  v-model="form.start_time"
-                  label="Horário Inicial"
-                  class="col-4"
-                  mask="##:##"
-                />
-
-                <DefaultInput
-                  :model-value="firstEndTime"
-                  label="Horário Final"
-                  class="col-4"
-                  disable
-                />
-
-                <DefaultSelect
-                  v-model="form.second_weekday"
-                  label="2º Dia da Semana"
-                  class="col-4"
-                  :options="secondWeekdayOptions"
-                  option-value="value"
-                  option-label="label"
-                  emit-value
-                  map-options
-                  clearable
-                />
-
-                <DefaultInput
-                  v-model="form.second_start_time"
-                  label="Horário Inicial do 2º dia"
-                  class="col-4"
-                  mask="##:##"
-                />
-
-                <DefaultInput
-                  :model-value="secondEndTime"
-                  label="Horário Final do 2º dia"
-                  class="col-4"
-                  disable
-                />
-
-                <DefaultCurrencyInput
-                  v-model="form.contract_register_value"
-                  label="R$ Matrícula"
-                  class="col-4"
-                />
-
-                <DefaultCurrencyInput
-                  v-model="form.contract_value"
-                  label="R$ Total do Contrato"
-                  class="col-4"
-                />
-
-                <DefaultInput
-                  v-model="form.contrat_discount_value"
-                  label="Desconto em %"
-                  class="col-4"
-                  type="number"
-                  min="0"
-                  max="100"
-                />
-
-                <div
-                  v-for="(material, index) in form.materials"
-                  :key="index"
-                  class="col-12"
-                >
-                  <div class="row q-col-gutter-sm items-center">
-                    <DefaultSelect
-                      v-model="material.product_id"
-                      label="Material"
-                      class="col"
-                      :options="productOptions"
-                      emit-value
-                      map-options
-                      @update:model-value="onProductSelected(material)"
-                    />
+                  <DefaultInput
+                    v-model="form.class_duration_hours"
+                    label="Duração da Aula (horas)"
+                    class="col-12"
+                    type="number"
+                    min="0.5"
+                    max="24"
+                    step="0.5"
+                  />
 
-                    <DefaultInput
-                      v-model="material.quantity"
-                      label="Qtd"
-                      class="col-2"
-                      type="number"
-                      min="1"
-                    />
+                  <div class="col-12 package-duration-hint">
+                    A duração padrão das aulas é de 2 horas e pode ser alterada
+                    se necessário. É possível configurar no máximo dois dias.
+                  </div>
 
-                    <DefaultCurrencyInput
-                      v-model="material.price"
-                      label="R$ Unitário"
-                      class="col-3"
-                    />
+                  <DefaultSelect
+                    v-model="form.weekday"
+                    label="1º Dia da Semana"
+                    class="col-4"
+                    :options="weekdays"
+                    option-value="value"
+                    option-label="label"
+                    emit-value
+                    map-options
+                    clearable
+                  />
 
-                    <div class="col-auto">
-                      <q-btn
-                        v-if="index === form.materials.length - 1"
-                        color="primary"
-                        icon="mdi-plus"
-                        unelevated
-                        style="border-radius: 8px; height: 40px; width: 40px"
-                        @click="addMaterial"
+                  <DefaultInput
+                    v-model="form.start_time"
+                    label="Horário Inicial"
+                    class="col-4"
+                    mask="##:##"
+                  />
+
+                  <DefaultInput
+                    :model-value="firstEndTime"
+                    label="Horário Final"
+                    class="col-4"
+                    disable
+                  />
+
+                  <DefaultSelect
+                    v-model="form.second_weekday"
+                    label="2º Dia da Semana"
+                    class="col-4"
+                    :options="secondWeekdayOptions"
+                    option-value="value"
+                    option-label="label"
+                    emit-value
+                    map-options
+                    clearable
+                  />
+
+                  <DefaultInput
+                    v-model="form.second_start_time"
+                    label="Horário Inicial do 2º dia"
+                    class="col-4"
+                    mask="##:##"
+                  />
+
+                  <DefaultInput
+                    :model-value="secondEndTime"
+                    label="Horário Final do 2º dia"
+                    class="col-4"
+                    disable
+                  />
+
+                  <DefaultCurrencyInput
+                    v-model="form.contract_register_value"
+                    label="R$ Matrícula"
+                    class="col-4"
+                  />
+
+                  <DefaultCurrencyInput
+                    v-model="form.contract_value"
+                    label="R$ Total do Contrato"
+                    class="col-4"
+                  />
+
+                  <DefaultInput
+                    v-model="form.contrat_discount_value"
+                    label="Desconto em %"
+                    class="col-4"
+                    type="number"
+                    min="0"
+                    max="100"
+                  />
+
+                  <div
+                    v-for="(material, index) in form.materials"
+                    :key="index"
+                    class="col-12"
+                  >
+                    <div class="row q-col-gutter-sm items-center">
+                      <DefaultSelect
+                        v-model="material.product_id"
+                        label="Material"
+                        class="col"
+                        :options="productOptions"
+                        emit-value
+                        map-options
+                        @update:model-value="onProductSelected(material)"
                       />
-                      <q-btn
-                        v-else
-                        flat
-                        round
-                        dense
-                        icon="mdi-delete-outline"
-                        color="negative"
-                        @click="removeMaterial(index)"
+
+                      <DefaultInput
+                        v-model="material.quantity"
+                        label="Qtd"
+                        class="col-2"
+                        type="number"
+                        min="1"
                       />
+
+                      <DefaultCurrencyInput
+                        v-model="material.price"
+                        label="R$ Unitário"
+                        class="col-3"
+                      />
+
+                      <div class="col-auto">
+                        <q-btn
+                          v-if="index === form.materials.length - 1"
+                          color="primary"
+                          icon="mdi-plus"
+                          unelevated
+                          style="border-radius: 8px; height: 40px; width: 40px"
+                          @click="addMaterial"
+                        />
+                        <q-btn
+                          v-else
+                          flat
+                          round
+                          dense
+                          icon="mdi-delete-outline"
+                          color="negative"
+                          @click="removeMaterial(index)"
+                        />
+                      </div>
                     </div>
                   </div>
                 </div>
               </div>
-            </div>
-
-            <!-- Tab: Unidades -->
-            <div v-show="currentTab === 'unidades'">
-              <div class="column q-gutter-y-sm">
-                <!-- Visível para todas as Unidades -->
-                <q-input
-                  outlined
-                  readonly
-                  dense
-                  label="Visível para todas as Unidades / Grupos"
-                  label-color="secondary"
-                  color="secondary"
-                  bg-color="white"
-                  style="border-radius: 8px"
-                >
-                  <template #append>
-                    <q-btn
-                      flat
-                      round
-                      dense
-                      size="sm"
-                      icon="mdi-check"
-                      color="positive"
-                      @click="setAllVisible(true)"
-                    />
-                    <q-btn
-                      flat
-                      round
-                      dense
-                      size="sm"
-                      icon="mdi-close-circle-outline"
-                      color="negative"
-                      @click="setAllVisible(false)"
-                    />
-                  </template>
-                </q-input>
 
-                <!-- Buscar -->
-                <div class="row items-center q-gutter-x-sm">
+              <!-- Tab: Unidades -->
+              <div v-show="currentTab === 'unidades'">
+                <div class="column q-gutter-y-sm">
+                  <!-- Visível para todas as Unidades -->
                   <q-input
-                    v-model="unitSearch"
                     outlined
+                    readonly
                     dense
-                    label="Buscar Unidade / Grupo"
+                    label="Visível para todas as Unidades / Grupos"
                     label-color="secondary"
                     color="secondary"
                     bg-color="white"
-                    class="col"
                     style="border-radius: 8px"
                   >
                     <template #append>
-                      <q-icon name="mdi-magnify" color="secondary" />
+                      <q-btn
+                        flat
+                        round
+                        dense
+                        size="sm"
+                        icon="mdi-check"
+                        color="positive"
+                        @click="setAllVisible(true)"
+                      />
+                      <q-btn
+                        flat
+                        round
+                        dense
+                        size="sm"
+                        icon="mdi-close-circle-outline"
+                        color="negative"
+                        @click="setAllVisible(false)"
+                      />
                     </template>
                   </q-input>
 
-                  <q-btn
-                    color="primary"
-                    icon="mdi-plus"
-                    unelevated
-                    style="
-                      border-radius: 8px;
-                      height: 40px;
-                      width: 40px;
-                      flex-shrink: 0;
-                    "
-                  />
-                </div>
+                  <!-- Buscar -->
+                  <div class="row items-center q-gutter-x-sm">
+                    <q-input
+                      v-model="unitSearch"
+                      outlined
+                      dense
+                      label="Buscar Unidade / Grupo"
+                      label-color="secondary"
+                      color="secondary"
+                      bg-color="white"
+                      class="col"
+                      style="border-radius: 8px"
+                    >
+                      <template #append>
+                        <q-icon name="mdi-magnify" color="secondary" />
+                      </template>
+                    </q-input>
 
-                <!-- Lista de Unidades -->
-                <div
-                  class="rounded-borders q-pa-sm"
-                  style="
-                    background-color: #f5f5f5;
-                    max-height: 280px;
-                    overflow-y: auto;
-                  "
-                >
-                  <div v-if="loadingUnits" class="flex flex-center q-py-md">
-                    <q-spinner color="secondary" size="sm" />
+                    <q-btn
+                      color="primary"
+                      icon="mdi-plus"
+                      unelevated
+                      style="
+                        border-radius: 8px;
+                        height: 40px;
+                        width: 40px;
+                        flex-shrink: 0;
+                      "
+                    />
                   </div>
 
-                  <template v-else>
-                    <div class="row items-center q-mb-xs q-px-sm">
-                      <q-icon
-                        name="mdi-account-multiple"
-                        color="secondary"
-                        size="sm"
-                        class="q-mr-xs"
-                      />
-                      <span class="text-body2">
-                        Unidades Ativas ({{ filteredUnits.length }})
-                      </span>
-                    </div>
+                  <!-- Lista de Unidades -->
+                  <q-scroll-area
+                    class="rounded-borders"
+                    style="height: 280px; background-color: #f5f5f5"
+                  >
+                    <div class="q-pa-sm">
+                      <div v-if="loadingUnits" class="flex flex-center q-py-md">
+                        <q-spinner color="secondary" size="sm" />
+                      </div>
 
-                    <q-separator class="q-mb-xs" />
+                      <template v-else>
+                        <div class="row items-center q-mb-xs q-px-sm">
+                          <q-icon
+                            name="mdi-account-multiple"
+                            color="secondary"
+                            size="sm"
+                            class="q-mr-xs"
+                          />
+                          <span class="text-body2">
+                            Unidades Ativas ({{ filteredUnits.length }})
+                          </span>
+                        </div>
 
-                    <div class="row q-px-sm q-py-xs">
-                      <span class="col text-caption text-grey-6">Unidade</span>
-                      <span class="col-auto text-caption text-grey-6"
-                        >Status</span
-                      >
-                    </div>
+                        <q-separator class="q-mb-xs" />
 
-                    <template v-for="unit in filteredUnits" :key="unit.id">
-                      <q-separator />
-                      <div class="row items-center q-px-sm q-py-sm">
-                        <span class="col text-body2 row items-center q-gutter-x-xs">
-                          {{ formatUnitName(unit) }}
-                          <q-icon
-                            v-if="lockedUnitIds.has(unit.id)"
-                            name="mdi-lock"
-                            color="amber-7"
-                            size="xs"
+                        <div class="row q-px-sm q-py-xs">
+                          <span class="col text-caption text-grey-6"
+                            >Unidade</span
+                          >
+                          <span class="col-auto text-caption text-grey-6"
+                            >Status</span
                           >
-                            <q-tooltip>Visibilidade definida por grupo</q-tooltip>
-                          </q-icon>
-                        </span>
-                        <div class="col-auto row q-gutter-x-xs">
-                          <q-btn
-                            round
-                            unelevated
-                            :color="unit.visible ? 'positive' : 'grey-4'"
-                            icon="mdi-check"
-                            size="xs"
-                            @click="unit.visible = true"
-                          />
-                          <q-btn
-                            round
-                            :outline="unit.visible"
-                            :unelevated="!unit.visible"
-                            color="negative"
-                            icon="mdi-close"
-                            size="xs"
-                            :disable="lockedUnitIds.has(unit.id)"
-                            @click="!lockedUnitIds.has(unit.id) && (unit.visible = false)"
-                          />
                         </div>
-                      </div>
-                    </template>
-                  </template>
+
+                        <template v-for="unit in filteredUnits" :key="unit.id">
+                          <q-separator />
+                          <div class="row items-center q-px-sm q-py-sm">
+                            <span
+                              class="col text-body2 row items-center q-gutter-x-xs"
+                            >
+                              {{ formatUnitName(unit) }}
+                              <q-icon
+                                v-if="lockedUnitIds.has(unit.id)"
+                                name="mdi-lock"
+                                color="amber-7"
+                                size="xs"
+                              >
+                                <q-tooltip
+                                  >Visibilidade definida por grupo</q-tooltip
+                                >
+                              </q-icon>
+                            </span>
+                            <div class="col-auto row q-gutter-x-xs">
+                              <q-btn
+                                round
+                                unelevated
+                                :color="unit.visible ? 'positive' : 'grey-4'"
+                                icon="mdi-check"
+                                size="xs"
+                                @click="unit.visible = true"
+                              />
+                              <q-btn
+                                round
+                                :outline="unit.visible"
+                                :unelevated="!unit.visible"
+                                color="negative"
+                                icon="mdi-close"
+                                size="xs"
+                                :disable="lockedUnitIds.has(unit.id)"
+                                @click="
+                                  !lockedUnitIds.has(unit.id) &&
+                                  (unit.visible = false)
+                                "
+                              />
+                            </div>
+                          </div>
+                        </template>
+                      </template>
+                    </div>
+                  </q-scroll-area>
                 </div>
               </div>
-            </div>
-
-            <!-- Tab: Grupos -->
-            <div v-show="currentTab === 'grupos'">
-              <div class="column q-gutter-y-sm">
-                <!-- Visível para todos os Grupos -->
-                <q-input
-                  outlined
-                  readonly
-                  dense
-                  label="Visível Para todos os Grupos"
-                  label-color="secondary"
-                  color="secondary"
-                  bg-color="white"
-                  style="border-radius: 8px"
-                >
-                  <template #append>
-                    <q-btn
-                      flat
-                      round
-                      dense
-                      size="sm"
-                      icon="mdi-check"
-                      color="positive"
-                      @click="setAllGroupsVisible(true)"
-                    />
-                    <q-btn
-                      flat
-                      round
-                      dense
-                      size="sm"
-                      icon="mdi-close-circle-outline"
-                      color="negative"
-                      @click="setAllGroupsVisible(false)"
-                    />
-                  </template>
-                </q-input>
-
-                <!-- Buscar Grupo -->
-                <q-input
-                  v-model="groupSearch"
-                  outlined
-                  dense
-                  label="Buscar Grupo"
-                  label-color="secondary"
-                  color="secondary"
-                  bg-color="white"
-                  style="border-radius: 8px"
-                >
-                  <template #append>
-                    <q-icon name="mdi-magnify" color="secondary" />
-                  </template>
-                </q-input>
-
-                <!-- Lista de Grupos -->
-                <div
-                  class="rounded-borders q-pa-sm"
-                  style="
-                    background-color: #f5f5f5;
-                    max-height: 280px;
-                    overflow-y: auto;
-                  "
-                >
-                  <div v-if="loadingGroups" class="flex flex-center q-py-md">
-                    <q-spinner color="secondary" size="sm" />
-                  </div>
 
-                  <template v-else>
-                    <div class="row items-center q-mb-xs q-px-sm">
-                      <q-icon
-                        name="mdi-account-group"
-                        color="secondary"
+              <!-- Tab: Grupos -->
+              <div v-show="currentTab === 'grupos'">
+                <div class="column q-gutter-y-sm">
+                  <!-- Visível para todos os Grupos -->
+                  <q-input
+                    outlined
+                    readonly
+                    dense
+                    label="Visível Para todos os Grupos"
+                    label-color="secondary"
+                    color="secondary"
+                    bg-color="white"
+                    style="border-radius: 8px"
+                  >
+                    <template #append>
+                      <q-btn
+                        flat
+                        round
+                        dense
                         size="sm"
-                        class="q-mr-xs"
+                        icon="mdi-check"
+                        color="positive"
+                        @click="setAllGroupsVisible(true)"
                       />
-                      <span class="text-body2">
-                        Grupos ({{ filteredGroups.length }})
-                      </span>
-                    </div>
+                      <q-btn
+                        flat
+                        round
+                        dense
+                        size="sm"
+                        icon="mdi-close-circle-outline"
+                        color="negative"
+                        @click="setAllGroupsVisible(false)"
+                      />
+                    </template>
+                  </q-input>
 
-                    <q-separator class="q-mb-xs" />
+                  <!-- Buscar Grupo -->
+                  <q-input
+                    v-model="groupSearch"
+                    outlined
+                    dense
+                    label="Buscar Grupo"
+                    label-color="secondary"
+                    color="secondary"
+                    bg-color="white"
+                    style="border-radius: 8px"
+                  >
+                    <template #append>
+                      <q-icon name="mdi-magnify" color="secondary" />
+                    </template>
+                  </q-input>
 
-                    <div class="row q-px-sm q-py-xs">
-                      <span class="col text-caption text-grey-6">Grupo</span>
-                      <span class="col-6 text-caption text-grey-6">Unidades</span>
-                      <span class="col-auto text-caption text-grey-6">Status</span>
-                    </div>
+                  <!-- Lista de Grupos -->
+                  <q-scroll-area
+                    class="rounded-borders"
+                    style="height: 280px; background-color: #f5f5f5"
+                  >
+                    <div class="q-pa-sm">
+                      <div
+                        v-if="loadingGroups"
+                        class="flex flex-center q-py-md"
+                      >
+                        <q-spinner color="secondary" size="sm" />
+                      </div>
 
-                    <template v-for="group in filteredGroups" :key="group.id">
-                      <q-separator />
-                      <div class="row items-center q-px-sm q-py-sm">
-                        <span class="col text-body2">{{ group.name }}</span>
-                        <span class="col-6 text-caption text-grey-6">
-                          {{ group.unit_ids?.length ?? 0 }} unidade(s)
-                        </span>
-                        <div class="col-auto row q-gutter-x-xs">
-                          <q-btn
-                            round
-                            unelevated
-                            :color="group.visible ? 'positive' : 'grey-4'"
-                            icon="mdi-check"
-                            size="xs"
-                            @click="group.visible = true"
-                          />
-                          <q-btn
-                            round
-                            :outline="group.visible"
-                            :unelevated="!group.visible"
-                            color="negative"
-                            icon="mdi-close"
-                            size="xs"
-                            @click="group.visible = false"
+                      <template v-else>
+                        <div class="row items-center q-mb-xs q-px-sm">
+                          <q-icon
+                            name="mdi-account-group"
+                            color="secondary"
+                            size="sm"
+                            class="q-mr-xs"
                           />
+                          <span class="text-body2">
+                            Grupos ({{ filteredGroups.length }})
+                          </span>
                         </div>
-                      </div>
-                    </template>
-                  </template>
+
+                        <q-separator class="q-mb-xs" />
+
+                        <div class="row q-px-sm q-py-xs">
+                          <span class="col text-caption text-grey-6"
+                            >Grupo</span
+                          >
+                          <span class="col-6 text-caption text-grey-6"
+                            >Unidades</span
+                          >
+                          <span class="col-auto text-caption text-grey-6"
+                            >Status</span
+                          >
+                        </div>
+
+                        <template
+                          v-for="group in filteredGroups"
+                          :key="group.id"
+                        >
+                          <q-separator />
+                          <div class="row items-center q-px-sm q-py-sm">
+                            <span class="col text-body2">{{ group.name }}</span>
+                            <span class="col-6 text-caption text-grey-6">
+                              {{ group.unit_ids?.length ?? 0 }} unidade(s)
+                            </span>
+                            <div class="col-auto row q-gutter-x-xs">
+                              <q-btn
+                                round
+                                unelevated
+                                :color="group.visible ? 'positive' : 'grey-4'"
+                                icon="mdi-check"
+                                size="xs"
+                                @click="group.visible = true"
+                              />
+                              <q-btn
+                                round
+                                :outline="group.visible"
+                                :unelevated="!group.visible"
+                                color="negative"
+                                icon="mdi-close"
+                                size="xs"
+                                @click="group.visible = false"
+                              />
+                            </div>
+                          </div>
+                        </template>
+                      </template>
+                    </div>
+                  </q-scroll-area>
                 </div>
               </div>
-            </div>
-          </q-card-section>
+            </q-card-section>
+          </q-scroll-area>
 
           <q-card-actions align="right" class="q-px-md q-pb-md">
             <q-btn
@@ -516,8 +535,7 @@ const calculateEndTime = (startTime, durationHours) => {
   const [hours, minutes] = startTime.split(":").map(Number);
   if (hours > 23 || minutes > 59) return null;
   const end =
-    (hours * 60 + minutes + Math.round(Number(durationHours) * 60)) %
-    (24 * 60);
+    (hours * 60 + minutes + Math.round(Number(durationHours) * 60)) % (24 * 60);
   return `${String(Math.floor(end / 60)).padStart(2, "0")}:${String(
     end % 60,
   ).padStart(2, "0")}`;
@@ -537,8 +555,7 @@ const form = ref({
   contract_register_value: props.package?.contract_register_value ?? null,
   contract_value: props.package?.contract_value ?? null,
   contrat_discount_value: props.package?.contrat_discount_value ?? null,
-  class_duration_hours:
-    (props.package?.class_duration_minutes ?? 120) / 60,
+  class_duration_hours: (props.package?.class_duration_minutes ?? 120) / 60,
   weekday: props.package?.weekday ?? null,
   start_time: trimTime(props.package?.start_time),
   second_weekday: props.package?.second_weekday ?? null,
@@ -642,9 +659,7 @@ const setAllVisible = (visible) => {
 const filteredGroups = computed(() => {
   const q = groupSearch.value.toLowerCase();
   if (!q) return groups.value;
-  return groups.value.filter((g) =>
-    g.name.toLowerCase().includes(q),
-  );
+  return groups.value.filter((g) => g.name.toLowerCase().includes(q));
 });
 
 const setAllGroupsVisible = (visible) => {
@@ -731,9 +746,7 @@ const onOKClick = async () => {
       start_time: form.value.start_time || null,
       second_weekday: form.value.second_weekday,
       second_start_time: form.value.second_start_time || null,
-      group_ids: groups.value
-        .filter((g) => g.visible)
-        .map((g) => g.id),
+      group_ids: groups.value.filter((g) => g.visible).map((g) => g.id),
       unit_visibilities: units.value.map((u) => ({
         unit_id: u.id,
         visible: u.visible,

+ 26 - 20
src/pages/permissions/components/AddEditPermissionGroupDialog.vue

@@ -4,25 +4,31 @@
       <DefaultDialogHeader :title="dialogTitle" @close="onDialogCancel" />
 
       <DefaultForm ref="formRef" @submit="onSave">
-        <q-card-section class="column q-gutter-md">
-          <DefaultInput
-            v-model="form.label"
-            label="Nome"
-            outlined
-            autofocus
-            :rules="[inputRules.required]"
-            @update:model-value="onLabelChange"
-          />
-
-          <DefaultInput
-            v-model="form.slug"
-            label="Código"
-            outlined
-            hint="Identificador único do grupo. Ex.: FINANCEIRO"
-            :rules="[inputRules.required, slugRule]"
-            @update:model-value="onSlugTouched"
-          />
-        </q-card-section>
+        <q-scroll-area class="form-dialog-scroll-area">
+          <q-card-section class="column q-gutter-md">
+            <DefaultInput
+              v-model="form.label"
+              v-model:error="validationErrors.label"
+              :error-message="validationErrors.label"
+              label="Nome"
+              outlined
+              autofocus
+              :rules="[inputRules.required]"
+              @update:model-value="onLabelChange"
+            />
+
+            <DefaultInput
+              v-model="form.slug"
+              v-model:error="validationErrors.slug"
+              :error-message="validationErrors.slug"
+              label="Código"
+              outlined
+              hint="Identificador único do grupo. Ex.: FINANCEIRO"
+              :rules="[inputRules.required, slugRule]"
+              @update:model-value="onSlugTouched"
+            />
+          </q-card-section>
+        </q-scroll-area>
 
         <q-card-actions align="right">
           <q-btn
@@ -104,7 +110,7 @@ const onSlugTouched = (value) => {
   form.value.slug = toSlug(value);
 };
 
-const { loading, execute } = useSubmitHandler({
+const { loading, validationErrors, execute } = useSubmitHandler({
   formRef,
   onSuccess: () => onDialogOK(),
 });

+ 55 - 49
src/pages/products/components/AddEditProductDialog.vue

@@ -8,47 +8,49 @@
         />
 
         <DefaultForm ref="formRef" @submit="onOKClick">
-          <q-card-section class="q-pt-sm">
-            <div class="text-h6 q-mb-sm">Produto</div>
-
-            <div class="row q-col-gutter-sm">
-              <DefaultInput
-                v-model="form.name"
-                label="Nome do Produto"
-                class="col-12"
-              />
-
-              <DefaultInput
-                v-model="form.description"
-                label="Descrição"
-                class="col-12"
-                type="textarea"
-                autogrow
-              />
-
-              <DefaultCurrencyInput
-                v-model="form.unitPrice"
-                label="Valor Unitário"
-                class="col-4"
-              />
-
-              <DefaultInput
-                v-model="form.quantity"
-                label="Quantidade"
-                class="col-4"
-                type="number"
-                min="0"
-              />
-
-              <DefaultInput
-                :model-value="totalValueFormatted"
-                label="Valor Total do Produto"
-                class="col-4"
-                disable
-                readonly
-              />
-            </div>
-          </q-card-section>
+          <q-scroll-area class="form-dialog-scroll-area">
+            <q-card-section class="q-pt-sm">
+              <div class="text-h6 q-mb-sm">Produto</div>
+
+              <div class="row q-col-gutter-sm">
+                <DefaultInput
+                  v-model="form.name"
+                  label="Nome do Produto"
+                  class="col-12"
+                />
+
+                <DefaultInput
+                  v-model="form.description"
+                  label="Descrição"
+                  class="col-12"
+                  type="textarea"
+                  autogrow
+                />
+
+                <DefaultCurrencyInput
+                  v-model="form.unitPrice"
+                  label="Valor Unitário"
+                  class="col-4"
+                />
+
+                <DefaultInput
+                  v-model="form.quantity"
+                  label="Quantidade"
+                  class="col-4"
+                  type="number"
+                  min="0"
+                />
+
+                <DefaultInput
+                  :model-value="totalValueFormatted"
+                  label="Valor Total do Produto"
+                  class="col-4"
+                  disable
+                  readonly
+                />
+              </div>
+            </q-card-section>
+          </q-scroll-area>
 
           <q-card-actions align="right" class="q-px-md q-pb-md">
             <q-btn
@@ -100,23 +102,25 @@ const loading = ref(false);
 const isEdit = computed(() => !!props.product);
 
 const form = ref({
-  name:        props.product?.name        ?? "",
+  name: props.product?.name ?? "",
   description: props.product?.description ?? "",
-  unitPrice:   props.product?.price_sale  ?? 0,
-  quantity:    props.product?.quantity    ?? 0,
+  unitPrice: props.product?.price_sale ?? 0,
+  quantity: props.product?.quantity ?? 0,
 });
 
 const totalValue = computed(() => form.value.unitPrice * form.value.quantity);
-const totalValueFormatted = computed(() => formatToBRLCurrency(totalValue.value));
+const totalValueFormatted = computed(() =>
+  formatToBRLCurrency(totalValue.value),
+);
 
 const onOKClick = async () => {
   loading.value = true;
   try {
     const payload = {
-      name:        form.value.name,
+      name: form.value.name,
       description: form.value.description,
-      price_sale:  form.value.unitPrice,
-      quantity:    Number(form.value.quantity),
+      price_sale: form.value.unitPrice,
+      quantity: Number(form.value.quantity),
     };
     const product = isEdit.value
       ? await updateProduct(props.product.id, payload)
@@ -125,7 +129,9 @@ const onOKClick = async () => {
   } catch {
     $q.notify({
       type: "negative",
-      message: isEdit.value ? "Erro ao editar produto." : "Erro ao cadastrar produto.",
+      message: isEdit.value
+        ? "Erro ao editar produto."
+        : "Erro ao cadastrar produto.",
     });
   } finally {
     loading.value = false;

+ 77 - 72
src/pages/products/components/AddEditStockProductDialog.vue

@@ -5,73 +5,75 @@
         <DefaultDialogHeader title="Ajustar Estoque" @close="onDialogCancel" />
 
         <DefaultForm @submit="onOKClick">
-          <q-card-section class="q-pt-sm">
-            <div class="text-h6 q-mb-sm">Produto</div>
-
-            <div class="row q-col-gutter-sm q-mb-md">
-              <DefaultInput
-                :model-value="product.name"
-                label="Nome do Produto"
-                class="col-12"
-                disable
-                readonly
-              />
-
-              <DefaultInput
-                :model-value="product.description"
-                label="Descrição"
-                class="col-12"
-                type="textarea"
-                autogrow
-                disable
-                readonly
-              />
-
-              <DefaultInput
-                :model-value="formatToBRLCurrency(product.price_sale)"
-                label="Valor Unitário"
-                class="col-6"
-                disable
-                readonly
-              />
-
-              <DefaultInput
-                :model-value="String(product.quantity)"
-                label="Estoque Atual"
-                class="col-6"
-                disable
-                readonly
-              />
-            </div>
-
-            <q-separator class="q-mb-md" />
-
-            <div class="text-h6 q-mb-sm">Movimentação</div>
-
-            <div class="row q-col-gutter-sm">
-              <DefaultSelect
-                v-model="form.type"
-                label="Tipo"
-                class="col-6"
-                :options="typeOptions"
-                emit-value
-                map-options
-              />
-
-              <DefaultInput
-                v-model="form.quantity"
-                label="Quantidade"
-                class="col-6"
-                type="number"
-                min="1"
-                :rules="[quantityRule]"
-              />
-            </div>
-
-            <div v-if="stockError" class="text-negative text-caption q-mt-sm">
-              {{ stockError }}
-            </div>
-          </q-card-section>
+          <q-scroll-area class="form-dialog-scroll-area">
+            <q-card-section class="q-pt-sm">
+              <div class="text-h6 q-mb-sm">Produto</div>
+
+              <div class="row q-col-gutter-sm q-mb-md">
+                <DefaultInput
+                  :model-value="product.name"
+                  label="Nome do Produto"
+                  class="col-12"
+                  disable
+                  readonly
+                />
+
+                <DefaultInput
+                  :model-value="product.description"
+                  label="Descrição"
+                  class="col-12"
+                  type="textarea"
+                  autogrow
+                  disable
+                  readonly
+                />
+
+                <DefaultInput
+                  :model-value="formatToBRLCurrency(product.price_sale)"
+                  label="Valor Unitário"
+                  class="col-6"
+                  disable
+                  readonly
+                />
+
+                <DefaultInput
+                  :model-value="String(product.quantity)"
+                  label="Estoque Atual"
+                  class="col-6"
+                  disable
+                  readonly
+                />
+              </div>
+
+              <q-separator class="q-mb-md" />
+
+              <div class="text-h6 q-mb-sm">Movimentação</div>
+
+              <div class="row q-col-gutter-sm">
+                <DefaultSelect
+                  v-model="form.type"
+                  label="Tipo"
+                  class="col-6"
+                  :options="typeOptions"
+                  emit-value
+                  map-options
+                />
+
+                <DefaultInput
+                  v-model="form.quantity"
+                  label="Quantidade"
+                  class="col-6"
+                  type="number"
+                  min="1"
+                  :rules="[quantityRule]"
+                />
+              </div>
+
+              <div v-if="stockError" class="text-negative text-caption q-mt-sm">
+                {{ stockError }}
+              </div>
+            </q-card-section>
+          </q-scroll-area>
 
           <q-card-actions align="right" class="q-px-md q-pb-md">
             <q-btn
@@ -125,13 +127,16 @@ const typeOptions = [
 ];
 
 const form = ref({
-  type:     "entrada",
+  type: "entrada",
   quantity: 1,
 });
 
-watch(() => [form.value.type, form.value.quantity], () => {
-  stockError.value = null;
-});
+watch(
+  () => [form.value.type, form.value.quantity],
+  () => {
+    stockError.value = null;
+  },
+);
 
 const quantityRule = (val) => {
   const qty = Number(val);
@@ -153,7 +158,7 @@ const onOKClick = async () => {
   loading.value = true;
   try {
     const updated = await adjustProductStock(props.product.id, {
-      type:     form.value.type,
+      type: form.value.type,
       quantity: qty,
     });
     onDialogOK(updated);

+ 48 - 44
src/pages/products/components/AddProductDialog.vue

@@ -5,47 +5,49 @@
         <DefaultDialogHeader title="Novo Produto" @close="onDialogCancel" />
 
         <DefaultForm ref="formRef" @submit="onOKClick">
-          <q-card-section class="q-pt-sm">
-            <div class="text-h6 q-mb-sm">Produto</div>
-
-            <div class="row q-col-gutter-sm">
-              <DefaultInput
-                v-model="form.name"
-                label="Nome do Produto"
-                class="col-12"
-              />
-
-              <DefaultInput
-                v-model="form.description"
-                label="Descrição"
-                class="col-12"
-                type="textarea"
-                autogrow
-              />
-
-              <DefaultCurrencyInput
-                v-model="form.unitPrice"
-                label="Valor Unitário"
-                class="col-4"
-              />
-
-              <DefaultInput
-                v-model="form.quantity"
-                label="Quantidade"
-                class="col-4"
-                type="number"
-                min="0"
-              />
-
-              <DefaultInput
-                :model-value="totalValueFormatted"
-                label="Valor Total do Produto"
-                class="col-4"
-                disable
-                readonly
-              />
-            </div>
-          </q-card-section>
+          <q-scroll-area class="form-dialog-scroll-area">
+            <q-card-section class="q-pt-sm">
+              <div class="text-h6 q-mb-sm">Produto</div>
+
+              <div class="row q-col-gutter-sm">
+                <DefaultInput
+                  v-model="form.name"
+                  label="Nome do Produto"
+                  class="col-12"
+                />
+
+                <DefaultInput
+                  v-model="form.description"
+                  label="Descrição"
+                  class="col-12"
+                  type="textarea"
+                  autogrow
+                />
+
+                <DefaultCurrencyInput
+                  v-model="form.unitPrice"
+                  label="Valor Unitário"
+                  class="col-4"
+                />
+
+                <DefaultInput
+                  v-model="form.quantity"
+                  label="Quantidade"
+                  class="col-4"
+                  type="number"
+                  min="0"
+                />
+
+                <DefaultInput
+                  :model-value="totalValueFormatted"
+                  label="Valor Total do Produto"
+                  class="col-4"
+                  disable
+                  readonly
+                />
+              </div>
+            </q-card-section>
+          </q-scroll-area>
 
           <q-card-actions align="right" class="q-px-md q-pb-md">
             <q-btn
@@ -95,15 +97,17 @@ const form = ref({
 });
 
 const totalValue = computed(() => form.value.unitPrice * form.value.quantity);
-const totalValueFormatted = computed(() => formatToBRLCurrency(totalValue.value));
+const totalValueFormatted = computed(() =>
+  formatToBRLCurrency(totalValue.value),
+);
 
 const onOKClick = async () => {
   loading.value = true;
   try {
     const product = await createProduct({
-      name:        form.value.name,
+      name: form.value.name,
       description: form.value.description,
-      price_sale:  form.value.unitPrice,
+      price_sale: form.value.unitPrice,
     });
     onDialogOK(product);
   } catch {

+ 13 - 9
src/pages/support/components/AddEditReplyDialog.vue

@@ -8,14 +8,16 @@
         />
 
         <DefaultForm ref="formRef" @submit="onOKClick">
-          <q-card-section class="q-pt-sm">
-            <DefaultInput
-              v-model="replyText"
-              label="Descreva a resposta"
-              type="textarea"
-              class="col-12"
-            />
-          </q-card-section>
+          <q-scroll-area class="form-dialog-scroll-area">
+            <q-card-section class="q-pt-sm">
+              <DefaultInput
+                v-model="replyText"
+                label="Descreva a resposta"
+                type="textarea"
+                class="col-12"
+              />
+            </q-card-section>
+          </q-scroll-area>
 
           <q-card-actions align="right" class="q-px-md q-pb-md">
             <q-btn
@@ -68,7 +70,9 @@ const onOKClick = async () => {
   loading.value = true;
   try {
     if (replyItem) {
-      await updateSupportReply(ticketId, replyItem.id, { reply: replyText.value });
+      await updateSupportReply(ticketId, replyItem.id, {
+        reply: replyText.value,
+      });
     } else {
       await createSupportReply(ticketId, { reply: replyText.value });
     }

+ 117 - 108
src/pages/support/components/AddEditTicketDialog.vue

@@ -1,7 +1,14 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
     <div style="width: 100%; max-width: 1100px">
-      <q-card style="height: 500px; display: flex; flex-direction: column; overflow: hidden">
+      <q-card
+        style="
+          height: 500px;
+          display: flex;
+          flex-direction: column;
+          overflow: hidden;
+        "
+      >
         <DefaultDialogHeader
           :title="ticket ? 'Editar Ticket' : 'Novo Ticket'"
           @close="onDialogCancel"
@@ -17,117 +24,114 @@
           "
           @submit="onOKClick"
         >
-          <q-card-section class="q-pt-sm" style="flex: 1; overflow-y: auto; min-height: 0">
-            <CustomTabComponent
-              v-if="ticket?.id"
-              v-model:active-tab="currentTab"
-              :tabs="tabs"
-              class="q-mb-md"
-            />
+          <q-scroll-area class="form-dialog-scroll-area">
+            <q-card-section class="q-pt-sm">
+              <CustomTabComponent
+                v-if="ticket?.id"
+                v-model:active-tab="currentTab"
+                :tabs="tabs"
+                class="q-mb-md"
+              />
+
+              <!-- Tab: Ticket -->
+              <div v-show="currentTab === 'ticket'">
+                <div class="row q-col-gutter-sm">
+                  <DefaultInput
+                    v-model="form.title"
+                    label="Título da Tarefa"
+                    class="col-12"
+                  />
 
-            <!-- Tab: Ticket -->
-            <div v-show="currentTab === 'ticket'">
-              <div class="row q-col-gutter-sm">
-                <DefaultInput
-                  v-model="form.title"
-                  label="Título da Tarefa"
-                  class="col-12"
-                />
-
-                <DefaultSelect
-                  v-model="form.severity"
-                  label="Prioridade"
-                  :options="priorityOptions"
-                  emit-value
-                  map-options
-                  class="col-6"
-                />
-
-                <DefaultInput
-                  :model-value="user?.name"
-                  label="Responsável"
-                  disable
-                  class="col-6"
-                />
-
-                <DefaultSelect
-                  v-model="form.scope"
-                  label="Destino"
-                  :options="unitTargetOptions"
-                  emit-value
-                  map-options
-                  class="col-6"
-                />
-
-                <DefaultInput
-                  v-model="form.sector"
-                  label="Setor"
-                  class="col-6"
-                />
-
-                <UnitSelect
-                  v-if="form.scope === 'specific'"
-                  v-model="selectedUnit"
-                  class="col-6"
-                />
-
-                <DefaultInput
-                  v-model="form.description"
-                  label="Descrição"
-                  type="textarea"
-                  class="col-12"
-                />
-              </div>
-            </div>
-
-            <!-- Tab: Comentários -->
-            <div v-show="currentTab === 'comentarios'">
-              <div
-                v-if="canAdd && ticket?.status === 'in_progress'"
-                class="flex justify-end q-mb-sm"
-              >
-                <q-btn
-                  color="primary"
-                  icon="mdi-plus"
-                  unelevated
-                  style="width: 40px; height: 40px"
-                  @click="onAddComment"
-                />
-              </div>
-              <div
-                style="
-                  height: 100%;
-                  max-height: 340px;
-                  overflow-y: auto;
-                  display: flex;
-                  flex-direction: column;
-                  gap: 8px;
-                "
-              >
-                <template v-if="replies.length">
-                  <TicketCommentCard
-                    v-for="reply in replies"
-                    :key="reply.id"
-                    :reply="reply.reply"
-                    :created-at="reply.created_at"
-                    :user-name="reply.user_name"
-                    :can-edit="canEdit"
-                    :can-delete="canDelete"
-                    @edit="onEditComment(reply)"
-                    @delete="onDeleteComment(reply)"
+                  <DefaultSelect
+                    v-model="form.severity"
+                    label="Prioridade"
+                    :options="priorityOptions"
+                    emit-value
+                    map-options
+                    class="col-6"
+                  />
+
+                  <DefaultInput
+                    :model-value="user?.name"
+                    label="Responsável"
+                    disable
+                    class="col-6"
+                  />
+
+                  <DefaultSelect
+                    v-model="form.scope"
+                    label="Destino"
+                    :options="unitTargetOptions"
+                    emit-value
+                    map-options
+                    class="col-6"
+                  />
+
+                  <DefaultInput
+                    v-model="form.sector"
+                    label="Setor"
+                    class="col-6"
                   />
-                </template>
+
+                  <UnitSelect
+                    v-if="form.scope === 'specific'"
+                    v-model="selectedUnit"
+                    class="col-6"
+                  />
+
+                  <DefaultInput
+                    v-model="form.description"
+                    label="Descrição"
+                    type="textarea"
+                    class="col-12"
+                  />
+                </div>
+              </div>
+
+              <!-- Tab: Comentários -->
+              <div v-show="currentTab === 'comentarios'">
                 <div
-                  v-else
-                  class="flex flex-center full-height text-grey-5 text-body2"
+                  v-if="canAdd && ticket?.status === 'in_progress'"
+                  class="flex justify-end q-mb-sm"
                 >
-                  Nenhum comentário registrado.
+                  <q-btn
+                    color="primary"
+                    icon="mdi-plus"
+                    unelevated
+                    style="width: 40px; height: 40px"
+                    @click="onAddComment"
+                  />
+                </div>
+                <div style="display: flex; flex-direction: column; gap: 8px">
+                  <template v-if="replies.length">
+                    <TicketCommentCard
+                      v-for="reply in replies"
+                      :key="reply.id"
+                      :reply="reply.reply"
+                      :created-at="reply.created_at"
+                      :user-name="reply.user_name"
+                      :can-edit="canEdit"
+                      :can-delete="canDelete"
+                      @edit="onEditComment(reply)"
+                      @delete="onDeleteComment(reply)"
+                    />
+                  </template>
+                  <div
+                    v-else
+                    class="flex flex-center full-height text-grey-5 text-body2"
+                  >
+                    Nenhum comentário registrado.
+                  </div>
                 </div>
               </div>
-            </div>
-          </q-card-section>
-
-          <q-card-actions align="right" class="q-px-md q-pb-md" style="flex-shrink: 0">
+            </q-card-section>
+          </q-scroll-area>
+
+          <q-card-actions
+            align="right"
+            class="q-px-md q-pb-md"
+            style="flex-shrink: 0"
+          >
             <q-btn
               outline
               color="primary"
@@ -142,7 +146,10 @@
               @click="onCloseTicket"
             />
             <q-btn
-              v-if="(!ticket?.id && canAdd) || (ticket?.id && canEdit && ticket?.status === 'in_progress')"
+              v-if="
+                (!ticket?.id && canAdd) ||
+                (ticket?.id && canEdit && ticket?.status === 'in_progress')
+              "
               color="primary"
               label="Salvar"
               type="submit"
@@ -198,7 +205,9 @@ const { user } = userStore();
 const formRef = ref(null);
 const loading = ref(false);
 const currentTab = ref("ticket");
-const selectedUnit = ref(ticket?.target_unit_id ? { value: ticket.target_unit_id } : null);
+const selectedUnit = ref(
+  ticket?.target_unit_id ? { value: ticket.target_unit_id } : null,
+);
 
 const tabs = computed(() => {
   const base = [{ name: "ticket", label: "Ticket" }];

+ 14 - 9
src/pages/support/components/AddReplyDialog.vue

@@ -2,17 +2,22 @@
   <q-dialog ref="dialogRef" @hide="onDialogHide">
     <div style="width: 100%; max-width: 600px">
       <q-card class="overflow-hidden" style="width: 100%">
-        <DefaultDialogHeader title="Responder Suporte" @close="onDialogCancel" />
+        <DefaultDialogHeader
+          title="Responder Suporte"
+          @close="onDialogCancel"
+        />
 
         <DefaultForm ref="formRef" @submit="onOKClick">
-          <q-card-section class="q-pt-sm">
-            <DefaultInput
-              v-model="reply"
-              label="Descreva a resposta"
-              type="textarea"
-              class="col-12"
-            />
-          </q-card-section>
+          <q-scroll-area class="form-dialog-scroll-area">
+            <q-card-section class="q-pt-sm">
+              <DefaultInput
+                v-model="reply"
+                label="Descreva a resposta"
+                type="textarea"
+                class="col-12"
+              />
+            </q-card-section>
+          </q-scroll-area>
 
           <q-card-actions align="right" class="q-px-md q-pb-md">
             <q-btn

+ 50 - 48
src/pages/tbr/components/AddEditInhabitantClassificationDialog.vue

@@ -10,54 +10,56 @@
       />
 
       <DefaultForm ref="formRef" @submit="onOKClick">
-        <q-card-section class="row q-col-gutter-sm q-pt-none">
-          <DefaultInput
-            v-model="form.description"
-            v-model:error="validationErrors.description"
-            class="col-12"
-            label="Descrição"
-            :error-message="validationErrors.description"
-            :rules="[inputRules.required]"
-          />
-
-          <DefaultInput
-            v-model="form.start"
-            v-model:error="validationErrors.start"
-            class="col-6"
-            label="Início"
-            min="1"
-            type="number"
-            :error-message="validationErrors.start"
-            :rules="[inputRules.required]"
-          />
-
-          <DefaultInput
-            v-model="form.end"
-            v-model:error="validationErrors.end"
-            class="col-6"
-            label="Fim"
-            min="1"
-            type="number"
-            :error-message="validationErrors.end"
-            :rules="[inputRules.required]"
-          />
-
-          <DefaultInput
-            v-model="form.tbr_percentage"
-            v-model:error="validationErrors.tbr_percentage"
-            class="col-12"
-            label="Porcentagem de Royalties"
-            min="0"
-            step="0.01"
-            type="number"
-            :error-message="validationErrors.tbr_percentage"
-            :rules="[
-              (v) =>
-                (v !== null && v !== undefined && v !== '') ||
-                $t('validation.rules.required'),
-            ]"
-          />
-        </q-card-section>
+        <q-scroll-area class="form-dialog-scroll-area">
+          <q-card-section class="row q-col-gutter-sm q-pt-none">
+            <DefaultInput
+              v-model="form.description"
+              v-model:error="validationErrors.description"
+              class="col-12"
+              label="Descrição"
+              :error-message="validationErrors.description"
+              :rules="[inputRules.required]"
+            />
+
+            <DefaultInput
+              v-model="form.start"
+              v-model:error="validationErrors.start"
+              class="col-6"
+              label="Início"
+              min="1"
+              type="number"
+              :error-message="validationErrors.start"
+              :rules="[inputRules.required]"
+            />
+
+            <DefaultInput
+              v-model="form.end"
+              v-model:error="validationErrors.end"
+              class="col-6"
+              label="Fim"
+              min="1"
+              type="number"
+              :error-message="validationErrors.end"
+              :rules="[inputRules.required]"
+            />
+
+            <DefaultInput
+              v-model="form.tbr_percentage"
+              v-model:error="validationErrors.tbr_percentage"
+              class="col-12"
+              label="Porcentagem de Royalties"
+              min="0"
+              step="0.01"
+              type="number"
+              :error-message="validationErrors.tbr_percentage"
+              :rules="[
+                (v) =>
+                  (v !== null && v !== undefined && v !== '') ||
+                  $t('validation.rules.required'),
+              ]"
+            />
+          </q-card-section>
+        </q-scroll-area>
 
         <q-card-actions>
           <q-space />

+ 69 - 57
src/pages/tbr/components/AddEditTbrDialog.vue

@@ -10,62 +10,74 @@
       />
 
       <DefaultForm ref="formRef" @submit="onOKClick">
-        <q-card-section class="q-pt-none">
-          <p class="text-subtitle2 q-mb-md">Definir Valores e TBR</p>
-
-          <div class="row q-col-gutter-sm">
-            <DefaultInput
-              v-model="form.year"
-              label="Ano"
-              class="col-3"
-              outlined
-              type="number"
-              min="2000"
-              max="2100"
-              :rules="[inputRules.required]"
-            />
-
-            <DefaultCurrencyInput
-              v-model="form.tbr_value"
-              label="TBR $"
-              class="col-3"
-              outlined
-            />
-
-            <DefaultInput
-              v-model="form.royalties_display"
-              label="Royalties %"
-              class="col-2"
-              outlined
-              type="number"
-              min="0"
-              max="100"
-              :rules="[inputRules.required]"
-            />
-
-            <DefaultInput
-              v-model="form.fnm_display"
-              label="FMN %"
-              class="col-2"
-              outlined
-              type="number"
-              min="0"
-              max="100"
-              :rules="[inputRules.required]"
-            />
-
-            <DefaultInput
-              v-model="form.maintenance_display"
-              label="Manutenção %"
-              class="col-2"
-              outlined
-              type="number"
-              min="0"
-              max="100"
-              :rules="[inputRules.required]"
-            />
-          </div>
-        </q-card-section>
+        <q-scroll-area class="form-dialog-scroll-area">
+          <q-card-section class="q-pt-none">
+            <p class="text-subtitle2 q-mb-md">Definir Valores e TBR</p>
+
+            <div class="row q-col-gutter-sm">
+              <DefaultInput
+                v-model="form.year"
+                v-model:error="validationErrors.year"
+                :error-message="validationErrors.year"
+                label="Ano"
+                class="col-3"
+                outlined
+                type="number"
+                min="2000"
+                max="2100"
+                :rules="[inputRules.required]"
+              />
+
+              <DefaultCurrencyInput
+                v-model="form.tbr_value"
+                v-model:error="validationErrors.tbr_value"
+                :error-message="validationErrors.tbr_value"
+                label="TBR $"
+                class="col-3"
+                outlined
+              />
+
+              <DefaultInput
+                v-model="form.royalties_display"
+                v-model:error="validationErrors.royalties_percentage"
+                :error-message="validationErrors.royalties_percentage"
+                label="Royalties %"
+                class="col-2"
+                outlined
+                type="number"
+                min="0"
+                max="100"
+                :rules="[inputRules.required]"
+              />
+
+              <DefaultInput
+                v-model="form.fnm_display"
+                v-model:error="validationErrors.fnm_percentage"
+                :error-message="validationErrors.fnm_percentage"
+                label="FMN %"
+                class="col-2"
+                outlined
+                type="number"
+                min="0"
+                max="100"
+                :rules="[inputRules.required]"
+              />
+
+              <DefaultInput
+                v-model="form.maintenance_display"
+                v-model:error="validationErrors.maintenance_percentage"
+                :error-message="validationErrors.maintenance_percentage"
+                label="Manutenção %"
+                class="col-2"
+                outlined
+                type="number"
+                min="0"
+                max="100"
+                :rules="[inputRules.required]"
+              />
+            </div>
+          </q-card-section>
+        </q-scroll-area>
 
         <q-card-actions>
           <q-space />
@@ -121,7 +133,7 @@ const form = ref({
   maintenance_display: tbr ? Number(tbr.maintenance_percentage) * 100 : null,
 });
 
-const { loading, execute } = useSubmitHandler({
+const { loading, validationErrors, execute } = useSubmitHandler({
   formRef,
   onSuccess: (result) => onDialogOK(result),
 });

+ 14 - 9
src/pages/tbr/components/GenerateBillingsDialog.vue

@@ -1,17 +1,22 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <q-card class="q-dialog-plugin overflow-hidden" style="width: 100%; max-width: 400px">
+    <q-card
+      class="q-dialog-plugin overflow-hidden"
+      style="width: 100%; max-width: 400px"
+    >
       <DefaultDialogHeader title="Mês de Cobrança" @close="onDialogCancel" />
 
       <DefaultForm ref="formRef" @submit="onGenerate">
-        <q-card-section class="q-pt-none">
-          <DefaultSelect
-            v-model="month"
-            label="Mês"
-            :options="monthOptions"
-            :rules="[inputRules.required]"
-          />
-        </q-card-section>
+        <q-scroll-area class="form-dialog-scroll-area">
+          <q-card-section class="q-pt-none">
+            <DefaultSelect
+              v-model="month"
+              label="Mês"
+              :options="monthOptions"
+              :rules="[inputRules.required]"
+            />
+          </q-card-section>
+        </q-scroll-area>
 
         <q-card-actions>
           <q-space />

+ 51 - 29
src/pages/unit/components/AddEditHistoryDialog.vue

@@ -1,39 +1,58 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <q-card class="q-dialog-plugin overflow-hidden" style="width: 560px; max-width: 95vw">
+    <q-card
+      class="q-dialog-plugin overflow-hidden"
+      style="width: 560px; max-width: 95vw"
+    >
       <DefaultDialogHeader
         :title="history ? 'Editar Histórico' : 'Novo Histórico'"
         @close="onDialogCancel"
       />
 
       <DefaultForm ref="formRef" @submit="onOKClick">
-        <q-card-section class="q-pt-none">
-          <div class="column q-gutter-sm">
-            <DefaultInput
-              v-model="form.title"
-              label="Título"
-              outlined
-              :rules="[inputRules.required]"
-            />
-            <q-input
-              v-model="form.content"
-              label="Conteúdo"
-              outlined
-              type="textarea"
-              rows="5"
-              autogrow
-            />
-            <q-toggle
-              v-model="form.visible_to_franchisee"
-              label="Visível ao franqueado"
-              color="positive"
-            />
-          </div>
-        </q-card-section>
+        <q-scroll-area class="form-dialog-scroll-area">
+          <q-card-section class="q-pt-none">
+            <div class="column q-gutter-sm">
+              <DefaultInput
+                v-model="form.title"
+                v-model:error="validationErrors.title"
+                :error-message="validationErrors.title"
+                label="Título"
+                outlined
+                :rules="[inputRules.required]"
+              />
+              <q-input
+                v-model="form.content"
+                :error="!!validationErrors.content"
+                :error-message="validationErrors.content"
+                label="Conteúdo"
+                outlined
+                type="textarea"
+                rows="5"
+                autogrow
+              />
+              <q-toggle
+                v-model="form.visible_to_franchisee"
+                label="Visível ao franqueado"
+                color="positive"
+              />
+            </div>
+          </q-card-section>
+        </q-scroll-area>
 
         <q-card-actions align="right" class="q-pa-md">
-          <q-btn outline color="primary" label="Cancelar" @click="onDialogCancel" />
-          <q-btn color="primary-2" :label="history ? 'Salvar' : 'Adicionar'" type="submit" :loading="loading" />
+          <q-btn
+            outline
+            color="primary"
+            label="Cancelar"
+            @click="onDialogCancel"
+          />
+          <q-btn
+            color="primary-2"
+            :label="history ? 'Salvar' : 'Adicionar'"
+            type="submit"
+            :loading="loading"
+          />
         </q-card-actions>
       </DefaultForm>
     </q-card>
@@ -57,7 +76,8 @@ const { history, unitId } = defineProps({
 });
 
 const { inputRules } = useInputRules();
-const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
 
 const formRef = ref(null);
 
@@ -67,7 +87,7 @@ const form = ref({
   visible_to_franchisee: history?.visible_to_franchisee ?? false,
 });
 
-const { loading, execute } = useSubmitHandler({
+const { loading, validationErrors, execute } = useSubmitHandler({
   formRef,
   onSuccess: (result) => onDialogOK(result),
 });
@@ -81,7 +101,9 @@ async function onOKClick() {
       visible_to_franchisee: form.value.visible_to_franchisee,
     };
 
-    return history ? updateHistory(history.id, payload) : createHistory(payload);
+    return history
+      ? updateHistory(history.id, payload)
+      : createHistory(payload);
   });
 }
 </script>

+ 193 - 150
src/pages/unit/components/AddEditPartnerDialog.vue

@@ -10,155 +10,198 @@
       />
 
       <DefaultForm ref="formRef" @submit="onOKClick">
-        <q-card-section class="q-pt-none">
-          <div class="column items-center q-mb-md">
-            <AvatarImageComponent
-              ref="avatarRef"
-              @update:file="onAvatarChange"
-            />
-          </div>
-
-          <div class="row q-col-gutter-sm">
-            <DefaultInput
-              v-model="form.name"
-              label="Nome completo"
-              class="col-8"
-              outlined
-              :rules="[inputRules.required]"
-            />
-
-            <DefaultInput
-              v-model="form.role"
-              label="Função"
-              class="col-4"
-              outlined
-            />
-
-            <DefaultInput
-              v-model="form.social_name"
-              label="Nome social"
-              class="col-6"
-              outlined
-            />
-
-            <DefaultInput
-              v-model="form.cpf"
-              label="CPF"
-              class="col-3"
-              outlined
-              :mask="masks.Brasil.cpf"
-              :rules="[inputRules.required]"
-            />
-
-            <DefaultInput v-model="form.rg" label="RG" class="col-3" outlined />
-
-            <DefaultInput
-              v-model="birthDateDisplay"
-              label="Data de Nascimento"
-              class="col-3"
-              outlined
-              :mask="masks.Brasil.date"
-              placeholder="DD/MM/AAAA"
-            />
-
-            <DefaultInput
-              v-model="form.participation"
-              label="Participação (%)"
-              class="col-3"
-              outlined
-              type="number"
-              min="0"
-              max="100"
-            />
-
-            <DefaultInput
-              v-model="form.email"
-              label="E-mail"
-              class="col-6"
-              outlined
-              :rules="[inputRules.email]"
-            />
-
-            <DefaultInput
-              v-model="form.secondary_email"
-              label="E-mail Secundário"
-              class="col-6"
-              outlined
-              :rules="[inputRules.email]"
-            />
-
-            <DefaultInput
-              v-model="form.phone_number"
-              label="Telefone"
-              class="col-6"
-              outlined
-              :mask="masks.Brasil.telefone"
-            />
-
-            <DefaultInput
-              v-model="form.cell_number"
-              label="Celular"
-              class="col-6"
-              outlined
-              :mask="masks.Brasil.celular"
-            />
-
-            <DefaultCepInput
-              v-model="form.postal_code"
-              class="col-6"
-              outlined
-              @rua="form.street = $event"
-              @bairro="form.neighborhood = $event"
-              @uf="stateSelectRef?.selectStateByCode($event)"
-              @cidade="citySelectRef?.selectCityByName($event)"
-            />
-
-            <DefaultInput
-              v-model="form.street"
-              label="Endereço"
-              class="col-6"
-              outlined
-            />
-
-            <DefaultInput
-              v-model="form.address_number"
-              label="Número"
-              class="col-6"
-              outlined
-            />
-
-            <DefaultInput
-              v-model="form.neighborhood"
-              label="Bairro"
-              class="col-4"
-              outlined
-            />
-
-            <StateSelect
-              ref="stateSelectRef"
-              v-model="selectedState"
-              label="Estado"
-              class="col-4"
-              outlined
-            />
-
-            <CitySelect
-              ref="citySelectRef"
-              v-model="selectedCity"
-              label="Cidade"
-              class="col-4"
-              outlined
-              :state="selectedState"
-            />
-
-            <DefaultInput
-              v-model="form.complement"
-              label="Complemento"
-              class="col-12"
-              outlined
-            />
-          </div>
-        </q-card-section>
+        <q-scroll-area class="form-dialog-scroll-area">
+          <q-card-section class="q-pt-none">
+            <div class="column items-center q-mb-md">
+              <AvatarImageComponent
+                ref="avatarRef"
+                @update:file="onAvatarChange"
+              />
+            </div>
+
+            <div class="row q-col-gutter-sm">
+              <DefaultInput
+                v-model="form.name"
+                v-model:error="validationErrors.name"
+                :error-message="validationErrors.name"
+                label="Nome completo"
+                class="col-8"
+                outlined
+                :rules="[inputRules.required, inputRules.cpf]"
+              />
+
+              <DefaultInput
+                v-model="form.role"
+                v-model:error="validationErrors.role"
+                :error-message="validationErrors.role"
+                label="Função"
+                class="col-4"
+                outlined
+              />
+
+              <DefaultInput
+                v-model="form.social_name"
+                v-model:error="validationErrors.social_name"
+                :error-message="validationErrors.social_name"
+                label="Nome social"
+                class="col-6"
+                outlined
+              />
+
+              <DefaultInput
+                v-model="form.cpf"
+                v-model:error="validationErrors.cpf"
+                :error-message="validationErrors.cpf"
+                label="CPF"
+                class="col-3"
+                outlined
+                :mask="masks.Brasil.cpf"
+                :rules="[inputRules.required]"
+              />
+
+              <DefaultInput
+                v-model="form.rg"
+                v-model:error="validationErrors.rg"
+                :error-message="validationErrors.rg"
+                label="RG"
+                class="col-3"
+                outlined
+              />
+
+              <DefaultInput
+                v-model="birthDateDisplay"
+                v-model:error="validationErrors.birth_date"
+                :error-message="validationErrors.birth_date"
+                label="Data de Nascimento"
+                class="col-3"
+                outlined
+                :mask="masks.Brasil.date"
+                placeholder="DD/MM/AAAA"
+              />
+
+              <DefaultInput
+                v-model="form.participation"
+                v-model:error="validationErrors.participation"
+                :error-message="validationErrors.participation"
+                label="Participação (%)"
+                class="col-3"
+                outlined
+                type="number"
+                min="0"
+                max="100"
+              />
+
+              <DefaultInput
+                v-model="form.email"
+                v-model:error="validationErrors.email"
+                :error-message="validationErrors.email"
+                label="E-mail"
+                class="col-6"
+                outlined
+                :rules="[inputRules.email]"
+              />
+
+              <DefaultInput
+                v-model="form.secondary_email"
+                v-model:error="validationErrors.secondary_email"
+                :error-message="validationErrors.secondary_email"
+                label="E-mail Secundário"
+                class="col-6"
+                outlined
+                :rules="[inputRules.email]"
+              />
+
+              <DefaultInput
+                v-model="form.phone_number"
+                v-model:error="validationErrors.phone_number"
+                :error-message="validationErrors.phone_number"
+                label="Telefone"
+                class="col-6"
+                outlined
+                :mask="masks.Brasil.telefone"
+              />
+
+              <DefaultInput
+                v-model="form.cell_number"
+                v-model:error="validationErrors.cell_number"
+                :error-message="validationErrors.cell_number"
+                label="Celular"
+                class="col-6"
+                outlined
+                :mask="masks.Brasil.celular"
+              />
+
+              <DefaultCepInput
+                v-model="form.postal_code"
+                v-model:error="validationErrors.postal_code"
+                :error-message="validationErrors.postal_code"
+                class="col-6"
+                outlined
+                @rua="form.street = $event"
+                @bairro="form.neighborhood = $event"
+                @uf="stateSelectRef?.selectStateByCode($event)"
+                @cidade="citySelectRef?.selectCityByName($event)"
+              />
+
+              <DefaultInput
+                v-model="form.street"
+                v-model:error="validationErrors.street"
+                :error-message="validationErrors.street"
+                label="Endereço"
+                class="col-6"
+                outlined
+              />
+
+              <DefaultInput
+                v-model="form.address_number"
+                v-model:error="validationErrors.address_number"
+                :error-message="validationErrors.address_number"
+                label="Número"
+                class="col-6"
+                outlined
+              />
+
+              <DefaultInput
+                v-model="form.neighborhood"
+                v-model:error="validationErrors.neighborhood"
+                :error-message="validationErrors.neighborhood"
+                label="Bairro"
+                class="col-4"
+                outlined
+              />
+
+              <StateSelect
+                ref="stateSelectRef"
+                v-model="selectedState"
+                v-model:error="validationErrors.state_id"
+                :error-message="validationErrors.state_id"
+                label="Estado"
+                class="col-4"
+                outlined
+              />
+
+              <CitySelect
+                ref="citySelectRef"
+                v-model="selectedCity"
+                v-model:error="validationErrors.city_id"
+                :error-message="validationErrors.city_id"
+                label="Cidade"
+                class="col-4"
+                outlined
+                :state="selectedState"
+              />
+
+              <DefaultInput
+                v-model="form.complement"
+                v-model:error="validationErrors.complement"
+                :error-message="validationErrors.complement"
+                label="Complemento"
+                class="col-12"
+                outlined
+              />
+            </div>
+          </q-card-section>
+        </q-scroll-area>
 
         <q-card-actions>
           <q-space />
@@ -276,7 +319,7 @@ function onAvatarChange(file) {
   avatarFile.value = file;
 }
 
-const { loading, execute } = useSubmitHandler({
+const { loading, validationErrors, execute } = useSubmitHandler({
   formRef,
   onSuccess: (result) => onDialogOK(result),
 });

+ 56 - 33
src/pages/unit/components/AddMediaDialog.vue

@@ -1,42 +1,61 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <q-card class="q-dialog-plugin overflow-hidden" style="width: 480px; max-width: 95vw">
+    <q-card
+      class="q-dialog-plugin overflow-hidden"
+      style="width: 480px; max-width: 95vw"
+    >
       <DefaultDialogHeader title="Adicionar Mídia" @close="onDialogCancel" />
 
       <DefaultForm ref="formRef" @submit="onOKClick">
-        <q-card-section class="q-pt-none">
-          <div class="column q-gutter-sm">
-            <DefaultInput
-              v-model="form.title"
-              label="Título"
-              outlined
-              :rules="[inputRules.required]"
-            />
+        <q-scroll-area class="form-dialog-scroll-area">
+          <q-card-section class="q-pt-none">
+            <div class="column q-gutter-sm">
+              <DefaultInput
+                v-model="form.title"
+                v-model:error="validationErrors.title"
+                :error-message="validationErrors.title"
+                label="Título"
+                outlined
+                :rules="[inputRules.required]"
+              />
 
-            <q-file
-              v-model="selectedFile"
-              class="required-field"
-              label="Arquivo"
-              outlined
-              accept="image/*,video/*,.pdf"
-              :rules="[inputRules.required]"
-            >
-              <template #prepend>
-                <q-icon name="attach_file" />
-              </template>
-            </q-file>
+              <q-file
+                v-model="selectedFile"
+                :error="!!validationErrors.file"
+                :error-message="validationErrors.file"
+                class="required-field"
+                label="Arquivo"
+                outlined
+                accept="image/*,video/*,.pdf"
+                :rules="[inputRules.required]"
+              >
+                <template #prepend>
+                  <q-icon name="attach_file" />
+                </template>
+              </q-file>
 
-            <q-toggle
-              v-model="form.visible_to_franchisee"
-              label="Visível ao franqueado"
-              color="positive"
-            />
-          </div>
-        </q-card-section>
+              <q-toggle
+                v-model="form.visible_to_franchisee"
+                label="Visível ao franqueado"
+                color="positive"
+              />
+            </div>
+          </q-card-section>
+        </q-scroll-area>
 
         <q-card-actions align="right" class="q-pa-md">
-          <q-btn outline color="primary" label="Cancelar" @click="onDialogCancel" />
-          <q-btn color="primary-2" label="Adicionar" type="submit" :loading="loading" />
+          <q-btn
+            outline
+            color="primary"
+            label="Cancelar"
+            @click="onDialogCancel"
+          />
+          <q-btn
+            color="primary-2"
+            label="Adicionar"
+            type="submit"
+            :loading="loading"
+          />
         </q-card-actions>
       </DefaultForm>
     </q-card>
@@ -59,7 +78,8 @@ const { unitId } = defineProps({
 });
 
 const { inputRules } = useInputRules();
-const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
 
 const formRef = ref(null);
 const selectedFile = ref(null);
@@ -69,7 +89,7 @@ const form = ref({
   visible_to_franchisee: false,
 });
 
-const { loading, execute } = useSubmitHandler({
+const { loading, validationErrors, execute } = useSubmitHandler({
   formRef,
   onSuccess: (result) => onDialogOK(result),
 });
@@ -80,7 +100,10 @@ async function onOKClick() {
     formData.append("unit_id", unitId);
     formData.append("title", form.value.title);
     formData.append("file", selectedFile.value);
-    formData.append("visible_to_franchisee", form.value.visible_to_franchisee ? 1 : 0);
+    formData.append(
+      "visible_to_franchisee",
+      form.value.visible_to_franchisee ? 1 : 0,
+    );
     return createMedia(formData);
   });
 }

+ 36 - 2
src/pages/unit/tabs/UnitDataTab.vue

@@ -7,6 +7,8 @@
         <div class="row full-width q-mt-md q-col-gutter-sm">
           <DefaultInput
             v-model="form.name"
+            v-model:error="validationErrors.name"
+            :error-message="validationErrors.name"
             label="Nome da Unidade"
             class="col-12"
             outlined
@@ -15,6 +17,8 @@
 
           <DefaultInput
             v-model="form.social_reason"
+            v-model:error="validationErrors.social_reason"
+            :error-message="validationErrors.social_reason"
             label="Razão Social"
             class="col-12"
             outlined
@@ -23,6 +27,8 @@
 
           <DefaultInput
             v-model="form.fantasy_name"
+            v-model:error="validationErrors.fantasy_name"
+            :error-message="validationErrors.fantasy_name"
             label="Nome Fantasia"
             class="col-12"
             outlined
@@ -30,6 +36,8 @@
 
           <DefaultInput
             v-model="form.cnpj"
+            v-model:error="validationErrors.cnpj"
+            :error-message="validationErrors.cnpj"
             label="CNPJ"
             class="col-4"
             outlined
@@ -39,6 +47,8 @@
 
           <DefaultInput
             v-model="form.state_registration"
+            v-model:error="validationErrors.state_registration"
+            :error-message="validationErrors.state_registration"
             label="Inscrição Estadual"
             class="col-4"
             outlined
@@ -46,6 +56,8 @@
 
           <DefaultInput
             v-model="form.name_responsible"
+            v-model:error="validationErrors.name_responsible"
+            :error-message="validationErrors.name_responsible"
             label="Responsável"
             class="col-4"
             outlined
@@ -54,6 +66,8 @@
 
           <DefaultCepInput
             v-model="form.postal_code"
+            v-model:error="validationErrors.postal_code"
+            :error-message="validationErrors.postal_code"
             class="col-3"
             outlined
             :rules="[inputRules.required]"
@@ -65,6 +79,8 @@
 
           <DefaultInput
             v-model="form.street"
+            v-model:error="validationErrors.street"
+            :error-message="validationErrors.street"
             label="Endereço"
             class="col-6"
             outlined
@@ -73,6 +89,8 @@
 
           <DefaultInput
             v-model="form.address_number"
+            v-model:error="validationErrors.address_number"
+            :error-message="validationErrors.address_number"
             label="Número"
             class="col-3"
             outlined
@@ -80,6 +98,8 @@
 
           <DefaultInput
             v-model="form.neighborhood"
+            v-model:error="validationErrors.neighborhood"
+            :error-message="validationErrors.neighborhood"
             label="Bairro"
             class="col-4"
             outlined
@@ -89,6 +109,8 @@
           <StateSelect
             ref="stateSelectRef"
             v-model="selectedState"
+            v-model:error="validationErrors.state_id"
+            :error-message="validationErrors.state_id"
             label="Estado"
             class="col-4"
             outlined
@@ -98,6 +120,8 @@
           <CitySelect
             ref="citySelectRef"
             v-model="selectedCity"
+            v-model:error="validationErrors.city_id"
+            :error-message="validationErrors.city_id"
             label="Cidade"
             class="col-4"
             outlined
@@ -107,6 +131,8 @@
 
           <DefaultInput
             v-model="form.complement"
+            v-model:error="validationErrors.complement"
+            :error-message="validationErrors.complement"
             label="Complemento"
             class="col-12"
             outlined
@@ -114,6 +140,8 @@
 
           <DefaultInput
             v-model="form.email"
+            v-model:error="validationErrors.email"
+            :error-message="validationErrors.email"
             label="E-mail Principal"
             class="col-6"
             outlined
@@ -122,6 +150,8 @@
 
           <DefaultInput
             v-model="form.secondary_email"
+            v-model:error="validationErrors.secondary_email"
+            :error-message="validationErrors.secondary_email"
             label="E-mail Administrativo"
             class="col-6"
             outlined
@@ -130,19 +160,23 @@
 
           <DefaultInput
             v-model="form.phone_number"
+            v-model:error="validationErrors.phone_number"
+            :error-message="validationErrors.phone_number"
             label="Telefone"
             class="col-6"
             outlined
             :mask="masks.Brasil.telefone"
-            :rules="[inputRules.required]"
           />
 
           <DefaultInput
             v-model="form.cell_number"
+            v-model:error="validationErrors.cell_number"
+            :error-message="validationErrors.cell_number"
             label="Celular"
             class="col-6"
             outlined
             :mask="masks.Brasil.celular"
+            :rules="[inputRules.required]"
           />
         </div>
 
@@ -260,7 +294,7 @@ onMounted(async () => {
   }
 });
 
-const { loading, execute } = useSubmitHandler({
+const { loading, validationErrors, execute } = useSubmitHandler({
   formRef,
   onSuccess: (result) => {
     if (props.unitId) {

+ 18 - 11
src/pages/user_types/components/AddUserTypeDialog.vue

@@ -1,18 +1,25 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
     <q-card style="min-width: 400px">
-      <DefaultDialogHeader title="Novo Tipo de Usuário" @close="onDialogCancel" />
+      <DefaultDialogHeader
+        title="Novo Tipo de Usuário"
+        @close="onDialogCancel"
+      />
 
       <DefaultForm ref="formRef" @submit="onSave">
-        <q-card-section>
-          <DefaultInput
-            v-model="form.label"
-            label="Nome"
-            outlined
-            autofocus
-            :rules="[inputRules.required]"
-          />
-        </q-card-section>
+        <q-scroll-area class="form-dialog-scroll-area">
+          <q-card-section>
+            <DefaultInput
+              v-model="form.label"
+              v-model:error="validationErrors.label"
+              :error-message="validationErrors.label"
+              label="Nome"
+              outlined
+              autofocus
+              :rules="[inputRules.required]"
+            />
+          </q-card-section>
+        </q-scroll-area>
 
         <q-card-actions align="right">
           <q-btn
@@ -52,7 +59,7 @@ const formRef = useTemplateRef("formRef");
 
 const form = ref({ label: "" });
 
-const { loading, execute } = useSubmitHandler({
+const { loading, validationErrors, execute } = useSubmitHandler({
   formRef,
   onSuccess: () => onDialogOK(),
 });

+ 17 - 1
src/pages/users/UserActionPage.vue

@@ -28,6 +28,8 @@
             <StateSelect
               v-if="form.access_scope === 'unit'"
               v-model="form.state"
+              v-model:error="validationErrors.state_id"
+              :error-message="validationErrors.state_id"
               class="col-3"
               outlined
               label="Estado / UF"
@@ -37,6 +39,8 @@
             <UnitMultiSelect
               v-if="form.access_scope === 'unit'"
               v-model="form.units"
+              v-model:error="validationErrors.unit_ids"
+              :error-message="validationErrors.unit_ids"
               class="col-9"
               outlined
               label="Unidades"
@@ -46,6 +50,8 @@
             <UserTypeSelect
               :key="form.access_scope"
               v-model="form.user_type"
+              v-model:error="validationErrors.user_type"
+              :error-message="validationErrors.user_type"
               :access-scope="form.access_scope"
               :unit-id="selectedUnitId"
               class="col-6"
@@ -56,6 +62,8 @@
 
             <DefaultInput
               v-model="form.cpf"
+              v-model:error="validationErrors.cpf"
+              :error-message="validationErrors.cpf"
               label="CPF"
               class="col-6"
               outlined
@@ -65,6 +73,8 @@
 
             <DefaultInput
               v-model="form.name"
+              v-model:error="validationErrors.name"
+              :error-message="validationErrors.name"
               label="Nome"
               class="col-6"
               outlined
@@ -73,6 +83,8 @@
 
             <DefaultInput
               v-model="form.email"
+              v-model:error="validationErrors.email"
+              :error-message="validationErrors.email"
               label="E-mail"
               class="col-6"
               outlined
@@ -82,6 +94,8 @@
 
             <DefaultInput
               v-model="form.password"
+              v-model:error="validationErrors.password"
+              :error-message="validationErrors.password"
               label="Senha"
               class="col-6"
               outlined
@@ -104,6 +118,8 @@
 
             <DefaultInput
               v-model="form.password_confirmation"
+              v-model:error="validationErrors.password_confirmation"
+              :error-message="validationErrors.password_confirmation"
               label="Repetir Senha"
               class="col-6"
               outlined
@@ -213,7 +229,7 @@ const form = ref({
   password_confirmation: null,
 });
 
-const { loading, execute } = useSubmitHandler({
+const { loading, validationErrors, execute } = useSubmitHandler({
   formRef,
   onSuccess: () => {
     router.push({ name: "CadastrosPage" });