Explorar el Código

feat: :sparkles: crud provider blocked days

crud provider blocked days
Gustavo Zanatta hace 1 mes
padre
commit
75bd654f1e

+ 26 - 0
src/api/providerBlockedDay.js

@@ -0,0 +1,26 @@
+import api from './index'
+
+export const getProviderBlockedDays = async (providerId) => {
+    const response = await api.get(`/provider/blocked-days/${providerId}`)
+    return response.data.payload;
+}
+
+export const getProviderBlockedDay = async (id) => {
+    const response = await api.get(`/provider/blocked-day/${id}`)
+    return response.data.payload;
+}
+
+export const createProviderBlockedDay = async (data) => {
+    const response = await api.post('/provider/blocked-day', data);
+    return response.data.payload;
+}
+
+export const updateProviderBlockedDay = async (id, data) => {
+    const response = await api.put(`/provider/blocked-day/${id}`, data);
+    return response.data.payload;
+}
+
+export const deleteProviderBlockedDay = async (id) => {
+    const response = await api.delete(`/provider/blocked-day/${id}`);
+    return response.data.payload;
+}

+ 152 - 0
src/components/defaults/DefaultInputTimePicker.vue

@@ -0,0 +1,152 @@
+<template>
+  <div v-bind="$attrs">
+    <q-input
+      ref="inputRef"
+      v-model="treatedTime"
+      mask="##:##"
+      debounce="2000"
+      :label="label"
+      :rules="rules"
+      :dense="dense"
+      :error="error"
+      :error-message="errorMessage"
+      clearable
+    >
+      <template #append>
+        <q-icon name="mdi-clock-outline" class="cursor-pointer">
+          <q-popup-proxy cover transition-show="scale" transition-hide="scale">
+            <q-time
+              v-model="untreatedTime"
+              mask="HH:mm"
+              format24h
+              :options="timeOptionsFunc"
+            >
+              <div class="row items-center justify-end">
+                <q-btn v-close-popup label="Close" color="primary" flat />
+              </div>
+            </q-time>
+          </q-popup-proxy>
+        </q-icon>
+      </template>
+    </q-input>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, watch } from 'vue'
+import { useQuasar } from 'quasar'
+
+const props = defineProps({
+  untreatedTime: {
+    type: String,
+    default: null
+  },
+  label: {
+    type: String,
+    default: 'Hora'
+  },
+  rules: {
+    type: Array,
+    default: () => []
+  },
+  dense: {
+    type: Boolean,
+    default: false
+  },
+  error: {
+    type: Boolean,
+    default: false
+  },
+  errorMessage: {
+    type: String,
+    default: ''
+  },
+  limitarHoraAteAgora: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const emit = defineEmits(['update:untreatedTime'])
+
+const $q = useQuasar()
+const inputRef = ref(null)
+const untreatedTime = computed({
+  get: () => props.untreatedTime,
+  set: (value) => emit('update:untreatedTime', value)
+})
+
+const treatedTime = computed({
+  get: () => props.untreatedTime,
+  set: (value) => {
+    if (!value) {
+      emit('update:untreatedTime', null)
+      return
+    }
+
+    // Validate time format
+    if (!isValidTime(value)) {
+      $q.notify({
+        type: 'negative',
+        message: 'Hora inválida!'
+      })
+      return
+    }
+
+    // Check if time is not in the future (if limited)
+    if (props.limitarHoraAteAgora && !isTimeBeforeNow(value)) {
+      $q.notify({
+        type: 'negative',
+        message: 'Hora não pode ser posterior a agora!'
+      })
+      emit('update:untreatedTime', null)
+      return
+    }
+
+    emit('update:untreatedTime', value)
+  }
+})
+
+const isValidTime = (time) => {
+  if (!time) return false
+  const regex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/
+  return regex.test(time)
+}
+
+const isTimeBeforeNow = (time) => {
+  const [hours, minutes] = time.split(':').map(Number)
+  const now = new Date()
+  const currentHours = now.getHours()
+  const currentMinutes = now.getMinutes()
+
+  if (hours > currentHours) return false
+  if (hours === currentHours && minutes > currentMinutes) return false
+
+  return true
+}
+
+const timeOptionsFunc = (hr, min) => {
+  if (!props.limitarHoraAteAgora) return true
+
+  const now = new Date()
+  const currentHours = now.getHours()
+  const currentMinutes = now.getMinutes()
+
+  if (hr > currentHours) return false
+  if (min && hr === currentHours && min > currentMinutes) return false
+
+  return true
+}
+
+watch(
+  () => props.untreatedTime,
+  () => {
+    inputRef.value?.resetValidation()
+  }
+)
+
+defineExpose({
+  resetValidation: () => inputRef.value?.resetValidation(),
+  validate: () => inputRef.value?.validate()
+})
+</script>

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

@@ -367,6 +367,24 @@
       "7": "Saturday"
     }
   },
