|
@@ -1,60 +1,23 @@
|
|
|
<template>
|
|
<template>
|
|
|
<div>
|
|
<div>
|
|
|
- <DefaultHeaderPage title="Cadastro de Usuário" />
|
|
|
|
|
|
|
+ <DefaultHeaderPage :title="isEdit ? 'Editar Usuário' : 'Cadastro de Usuário'" />
|
|
|
|
|
|
|
|
<div class="q-pa-md">
|
|
<div class="q-pa-md">
|
|
|
- <div class="column justify-center items-center q-mb-lg">
|
|
|
|
|
- <div class="q-mb-md">
|
|
|
|
|
- <q-avatar size="80px" color="grey-3">
|
|
|
|
|
- <q-icon name="mdi-account" size="48px" color="grey-6" />
|
|
|
|
|
- <q-btn
|
|
|
|
|
- round
|
|
|
|
|
- dense
|
|
|
|
|
- color="primary"
|
|
|
|
|
- icon="mdi-camera"
|
|
|
|
|
- size="xs"
|
|
|
|
|
- style="position: absolute; bottom: 0; right: 0"
|
|
|
|
|
- @click="triggerFileInput"
|
|
|
|
|
- />
|
|
|
|
|
- </q-avatar>
|
|
|
|
|
- <input
|
|
|
|
|
- ref="fileInputRef"
|
|
|
|
|
- type="file"
|
|
|
|
|
- accept="image/*"
|
|
|
|
|
- style="display: none"
|
|
|
|
|
- @change="onAvatarChange"
|
|
|
|
|
|
|
+ <q-form ref="formRef">
|
|
|
|
|
+ <div class="column items-center q-mb-lg">
|
|
|
|
|
+ <AvatarImageComponent
|
|
|
|
|
+ ref="avatarRef"
|
|
|
|
|
+ @update:file="(f) => (avatarFile = f)"
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <div class="row full-width q-mt-md q-col-gutter-sm">
|
|
|
|
|
- <DefaultSelect
|
|
|
|
|
- v-model="form.state"
|
|
|
|
|
- label="Estado / UF"
|
|
|
|
|
- class="col-6"
|
|
|
|
|
- outlined
|
|
|
|
|
- emit-value
|
|
|
|
|
- map-options
|
|
|
|
|
- :options="stateOptions"
|
|
|
|
|
- />
|
|
|
|
|
-
|
|
|
|
|
- <DefaultSelect
|
|
|
|
|
- v-model="form.unit"
|
|
|
|
|
- label="Unidade"
|
|
|
|
|
- class="col-6"
|
|
|
|
|
- outlined
|
|
|
|
|
- emit-value
|
|
|
|
|
- map-options
|
|
|
|
|
- :options="unitOptions"
|
|
|
|
|
- />
|
|
|
|
|
-
|
|
|
|
|
- <DefaultSelect
|
|
|
|
|
- v-model="form.role"
|
|
|
|
|
- label="Tipo de Usuário"
|
|
|
|
|
|
|
+ <div class="row q-col-gutter-sm">
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="form.name"
|
|
|
|
|
+ label="Nome completo"
|
|
|
class="col-6"
|
|
class="col-6"
|
|
|
outlined
|
|
outlined
|
|
|
- emit-value
|
|
|
|
|
- map-options
|
|
|
|
|
- :options="roleOptions"
|
|
|
|
|
|
|
+ :rules="[inputRules.required]"
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
<DefaultInput
|
|
<DefaultInput
|
|
@@ -62,134 +25,163 @@
|
|
|
label="CPF"
|
|
label="CPF"
|
|
|
class="col-6"
|
|
class="col-6"
|
|
|
outlined
|
|
outlined
|
|
|
|
|
+ :mask="masks.Brasil.cpf"
|
|
|
|
|
+ :rules="[inputRules.cpf]"
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
- <DefaultInput
|
|
|
|
|
- v-model="form.name"
|
|
|
|
|
- label="Nome"
|
|
|
|
|
|
|
+ <UserTypeSelect
|
|
|
|
|
+ v-model="form.user_type"
|
|
|
class="col-6"
|
|
class="col-6"
|
|
|
outlined
|
|
outlined
|
|
|
|
|
+ label="Função"
|
|
|
|
|
+ :rules="[inputRules.required]"
|
|
|
|
|
+ :disable="!canChangeUserType"
|
|
|
|
|
+ :exclude-types="['ADMIN']"
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
- <DefaultPasswordInput
|
|
|
|
|
- v-model="form.password"
|
|
|
|
|
- label="Senha"
|
|
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="form.phone"
|
|
|
|
|
+ label="Telefone"
|
|
|
class="col-6"
|
|
class="col-6"
|
|
|
outlined
|
|
outlined
|
|
|
|
|
+ :mask="masks.Brasil.celular"
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
<DefaultInput
|
|
<DefaultInput
|
|
|
- v-model="form.surname"
|
|
|
|
|
- label="Sobrenome"
|
|
|
|
|
|
|
+ v-model="form.email"
|
|
|
|
|
+ label="E-mail"
|
|
|
class="col-6"
|
|
class="col-6"
|
|
|
outlined
|
|
outlined
|
|
|
|
|
+ type="email"
|
|
|
|
|
+ :rules="[inputRules.required, inputRules.email]"
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
<DefaultPasswordInput
|
|
<DefaultPasswordInput
|
|
|
- v-model="form.password_confirmation"
|
|
|
|
|
- label="Repetir Senha"
|
|
|
|
|
- class="col-6"
|
|
|
|
|
- outlined
|
|
|
|
|
- />
|
|
|
|
|
-
|
|
|
|
|
- <DefaultInput
|
|
|
|
|
- v-model="form.email"
|
|
|
|
|
- label="E-mail"
|
|
|
|
|
|
|
+ v-model="form.password"
|
|
|
|
|
+ label="Senha"
|
|
|
class="col-6"
|
|
class="col-6"
|
|
|
outlined
|
|
outlined
|
|
|
- type="email"
|
|
|
|
|
|
|
+ :rules="
|
|
|
|
|
+ isEdit
|
|
|
|
|
+ ? [(v) => !v || v.length >= 8 || 'Mínimo 8 caracteres']
|
|
|
|
|
+ : [inputRules.required, inputRules.min(8)]
|
|
|
|
|
+ "
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
- <div class="col-6 flex items-center">
|
|
|
|
|
- <q-btn
|
|
|
|
|
- label="Gerar senha segura"
|
|
|
|
|
- icon="mdi-thumb-up-outline"
|
|
|
|
|
- color="primary"
|
|
|
|
|
- @click="generatePassword"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <div v-if="isEdit" class="col-12 text-caption text-grey-6">
|
|
|
|
|
+ Deixe os campos de senha em branco para manter a senha atual.
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <div class="row justify-end q-mt-md full-width">
|
|
|
|
|
|
|
+ <div class="row justify-end q-mt-lg q-gutter-sm">
|
|
|
|
|
+ <q-btn
|
|
|
|
|
+ label="Cancelar"
|
|
|
|
|
+ color="primary"
|
|
|
|
|
+ outline
|
|
|
|
|
+ @click="router.push({ name: 'UsersPage' })"
|
|
|
|
|
+ />
|
|
|
<q-btn
|
|
<q-btn
|
|
|
- label="SALVAR"
|
|
|
|
|
- icon-right="mdi-check"
|
|
|
|
|
|
|
+ label="Salvar"
|
|
|
color="primary"
|
|
color="primary"
|
|
|
|
|
+ :loading="loading"
|
|
|
@click="onSave"
|
|
@click="onSave"
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
|
+ </q-form>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
-import { ref, useTemplateRef } from "vue";
|
|
|
|
|
|
|
+import { ref, computed, onMounted } from "vue";
|
|
|
|
|
+import { useRouter, useRoute } from "vue-router";
|
|
|
|
|
+import { storeToRefs } from "pinia";
|
|
|
|
|
+import { useQuasar } from "quasar";
|
|
|
import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
|
|
import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
|
|
|
import DefaultInput from "src/components/defaults/DefaultInput.vue";
|
|
import DefaultInput from "src/components/defaults/DefaultInput.vue";
|
|
|
-import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
|
|
|
|
|
import DefaultPasswordInput from "src/components/defaults/DefaultPasswordInput.vue";
|
|
import DefaultPasswordInput from "src/components/defaults/DefaultPasswordInput.vue";
|
|
|
-
|
|
|
|
|
-const fileInputRef = useTemplateRef("fileInputRef");
|
|
|
|
|
|
|
+import AvatarImageComponent from "src/components/shared/AvatarImageComponent.vue";
|
|
|
|
|
+import UserTypeSelect from "src/components/selects/UserTypeSelect.vue";
|
|
|
|
|
+import { useInputRules } from "src/composables/useInputRules";
|
|
|
|
|
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
|
|
|
|
|
+import { createUser, updateUser, getUserById } from "src/api/user";
|
|
|
|
|
+import { userStore } from "src/stores/user";
|
|
|
|
|
+import masks from "src/helpers/masks";
|
|
|
|
|
+
|
|
|
|
|
+const router = useRouter();
|
|
|
|
|
+const route = useRoute();
|
|
|
|
|
+const $q = useQuasar();
|
|
|
|
|
+const { inputRules } = useInputRules();
|
|
|
|
|
+const { user } = storeToRefs(userStore());
|
|
|
|
|
+
|
|
|
|
|
+const formRef = ref(null);
|
|
|
|
|
+const avatarRef = ref(null);
|
|
|
|
|
+const avatarFile = ref(null);
|
|
|
|
|
+const originalUserType = ref(null);
|
|
|
|
|
+
|
|
|
|
|
+const isEdit = computed(() => !!route.params.id);
|
|
|
|
|
+const isEditingSelf = computed(() => isEdit.value && parseInt(route.params.id) === user.value?.id);
|
|
|
|
|
+const canChangeUserType = computed(() => user.value?.user_type === "ADMIN_FRANCHISEE");
|
|
|
|
|
|
|
|
const form = ref({
|
|
const form = ref({
|
|
|
- state: null,
|
|
|
|
|
- unit: null,
|
|
|
|
|
- role: null,
|
|
|
|
|
- cpf: null,
|
|
|
|
|
name: null,
|
|
name: null,
|
|
|
- surname: null,
|
|
|
|
|
|
|
+ cpf: null,
|
|
|
|
|
+ phone: null,
|
|
|
|
|
+ user_type: null,
|
|
|
email: null,
|
|
email: null,
|
|
|
password: null,
|
|
password: null,
|
|
|
- password_confirmation: null,
|
|
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
-const stateOptions = ref([
|
|
|
|
|
- { label: "Paraná", value: "PR" },
|
|
|
|
|
- { label: "São Paulo", value: "SP" },
|
|
|
|
|
- { label: "Santa Catarina", value: "SC" },
|
|
|
|
|
-]);
|
|
|
|
|
-
|
|
|
|
|
-const unitOptions = ref([
|
|
|
|
|
- { label: "Toledo-PR", value: "toledo" },
|
|
|
|
|
- { label: "Arapongas-PR", value: "arapongas" },
|
|
|
|
|
- { label: "Curitiba-PR", value: "curitiba" },
|
|
|
|
|
- { label: "Londrina-PR", value: "londrina" },
|
|
|
|
|
- { label: "Ponta Grossa-PR", value: "ponta_grossa" },
|
|
|
|
|
- { label: "Maringá-PR", value: "maringa" },
|
|
|
|
|
- { label: "Cascavel-PR", value: "cascavel" },
|
|
|
|
|
-]);
|
|
|
|
|
-
|
|
|
|
|
-const roleOptions = ref([
|
|
|
|
|
- { label: "Neurotainer", value: "neurotainer" },
|
|
|
|
|
- { label: "Assessor", value: "assessor" },
|
|
|
|
|
- { label: "Marketing", value: "marketing" },
|
|
|
|
|
- { label: "Comercial", value: "comercial" },
|
|
|
|
|
- { label: "Gestor", value: "gestor" },
|
|
|
|
|
- { label: "Administrativo", value: "administrativo" },
|
|
|
|
|
- { label: "Recepção", value: "recepcao" },
|
|
|
|
|
-]);
|
|
|
|
|
-
|
|
|
|
|
-function triggerFileInput() {
|
|
|
|
|
- fileInputRef.value?.click();
|
|
|
|
|
-}
|
|
|
|
|
|
|
+const { loading, execute } = useSubmitHandler({
|
|
|
|
|
+ formRef,
|
|
|
|
|
+ onSuccess: () => router.push({ name: "UsersPage" }),
|
|
|
|
|
+});
|
|
|
|
|
|
|
|
-function onAvatarChange(event) {
|
|
|
|
|
- const file = event.target.files[0];
|
|
|
|
|
- console.log("Avatar file selected:", file);
|
|
|
|
|
-}
|
|
|
|
|
|
|
+onMounted(async () => {
|
|
|
|
|
+ if (!isEdit.value) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data = await getUserById(route.params.id);
|
|
|
|
|
+ form.value.name = data.name;
|
|
|
|
|
+ form.value.email = data.email;
|
|
|
|
|
+ form.value.cpf = data.cpf;
|
|
|
|
|
+ form.value.phone = data.phone;
|
|
|
|
|
+ form.value.user_type = data.user_type;
|
|
|
|
|
+ originalUserType.value = data.user_type;
|
|
|
|
|
+ if (data.avatar_url) avatarRef.value?.setImageUrl(data.avatar_url);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("Failed to load user:", error);
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+function buildPayload() {
|
|
|
|
|
+ const fd = new FormData();
|
|
|
|
|
+
|
|
|
|
|
+ if (avatarFile.value) fd.append("avatar", avatarFile.value);
|
|
|
|
|
+
|
|
|
|
|
+ if (user.value?.unit_id) fd.append("unit_id", user.value.unit_id);
|
|
|
|
|
+ if (form.value.user_type) fd.append("user_type", form.value.user_type);
|
|
|
|
|
+ if (form.value.cpf) fd.append("cpf", form.value.cpf);
|
|
|
|
|
+ if (form.value.phone) fd.append("phone", form.value.phone);
|
|
|
|
|
+ if (form.value.name) fd.append("name", form.value.name);
|
|
|
|
|
+ if (form.value.email) fd.append("email", form.value.email);
|
|
|
|
|
+ if (form.value.password) fd.append("password", form.value.password);
|
|
|
|
|
|
|
|
-function generatePassword() {
|
|
|
|
|
- const chars =
|
|
|
|
|
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%";
|
|
|
|
|
- const password = Array.from({ length: 12 }, () =>
|
|
|
|
|
- chars.charAt(Math.floor(Math.random() * chars.length)),
|
|
|
|
|
- ).join("");
|
|
|
|
|
- form.value.password = password;
|
|
|
|
|
- form.value.password_confirmation = password;
|
|
|
|
|
|
|
+ return fd;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function onSave() {
|
|
|
|
|
- console.log("Saving user:", form.value);
|
|
|
|
|
|
|
+async function onSave() {
|
|
|
|
|
+ if (isEditingSelf.value && form.value.user_type !== originalUserType.value) {
|
|
|
|
|
+ $q.notify({
|
|
|
|
|
+ type: "warning",
|
|
|
|
|
+ message: "Não é possível alterar o seu próprio tipo de usuário. Entre em contato com o suporte.",
|
|
|
|
|
+ });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (isEdit.value) {
|
|
|
|
|
+ await execute(() => updateUser(buildPayload(), route.params.id));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ await execute(() => createUser(buildPayload()));
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
</script>
|
|
</script>
|