|
|
@@ -0,0 +1,317 @@
|
|
|
+<template>
|
|
|
+ <q-dialog ref="dialogRef" persistent @hide="onDialogHide">
|
|
|
+ <q-card class="complete-profile-card">
|
|
|
+
|
|
|
+ <q-card-section class="complete-profile-header">
|
|
|
+ <div class="text-h6 text-white text-weight-bold">
|
|
|
+ {{ $t("associado.complete_profile.title") }}
|
|
|
+ </div>
|
|
|
+ <div class="text-caption text-white q-mt-xs" style="opacity: 0.85;">
|
|
|
+ {{ $t("associado.complete_profile.message") }}
|
|
|
+ </div>
|
|
|
+ </q-card-section>
|
|
|
+
|
|
|
+ <q-card-section class="q-pt-md">
|
|
|
+
|
|
|
+ <div class="flex flex-center q-mb-md">
|
|
|
+ <div class="avatar-wrap">
|
|
|
+ <q-avatar size="80px" class="complete-profile-avatar">
|
|
|
+ <img
|
|
|
+ v-if="localPhotoUrl"
|
|
|
+ :src="localPhotoUrl"
|
|
|
+ style="object-fit: cover; width: 100%; height: 100%;"
|
|
|
+ />
|
|
|
+ <q-icon v-else name="mdi-account" size="48px" color="white" />
|
|
|
+ </q-avatar>
|
|
|
+ <q-btn
|
|
|
+ round
|
|
|
+ unelevated
|
|
|
+ size="xs"
|
|
|
+ icon="mdi-pencil-outline"
|
|
|
+ class="avatar-edit-btn"
|
|
|
+ color="white"
|
|
|
+ text-color="violet-normal"
|
|
|
+ :loading="uploadingPhoto"
|
|
|
+ @click="avatarInputRef.click()"
|
|
|
+ />
|
|
|
+ <input
|
|
|
+ ref="avatarInputRef"
|
|
|
+ type="file"
|
|
|
+ accept="image/*"
|
|
|
+ style="display: none"
|
|
|
+ @change="onAvatarSelected"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="!localPhotoUrl" class="text-center text-caption text-negative q-mb-sm">
|
|
|
+ {{ $t("associado.complete_profile.photo_hint") }}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="q-col-gutter-sm row">
|
|
|
+
|
|
|
+ <div class="col-12">
|
|
|
+ <q-input
|
|
|
+ v-model="form.name"
|
|
|
+ outlined
|
|
|
+ dense
|
|
|
+ :label="$t('common.terms.name')"
|
|
|
+ :readonly="!!initialUser.name"
|
|
|
+ :bg-color="initialUser.name ? 'grey-2' : 'white'"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="col-12 col-sm-6">
|
|
|
+ <q-input
|
|
|
+ v-model="form.cpf"
|
|
|
+ outlined
|
|
|
+ dense
|
|
|
+ label="CPF"
|
|
|
+ :readonly="!!initialUser.cpf"
|
|
|
+ :bg-color="initialUser.cpf ? 'grey-2' : 'white'"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="col-12 col-sm-6">
|
|
|
+ <q-input
|
|
|
+ v-model="form.email"
|
|
|
+ outlined
|
|
|
+ dense
|
|
|
+ label="E-mail"
|
|
|
+ :readonly="!!initialUser.email"
|
|
|
+ :bg-color="initialUser.email ? 'grey-2' : 'white'"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="col-12 col-sm-6">
|
|
|
+ <q-select
|
|
|
+ v-model="form.position_id"
|
|
|
+ outlined
|
|
|
+ dense
|
|
|
+ :label="$t('associado.position')"
|
|
|
+ :options="positionOptions"
|
|
|
+ option-value="id"
|
|
|
+ option-label="name"
|
|
|
+ emit-value
|
|
|
+ map-options
|
|
|
+ :readonly="!!initialUser.position_id"
|
|
|
+ :bg-color="initialUser.position_id ? 'grey-2' : 'white'"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="col-12 col-sm-6">
|
|
|
+ <q-select
|
|
|
+ v-model="form.sector_id"
|
|
|
+ outlined
|
|
|
+ dense
|
|
|
+ :label="$t('associado.sector')"
|
|
|
+ :options="sectorOptions"
|
|
|
+ option-value="id"
|
|
|
+ option-label="name"
|
|
|
+ emit-value
|
|
|
+ map-options
|
|
|
+ :readonly="!!initialUser.sector_id"
|
|
|
+ :bg-color="initialUser.sector_id ? 'grey-2' : 'white'"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="col-12 col-sm-6">
|
|
|
+ <q-input
|
|
|
+ v-model="form.registration"
|
|
|
+ outlined
|
|
|
+ dense
|
|
|
+ :label="$t('associado.registration')"
|
|
|
+ readonly
|
|
|
+ bg-color="grey-2"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="col-12 col-sm-6">
|
|
|
+ <q-input
|
|
|
+ v-model="form.admission_date"
|
|
|
+ outlined
|
|
|
+ dense
|
|
|
+ :label="$t('associado.admission_date')"
|
|
|
+ readonly
|
|
|
+ bg-color="grey-2"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ </div>
|
|
|
+
|
|
|
+ </q-card-section>
|
|
|
+
|
|
|
+ <q-card-actions class="q-px-md q-pb-md">
|
|
|
+ <q-btn
|
|
|
+ unelevated
|
|
|
+ no-caps
|
|
|
+ color="violet-normal"
|
|
|
+ class="full-width"
|
|
|
+ :label="$t('associado.complete_profile.save_continue')"
|
|
|
+ :loading="saving"
|
|
|
+ :disable="!canSave"
|
|
|
+ @click="onSave"
|
|
|
+ />
|
|
|
+ </q-card-actions>
|
|
|
+
|
|
|
+ </q-card>
|
|
|
+ </q-dialog>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, computed, onMounted, useTemplateRef } from "vue";
|
|
|
+import { useQuasar, useDialogPluginComponent } from "quasar";
|
|
|
+import { useI18n } from "vue-i18n";
|
|
|
+import { userStore } from "src/stores/user";
|
|
|
+import { getPositions } from "src/api/position";
|
|
|
+import { getSectors } from "src/api/sector";
|
|
|
+import { uploadMyAvatar, updateMyProfile } from "src/api/user";
|
|
|
+
|
|
|
+defineEmits([...useDialogPluginComponent.emits]);
|
|
|
+
|
|
|
+const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
|
|
+const $q = useQuasar();
|
|
|
+const { t } = useI18n();
|
|
|
+const store = userStore();
|
|
|
+
|
|
|
+const avatarInputRef = useTemplateRef("avatarInputRef");
|
|
|
+
|
|
|
+const initialUser = ref({});
|
|
|
+const localPhotoUrl = ref(null);
|
|
|
+const uploadingPhoto = ref(false);
|
|
|
+const saving = ref(false);
|
|
|
+const positionOptions = ref([]);
|
|
|
+const sectorOptions = ref([]);
|
|
|
+
|
|
|
+const form = ref({
|
|
|
+ name: "",
|
|
|
+ cpf: "",
|
|
|
+ email: "",
|
|
|
+ position_id: null,
|
|
|
+ sector_id: null,
|
|
|
+ registration: "",
|
|
|
+ admission_date: "",
|
|
|
+});
|
|
|
+
|
|
|
+const canSave = computed(() => {
|
|
|
+ return (
|
|
|
+ !!localPhotoUrl.value &&
|
|
|
+ !!form.value.name?.trim() &&
|
|
|
+ !!form.value.cpf?.trim() &&
|
|
|
+ !!form.value.email?.trim() &&
|
|
|
+ !!form.value.position_id &&
|
|
|
+ !!form.value.sector_id
|
|
|
+ );
|
|
|
+});
|
|
|
+
|
|
|
+const onAvatarSelected = async (event) => {
|
|
|
+ const file = event.target.files?.[0];
|
|
|
+ if (!file) return;
|
|
|
+ uploadingPhoto.value = true;
|
|
|
+ try {
|
|
|
+ const updated = await uploadMyAvatar(file);
|
|
|
+ store.user = updated;
|
|
|
+ localPhotoUrl.value = updated.photo_url;
|
|
|
+ $q.notify({ type: "positive", message: t("http.success") });
|
|
|
+ } catch {
|
|
|
+ $q.notify({ type: "negative", message: t("http.errors.failed") });
|
|
|
+ } finally {
|
|
|
+ uploadingPhoto.value = false;
|
|
|
+ event.target.value = "";
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const onSave = async () => {
|
|
|
+ saving.value = true;
|
|
|
+ try {
|
|
|
+ const payload = {};
|
|
|
+ if (!initialUser.value.name) payload.name = form.value.name;
|
|
|
+ if (!initialUser.value.cpf) payload.cpf = form.value.cpf;
|
|
|
+ if (!initialUser.value.email) payload.email = form.value.email;
|
|
|
+ if (!initialUser.value.position_id) payload.position_id = form.value.position_id;
|
|
|
+ if (!initialUser.value.sector_id) payload.sector_id = form.value.sector_id;
|
|
|
+
|
|
|
+ if (Object.keys(payload).length > 0) {
|
|
|
+ const updated = await updateMyProfile(payload);
|
|
|
+ store.user = updated;
|
|
|
+ }
|
|
|
+
|
|
|
+ onDialogOK();
|
|
|
+ } catch {
|
|
|
+ $q.notify({ type: "negative", message: t("http.errors.failed") });
|
|
|
+ } finally {
|
|
|
+ saving.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ const user = store.user ?? {};
|
|
|
+ initialUser.value = {
|
|
|
+ name: user.name || null,
|
|
|
+ cpf: user.cpf || null,
|
|
|
+ email: user.email || null,
|
|
|
+ position_id: user.position_id ?? user.position?.id ?? null,
|
|
|
+ sector_id: user.sector_id ?? user.sector?.id ?? null,
|
|
|
+ registration: user.registration || null,
|
|
|
+ admission_date: user.admission_date || null,
|
|
|
+ };
|
|
|
+
|
|
|
+ form.value = {
|
|
|
+ name: initialUser.value.name || "",
|
|
|
+ cpf: initialUser.value.cpf || "",
|
|
|
+ email: initialUser.value.email || "",
|
|
|
+ position_id: initialUser.value.position_id || null,
|
|
|
+ sector_id: initialUser.value.sector_id || null,
|
|
|
+ registration: initialUser.value.registration || "",
|
|
|
+ admission_date: initialUser.value.admission_date || "",
|
|
|
+ };
|
|
|
+
|
|
|
+ localPhotoUrl.value = user.photo_url || null;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const [positions, sectors] = await Promise.all([getPositions(), getSectors()]);
|
|
|
+ positionOptions.value = positions ?? [];
|
|
|
+ sectorOptions.value = sectors ?? [];
|
|
|
+ } catch {
|
|
|
+ // silent
|
|
|
+ }
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+@use "src/css/quasar.variables.scss" as *;
|
|
|
+
|
|
|
+.complete-profile-card {
|
|
|
+ width: 95vw;
|
|
|
+ max-width: 480px;
|
|
|
+}
|
|
|
+
|
|
|
+.complete-profile-header {
|
|
|
+ background: linear-gradient(135deg, $violet-normal 0%, $violet-dark 100%);
|
|
|
+ border-radius: 4px 4px 0 0;
|
|
|
+}
|
|
|
+
|
|
|
+.avatar-wrap {
|
|
|
+ position: relative;
|
|
|
+ display: inline-block;
|
|
|
+}
|
|
|
+
|
|
|
+.complete-profile-avatar {
|
|
|
+ background: rgba(112, 32, 130, 0.18) !important;
|
|
|
+ border: 2px solid $violet-normal;
|
|
|
+
|
|
|
+ img {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.avatar-edit-btn {
|
|
|
+ position: absolute;
|
|
|
+ bottom: -2px;
|
|
|
+ right: -2px;
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ min-height: 24px;
|
|
|
+}
|
|
|
+</style>
|