+  "provider_blocked_days": {
+    "singular": "Blocked Day",
+    "plural": "Blocked Days",
+    "header": "Blocked Days",
+    "add_button": "Add Blocked Day",
+    "edit_button": "Edit Blocked Day",
+    "empty_state": "No blocked days registered",
+    "date": "Date",
+    "period": "Period",
+    "reason": "Reason",
+    "init_hour": "Start Time",
+    "end_hour": "End Time",
+    "periods": {
+      "morning": "Morning",
+      "afternoon": "Afternoon",
+      "all": "All Day"
+    }
+  },
   "orders": {
     "singular": "Order",
     "plural": "Orders",

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

@@ -367,6 +367,24 @@
       "7": "Sábado"
     }
   },
+  "provider_blocked_days": {
+    "singular": "Día Bloqueado",
+    "plural": "Días Bloqueados",
+    "header": "Días Bloqueados",
+    "add_button": "Agregar Día Bloqueado",
+    "edit_button": "Editar Día Bloqueado",
+    "empty_state": "No hay días bloqueados registrados",
+    "date": "Fecha",
+    "period": "Período",
+    "reason": "Motivo",
+    "init_hour": "Hora Inicio",
+    "end_hour": "Hora Fin",
+    "periods": {
+      "morning": "Mañana",
+      "afternoon": "Tarde",
+      "all": "Día Completo"
+    }
+  },
   "orders": {
     "singular": "Pedido",
     "plural": "Pedidos",

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

@@ -367,6 +367,24 @@
       "7": "Sábado"
     }
   },
