Browse Source

feat: :sparkles: crud provider specialities

crud provider specialities
Gustavo Zanatta 1 tháng trước cách đây
mục cha
commit
1d69547b20

+ 16 - 0
src/api/providerSpeciality.js

@@ -0,0 +1,16 @@
+import api from "src/api";
+
+export const getProviderSpecialities = async (providerId) => {
+  const response = await api.get(`/provider/specialities/${providerId}`)
+  return response.data
+}
+
+export const createProviderSpeciality = async (providerId, data) => {
+  const response = await api.post(`/provider/specialities/${providerId}`, data)
+  return response.data
+}
+
+export const deleteProviderSpeciality = async (providerId, id) => {
+  const response = await api.delete(`/provider/specialities/${providerId}/${id}`)
+  return response.data
+}

+ 14 - 4
src/components/speciality/SpecialitySelect.vue

@@ -27,11 +27,11 @@
 
 <script setup>
 import { getSpecialities } from "src/api/speciality";
-import { ref, onMounted } from "vue";
+import { ref, onMounted, computed } from "vue";
 import { normalizeString } from "src/helpers/utils";
 import { useI18n } from "vue-i18n";
 
-const { label, rules, initialId } = defineProps({
+const { label, rules, initialId, excludeIds } = defineProps({
   label: {
     type: String,
     default: () => useI18n().t("ui.navigation.speciality"),
@@ -53,17 +53,27 @@ const { label, rules, initialId } = defineProps({
     type: String,
     default: "",
   },
+  excludeIds: {
+    type: Array,
+    default: () => [],
+  },
 });
 
 const selectedSpeciality = defineModel({ type: Object });
 
 const loading = ref(false);
 const baseOptions = ref([]);
+const filteredOptions = computed(() => {
+  if (!excludeIds || excludeIds.length === 0) {
+    return baseOptions.value;
+  }
+  return baseOptions.value.filter(option => !excludeIds.includes(option.value));
+});
 const specialityOptions = ref([]);
 
 const filterFn = async (val, update) => {
   const needle = normalizeString(val);
-  specialityOptions.value = baseOptions.value.filter((v) => {
+  specialityOptions.value = filteredOptions.value.filter((v) => {
     return normalizeString(v.label).includes(needle);
   });
   update();
@@ -93,7 +103,7 @@ onMounted(async () => {
       label: speciality.description,
       value: speciality.id,
     }));
-    specialityOptions.value = baseOptions.value;
+    specialityOptions.value = filteredOptions.value;
     if (initialId) {
       selectSpecialityById(initialId);
     }

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

@@ -317,6 +317,13 @@
       "daily_price": "Value between R$ 100.00 and R$ 500.00"
     }
   },
+  "provider_specialities": {
+    "header": "Specialties",
+    "add_button": "Add",
+    "select_placeholder": "Select a specialty",
+    "empty_state": "No specialties added",
+    "remove_confirm": "Are you sure you want to remove this specialty?"
+  },
   "provider_services_types": {
     "header": "Service Types",
     "add_button": "Add",

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

@@ -317,6 +317,13 @@
       "daily_price": "Valor entre R$ 100,00 y R$ 500,00"
     }
   },
+  "provider_specialities": {
+    "header": "Especialidades",
+    "add_button": "Agregar",
+    "select_placeholder": "Seleccione una especialidad",
+    "empty_state": "No se han agregado especialidades",
+    "remove_confirm": "¿Está seguro de que desea eliminar esta especialidad?"
+  },
   "provider_services_types": {
     "header": "Tipos de Servicios",
     "add_button": "Agregar",

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

@@ -317,6 +317,13 @@
       "daily_price": "Valor entre R$ 100,00 e R$ 500,00"
     }
   },
+  "provider_specialities": {
+    "header": "Especialidades",
+    "add_button": "Adicionar",
+    "select_placeholder": "Selecione uma especialidade",
+    "empty_state": "Nenhuma especialidade adicionada",
+    "remove_confirm": "Tem certeza que deseja remover esta especialidade?"
+  },
   "provider_services_types": {
     "header": "Tipos de Serviços",
     "add_button": "Adicionar",

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

@@ -97,6 +97,8 @@
                 />
               </div>
 
+              <ProviderSpecialitiesPanel v-if="provider" :provider-id="provider.id" />
+
               <ProviderServicesTypesPanel v-if="provider" :provider-id="provider.id" />
 
               <div class="col-12 q-mt-md">
@@ -177,6 +179,7 @@ import UserSelect from "src/components/user/UserSelect.vue";
 import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
 import DefaultCurrencyInput from "src/components/defaults/DefaultCurrencyInput.vue";
 import AddressesPanel from "src/pages/address/components/AddressesPanel.vue";
+import ProviderSpecialitiesPanel from "./ProviderSpecialitiesPanel.vue";
 import ProviderServicesTypesPanel from "./ProviderServicesTypesPanel.vue";
 
 defineEmits([

+ 164 - 0
src/pages/provider/components/ProviderSpecialitiesPanel.vue

@@ -0,0 +1,164 @@
+<template>
+  <div class="col-12">
+    <div class="text-h6 q-mb-md">{{ $t('provider_specialities.header') }}</div>
+
+    <div class="row items-center">
+      <SpecialitySelect
+        v-model="selectedSpeciality"
+        :label="$t('provider_specialities.select_placeholder')"
+        :disable="!providerId || loading"
+        :exclude-ids="excludedSpecialityIds"
+        class="col-md-6 col-12 q-pb-none q-mr-sm"
+      />
+
+      <div class="col-auto">
+        <q-btn
+          color="primary"
+          :label="$t('provider_specialities.add_button')"
+          :disable="!selectedSpeciality || loading"
+          :loading="adding"
+          padding="16px 16px"
+          @click="handleAdd"
+        />
+      </div>
+    </div>
+
+    <div v-if="loading" class="q-mt-md text-center">
+      <q-spinner color="primary" size="40px" />
+    </div>
+
+    <div v-else-if="providerSpecialities.length > 0" class="q-mt-md q-gutter-sm">
+      <q-chip
+        v-for="item in providerSpecialities"
+        :key="item.id"
+        removable
+        color="primary"
+        text-color="white"
+        @remove="handleRemove(item)"
+      >
+        {{ item.speciality?.description || '-' }}
+      </q-chip>
+    </div>
+
+    <div v-else class="q-mt-md text-grey-7 text-center">
+      {{ $t('provider_specialities.empty_state') }}
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, watch, onMounted } from 'vue'
+import { useQuasar } from 'quasar'
+import { useI18n } from 'vue-i18n'
+import SpecialitySelect from 'src/components/speciality/SpecialitySelect.vue'
+import {
+  getProviderSpecialities,
+  createProviderSpeciality,
+  deleteProviderSpeciality
+} from 'src/api/providerSpeciality.js'
+
+const props = defineProps({
+  providerId: {
+    type: Number,
+    default: null
+  }
+})
+
+const $q = useQuasar()
+const { t } = useI18n()
+
+const providerSpecialities = ref([])
+const selectedSpeciality = ref(null)
+const loading = ref(false)
+const adding = ref(false)
+
+const excludedSpecialityIds = computed(() => {
+  return providerSpecialities.value.map(item => item.speciality_id)
+})
+
+const loadProviderSpecialities = async () => {
+  if (!props.providerId) {
+    providerSpecialities.value = []
+    return
+  }
+
+  try {
+    loading.value = true
+    const response = await getProviderSpecialities(props.providerId)
+    providerSpecialities.value = response.payload || []
+  } catch (error) {
+    console.error('Error loading provider specialities:', error)
+    $q.notify({
+      type: 'negative',
+      message: error.response?.data?.message || t('http.errors.failed')
+    })
+  } finally {
+    loading.value = false
+  }
+}
+
+const handleAdd = async () => {
+  if (!selectedSpeciality.value) return
+
+  try {
+    adding.value = true
+    await createProviderSpeciality(props.providerId, {
+      speciality_id: selectedSpeciality.value.value
+    })
+
+    $q.notify({
+      type: 'positive',
+      message: t('http.success')
+    })
+
+    selectedSpeciality.value = null
+    await loadProviderSpecialities()
+  } catch (error) {
+    console.error('Error adding speciality:', error)
+    $q.notify({
+      type: 'negative',
+      message: error.response?.data?.message || t('http.errors.failed')
+    })
+  } finally {
+    adding.value = false
+  }
+}
+
+const handleRemove = async (item) => {
+  $q.dialog({
+    title: t('common.messages.confirm'),
+    message: t('provider_specialities.remove_confirm'),
+    cancel: true,
+    persistent: true
+  }).onOk(async () => {
+    try {
+      await deleteProviderSpeciality(props.providerId, item.id)
+
+      $q.notify({
+        type: 'positive',
+        message: t('common.messages.deleted_successfully')
+      })
+
+      await loadProviderSpecialities()
+    } catch (error) {
+      console.error('Error removing speciality:', error)
+      $q.notify({
+        type: 'negative',
+        message: error.response?.data?.message || t('common.messages.error_deleting')
+      })
+    }
+  })
+}
+
+watch(() => props.providerId, () => {
+  loadProviderSpecialities()
+}, { immediate: true })
+
+onMounted(() => {
+  loadProviderSpecialities()
+})
+
+defineExpose({
+  loadProviderSpecialities
+})
+</script>