| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353 |
- <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"
- mask="###.###.###-##"
- :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-input
- v-model="form.phone"
- outlined
- dense
- mask="(##) #####-####"
- :label="$t('auth.first_access.whatsapp')"
- :readonly="!!initialUser.phone"
- :bg-color="initialUser.phone ? '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>
- <TermsAcceptCheckbox
- v-if="!initialUser.terms_accepted_at"
- v-model="termsAccepted"
- class="q-mt-md"
- />
- </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";
- import { useMediaFile } from "src/composables/useMediaFile";
- import TermsAcceptCheckbox from "src/components/TermsAcceptCheckbox.vue";
- defineEmits([...useDialogPluginComponent.emits]);
- const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
- const $q = useQuasar();
- const { t } = useI18n();
- const store = userStore();
- const { prepareFile } = useMediaFile();
- const avatarInputRef = useTemplateRef("avatarInputRef");
- const initialUser = ref({});
- const localPhotoUrl = ref(null);
- const uploadingPhoto = ref(false);
- const saving = ref(false);
- const positionOptions = ref([]);
- const termsAccepted = ref(false);
- const sectorOptions = ref([]);
- const form = ref({
- name: "",
- cpf: "",
- email: "",
- phone: "",
- position_id: null,
- sector_id: null,
- registration: "",
- admission_date: "",
- });
- const canSave = computed(() => {
- return (
- (!!initialUser.value.terms_accepted_at || termsAccepted.value) &&
- !!localPhotoUrl.value &&
- !!form.value.name?.trim() &&
- !!form.value.cpf?.trim() &&
- !!form.value.email?.trim() &&
- !!form.value.phone?.trim() &&
- !!form.value.position_id &&
- !!form.value.sector_id
- );
- });
- const onAvatarSelected = async (event) => {
- const selected = event.target.files?.[0];
- event.target.value = "";
- if (!selected) return;
- const { file } = await prepareFile(selected);
- 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;
- }
- };
- 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.phone) payload.phone = form.value.phone;
- 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 (!initialUser.value.terms_accepted_at) payload.terms_accepted = 1;
- 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,
- phone: user.phone || 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,
- terms_accepted_at: user.terms_accepted_at || null,
- };
- form.value = {
- name: initialUser.value.name || "",
- cpf: initialUser.value.cpf || "",
- email: initialUser.value.email || "",
- phone: initialUser.value.phone || "",
- 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>
|