+  "provider_blocked_days": {
+    "singular": "Dia Bloqueado",
+    "plural": "Dias Bloqueados",
+    "header": "Dias Bloqueados",
+    "add_button": "Adicionar Dia Bloqueado",
+    "edit_button": "Editar Dia Bloqueado",
+    "empty_state": "Nenhum dia bloqueado cadastrado",
+    "date": "Data",
+    "period": "Período",
+    "reason": "Motivo",
+    "init_hour": "Hora Início",
+    "end_hour": "Hora Fim",
+    "periods": {
+      "morning": "Manhã",
+      "afternoon": "Tarde",
+      "all": "Dia Todo"
+    }
+  },
   "orders": {
     "singular": "Pedido",
     "plural": "Pedidos",

+ 207 - 0
src/pages/provider/components/AddEditProviderBlockedDayDialog.vue

@@ -0,0 +1,207 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="q-dialog-plugin" style="width: 700px; max-width: 90vw">
+      <DefaultDialogHeader :title="title" @close="onDialogCancel" />
+      <q-form ref="formRef" @submit="onOKClick">
+        <q-card-section class="row q-col-gutter-sm">
+          <DefaultInputDatePicker
+            v-model:untreated-date="form.date"
+            :label="$t('provider_blocked_days.date')"
+            :rules="[inputRules.required]"
+            :error="serverErrors?.date"
+            :error-message="serverErrors?.date"
+            :max-date="null"
+            class="col-6"
+          />
+
+          <q-select
+            v-model="form.period"
+            :options="periodOptions"
+            :label="$t('provider_blocked_days.period')"
+            :rules="[inputRules.required]"
+            :error="!!serverErrors?.period"
+            :error-message="serverErrors?.period"
+            emit-value
+            map-options
+            class="col-6"
+            @update:model-value="onPeriodChange"
+          />
+
+          <DefaultInputTimePicker
+            v-model:untreated-time="form.init_hour"
+            :label="$t('provider_blocked_days.init_hour')"
+            :rules="[inputRules.required]"
+            :error="!!serverErrors?.init_hour"
+            :error-message="serverErrors?.init_hour"
+            :disable="form.period === 'all'"
+            class="col-md-6 col-12"
+          />
+
+          <DefaultInputTimePicker
+            v-model:untreated-time="form.end_hour"
+            :label="$t('provider_blocked_days.end_hour')"
+            :rules="[inputRules.required]"
+            :error="!!serverErrors?.end_hour"
+            :error-message="serverErrors?.end_hour"
+            :disable="form.period === 'all'"
+            class="col-md-6 col-12"
+          />
+
+          <q-input
+            v-model="form.reason"
+            :label="$t('provider_blocked_days.reason')"
+            :error="!!serverErrors?.reason"
+            :error-message="serverErrors?.reason"
+            type="textarea"
+            class="col-12"
+          />
+        </q-card-section>
+
+        <q-card-actions align="right">
+          <q-btn
+            flat
+            :label="$t('common.actions.cancel')"
+            color="negative"
+            @click="onDialogCancel"
+          />
+          <q-btn
+            type="submit"
+            :label="$t('common.actions.save')"
+            :loading="loading"
+            :disable="!hasUpdatedFields"
+            color="primary"
+          />
+        </q-card-actions>
+      </q-form>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue'
+import { useDialogPluginComponent } from 'quasar'
+import { useI18n } from 'vue-i18n'
+// import { useFormUpdateTracker } from 'src/composables/useFormUpdateTracker'
+import { useSubmitHandler } from 'src/composables/useSubmitHandler'
+import { useFormUpdateTracker } from "src/composables/useFormUpdateTracker";
+import {
+  createProviderBlockedDay,
+  updateProviderBlockedDay
+} from 'src/api/providerBlockedDay'
+import DefaultDialogHeader from 'src/components/defaults/DefaultDialogHeader.vue'
+import DefaultInputDatePicker from 'src/components/defaults/DefaultInputDatePicker.vue'
+import DefaultInputTimePicker from 'src/components/defaults/DefaultInputTimePicker.vue'
+import { useInputRules } from "src/composables/useInputRules";
+
+const props = defineProps({
+  blockedDay: {
+    type: Object,
+    default: null
+  },
+  providerId: {
+    type: Number,
+    required: true
+  },
+  title: {
+    type: Function,
+    default: () => ''
+  }
+})
+
+defineEmits([...useDialogPluginComponent.emits])
+const { t } = useI18n()
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
+const { inputRules } = useInputRules();
+const formRef = ref(null)
+// const loading = ref(false)
+// const serverErrors = ref({})
+
+// const form = ref({
+//   provider_id: props.providerId,
+//   date: null,
+//   period: null,
+//   reason: null,
+//   init_hour: null,
+//   end_hour: null
+// })
+// const { form, getUpdatedFields, hasUpdatedFields } = useFormUpdateTracker({
+//   user_id: provider ? provider?.user_id : null,
+//   document: provider ? provider?.document : "",
+//   rg: provider ? provider?.rg : "",
+//   birth_date: provider ? provider?.birth_date : null,
+//   is_approved: provider ? provider?.is_approved : false,
+//   daily_price_8h: provider ? Number(provider?.daily_price_8h) : null,
+//   daily_price_6h: provider ? Number(provider?.daily_price_6h) : null,
+//   daily_price_4h: provider ? Number(provider?.daily_price_4h) : null,
+//   daily_price_2h: provider ? Number(provider?.daily_price_2h) : null,
+// });
+const { form, getUpdatedFields, hasUpdatedFields } = useFormUpdateTracker({
+  provider_id: props.blockedDay ? props.blockedDay.provider_id : null,
+  date: props.blockedDay ? props.blockedDay.date : null,
+  period: props.blockedDay ? props.blockedDay.period : null,
+  reason: props.blockedDay ? props.blockedDay.reason : null,
+  init_hour: props.blockedDay ? props.blockedDay.init_hour : null,
+  end_hour: props.blockedDay ? props.blockedDay.end_hour : null
+})
+
+const periodOptions = computed(() => [
+  {
+    label: t('provider_blocked_days.periods.morning'),
+    value: 'morning'
+  },
+  {
+    label: t('provider_blocked_days.periods.afternoon'),
+    value: 'afternoon'
+  },
+  {
+    label: t('provider_blocked_days.periods.all'),
+    value: 'all'
+  }
+])
+
+const onPeriodChange = () => {
+  if (form.period === 'all') {
+    form.init_hour = '00:00'
+    form.end_hour = '23:59'
+  }
+}
+
+const {
+  loading,
+  serverErrors,
+  execute: submitForm,
+} = useSubmitHandler({
+  onSuccess: () => onDialogOK(true),
+  formRef: formRef,
+});
+
+const onOKClick = async () => {
+
+    if(props.blockedDay) {
+        await submitForm(() => updateProviderBlockedDay(props.blockedDay.id, getUpdatedFields.value));
+    } else {
+        await submitForm(() => createProviderBlockedDay({ ...form }));
+    }
+//   const submitFunction = props.blockedDay
+//     ? () => updateProviderBlockedDay(props.blockedDay.id, submitData)
+//     : () => createProviderBlockedDay(submitData)
+
+//   const result = await useSubmitHandler(
+//     submitFunction,
+//     loading,
+//     serverErrors,
+//     hasChanged,
+//     props.blockedDay ? 'edit' : 'create'
+//   )
+
+//   if (result.success) {
+//     onDialogOK(true)
+//   }
+}
+
+onMounted(() => {
+  if(props.providerId) {
+    form.provider_id = props.providerId
+  }
+})
+</script>

+ 6 - 0
src/pages/provider/components/AddEditProviderDialog.vue

@@ -8,6 +8,7 @@
         <q-tab v-if="provider" name="addresses" :label="$t('address.tab')" />
         <q-tab v-if="provider" name="payment_methods" :label="$t('provider_payment_methods.header')" />
         <q-tab v-if="provider" name="working_days" :label="$t('provider_working_days.header')" />
+        <q-tab v-if="provider" name="blocked_days" :label="$t('provider_blocked_days.header')" />
       </q-tabs>
 
       <q-separator v-if="provider" />
@@ -165,6 +166,10 @@
           <q-tab-panel v-if="provider" name="working_days">
             <ProviderWorkingDaysPanel :provider-id="provider.id" />
           </q-tab-panel>
+
+          <q-tab-panel v-if="provider" name="blocked_days">
+            <ProviderBlockedDaysPanel :provider-id="provider.id" />
+          </q-tab-panel>
         </q-tab-panels>
       </div>
       <q-card-actions align="center" class="">
@@ -193,6 +198,7 @@ import ProviderSpecialitiesPanel from "./ProviderSpecialitiesPanel.vue";
 import ProviderServicesTypesPanel from "./ProviderServicesTypesPanel.vue";
 import ProviderPaymentMethodsPanel from "./ProviderPaymentMethodsPanel.vue";
 import ProviderWorkingDaysPanel from "./ProviderWorkingDaysPanel.vue";
+import ProviderBlockedDaysPanel from "./ProviderBlockedDaysPanel.vue";
 
 defineEmits([
   ...useDialogPluginComponent.emits,

+ 137 - 0
src/pages/provider/components/ProviderBlockedDaysPanel.vue

@@ -0,0 +1,137 @@
+<template>
+  <div class="q-pa-md">
+    <div>
+      <DefaultTable
+        ref="tableRef"
+        :columns="columns"
+        :api-call="() => getProviderBlockedDays(props.providerId)"
+        :delete-function="deleteProviderBlockedDay"
+        :mostrar-selecao-de-colunas="false"
+        :mostrar-botao-fullscreen="false"
+        :mostrar-toggle-inativos="false"
+        open-item
+        add-item
+        @on-row-click="onRowClick"
+        @on-add-item="onAddItem"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, defineAsyncComponent } from 'vue'
+import { useQuasar } from 'quasar'
+import { useI18n } from 'vue-i18n'
+import { getProviderBlockedDays, deleteProviderBlockedDay } from 'src/api/providerBlockedDay'
+import { permissionStore } from 'src/stores/permission'
+import DefaultTable from 'src/components/defaults/DefaultTable.vue'
+import { formatDateYMDtoDMY } from 'src/helpers/utils'
+
+const AddEditProviderBlockedDayDialog = defineAsyncComponent(
+  () => import('./AddEditProviderBlockedDayDialog.vue')
+)
+
+const props = defineProps({
+  providerId: {
+    type: Number,
+    required: true
+  }
+})
+
+const { t } = useI18n()
+const $q = useQuasar()
+const permission_store = permissionStore()
+
+const tableRef = ref(null)
+
+const columns = computed(() => [
+  {
+    name: 'date',
+    label: t('provider_blocked_days.date'),
+    field: 'date',
+    format: (val) => val ? formatDateYMDtoDMY(val) : '-',
+    align: 'left',
+    sortable: true
+  },
+  {
+    name: 'period',
+    label: t('provider_blocked_days.period'),
+    field: 'period',
+    align: 'left',
+    sortable: true,
+    format: (val) => t(`provider_blocked_days.periods.${val}`)
+  },
+  {
+    name: 'init_hour',
+    label: t('provider_blocked_days.init_hour'),
+    field: 'init_hour',
+    align: 'left',
+    sortable: true
+  },
+  {
+    name: 'end_hour',
+    label: t('provider_blocked_days.end_hour'),
+    field: 'end_hour',
+    align: 'left',
+    sortable: true
+  },
+  {
+    name: 'reason',
+    label: t('provider_blocked_days.reason'),
+    field: 'reason',
+    align: 'left',
+    sortable: true
+  },
+  {
+    name: 'actions',
+    label: t('common.terms.actions'),
+    align: 'center',
+    required: true
+  }
+])
+
+const onRowClick = ({ row }) => {
+  if (permission_store.getAccess('config.provider_blocked_day', 'edit') === false) {
+    $q.loading.hide()
+    $q.notify({
+      type: 'negative',
+      message: t('validation.permissions.edit')
+    })
+    return
+  }
+  $q.dialog({
+    component: AddEditProviderBlockedDayDialog,
+    componentProps: {
+      blockedDay: row,
+      providerId: props.providerId,
+      title: () => useI18n().t('provider_blocked_days.edit_button')
+    }
+  }).onOk(async (success) => {
+    if (success) {
+      tableRef.value.refresh()
+    }
+  })
+}
+
+const onAddItem = () => {
+  if (permission_store.getAccess('config.provider_blocked_day', 'add') === false) {
+    $q.loading.hide()
+    $q.notify({
+      type: 'negative',
+      message: t('validation.permissions.add')
+    })
+    return
+  }
+  $q.dialog({
+    component: AddEditProviderBlockedDayDialog,
+    componentProps: {
+      title: () => useI18n().t('provider_blocked_days.add_button'),
+      providerId: props.providerId
+    }
+  }).onOk(async (success) => {
+    if (success) {
+      tableRef.value.refresh()
+    }
+  })
+}
+</script>