mirror of
https://github.com/hanzoai/helpdesk.git
synced 2026-10-12 16:56:02 +00:00
338 lines
11 KiB
Vue
338 lines
11 KiB
Vue
<template>
|
|
<SettingsLayoutBase
|
|
:title="__('Profile')"
|
|
:description="__('Manage your profile information.')"
|
|
>
|
|
<template #content>
|
|
<div class="flex items-center justify-between gap-2 pt-1.5 pb-8">
|
|
<FileUploader
|
|
:fileTypes="['image/*']"
|
|
@success="
|
|
(file) => {
|
|
updateImage(file.file_url);
|
|
}
|
|
"
|
|
>
|
|
<template #default="{ openFileSelector, error: _error, uploading }">
|
|
<div class="flex items-center justify-center gap-2">
|
|
<div class="group relative !size-14">
|
|
<Avatar
|
|
class="!size-14"
|
|
:image="user.doc?.user_image"
|
|
:label="fullName"
|
|
/>
|
|
<Tooltip
|
|
:hoverDelay="0"
|
|
placement="bottom"
|
|
:text="profileTooltipText"
|
|
>
|
|
<div
|
|
class="z-1 absolute top-0 left-0 flex h-9 cursor-pointer items-center justify-center rounded-full !size-14"
|
|
@click.stop="openFileSelector"
|
|
/>
|
|
<div
|
|
v-if="user.doc?.user_image"
|
|
class="z-1 size-4 absolute -top-1 -right-1 flex cursor-pointer items-center justify-center rounded-full bg-surface-white opacity-0 duration-300 ease-in-out group-hover:opacity-100 hover:bg-surface-gray-2 outline outline-black-overlay-50"
|
|
@click.stop="updateImage()"
|
|
@mouseenter="isHoveringRemove = true"
|
|
@mouseleave="isHoveringRemove = false"
|
|
>
|
|
<FeatherIcon
|
|
name="x"
|
|
class="size-3.5 cursor-pointer text-ink-gray-4"
|
|
/>
|
|
</div>
|
|
</Tooltip>
|
|
<div
|
|
v-if="uploading"
|
|
class="w-full h-full top-0 left-0 absolute bg-surface-gray-7 bg-opacity-20 rounded-full flex items-center justify-center"
|
|
>
|
|
<LoadingIndicator class="size-4" />
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-col gap-1">
|
|
<div class="flex flex-col gap-1">
|
|
<div v-if="!editName" class="flex items-end gap-1">
|
|
<span
|
|
class="text-lg sm:text-xl !font-semibold text-ink-gray-8"
|
|
>
|
|
{{ user?.doc?.full_name }}
|
|
</span>
|
|
<Button
|
|
class="!px-1 !h-5"
|
|
variant="ghost"
|
|
@click="editFullName"
|
|
>
|
|
<EditIcon class="size-3.5" />
|
|
</Button>
|
|
</div>
|
|
<div v-else class="flex items-center gap-1">
|
|
<TextInput
|
|
ref="fullNameRef"
|
|
v-model="fullName"
|
|
@keydown.enter="isNameDirty ? save() : (editName = false)"
|
|
@keydown.esc.stop="editName = false"
|
|
/>
|
|
<Button
|
|
variant="outline"
|
|
icon="check"
|
|
:loading="user?.save?.loading"
|
|
:disabled="user?.save?.loading"
|
|
@click="isNameDirty ? save() : (editName = false)"
|
|
/>
|
|
</div>
|
|
<span class="text-p-sm text-ink-gray-6">
|
|
{{ user?.doc?.email }}
|
|
</span>
|
|
</div>
|
|
<ErrorMessage :message="__(_error)" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</FileUploader>
|
|
</div>
|
|
<div>
|
|
<div class="flex items-center justify-between h-7">
|
|
<div class="flex gap-2 items-center">
|
|
<span class="text-base font-semibold text-ink-gray-9">
|
|
{{ __("Account Info & Security") }}
|
|
</span>
|
|
<UnsavedBadge :show="isDirty" />
|
|
</div>
|
|
|
|
<Transition name="fade">
|
|
<Button
|
|
variant="solid"
|
|
v-if="isDirty"
|
|
:label="__('Save')"
|
|
:loading="user?.save?.loading"
|
|
@click="save()"
|
|
/></Transition>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center justify-between mt-6">
|
|
<div class="flex flex-col gap-1">
|
|
<span class="text-base font-medium text-ink-gray-8">
|
|
{{ __("Emails & Signature") }}
|
|
</span>
|
|
<span class="text-p-sm text-ink-gray-6">
|
|
{{
|
|
__(
|
|
"Manage your account emails and email signature for communication."
|
|
)
|
|
}}
|
|
</span>
|
|
</div>
|
|
<Button
|
|
:label="__('Configure')"
|
|
@click="emit('updateStep', 'user-email-settings')"
|
|
/>
|
|
</div>
|
|
<div class="flex items-center justify-between mt-6">
|
|
<div class="flex flex-col gap-1">
|
|
<span class="text-base font-medium text-ink-gray-8">
|
|
{{ __("Password") }}
|
|
</span>
|
|
<span class="text-p-sm text-ink-gray-6">{{
|
|
__("Change your account password for security.")
|
|
}}</span>
|
|
</div>
|
|
<Button
|
|
icon-left="lock"
|
|
:label="__('Change Password')"
|
|
@click="showChangePasswordModal = true"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<div class="flex items-center justify-between mt-6">
|
|
<div class="flex flex-col gap-1">
|
|
<span class="text-base font-medium text-ink-gray-8">
|
|
{{ __("Language") }}
|
|
</span>
|
|
<span class="text-p-sm text-ink-gray-6">
|
|
{{ __("Change language of the application.") }}
|
|
</span>
|
|
</div>
|
|
<Link
|
|
:model-value="user.doc?.language"
|
|
@update:modelValue="updateLanguage"
|
|
doctype="Language"
|
|
class="w-40"
|
|
/>
|
|
</div>
|
|
<div class="flex items-center justify-between mt-6">
|
|
<div class="flex flex-col gap-1">
|
|
<span class="text-base font-medium text-ink-gray-8">
|
|
{{ __("Timezone") }}
|
|
</span>
|
|
<span class="text-p-sm text-ink-gray-6">
|
|
{{ __("Change timezone of the application.") }}
|
|
</span>
|
|
</div>
|
|
<TimezoneControl label="Timezone" v-model="timezone" class="!w-40" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</SettingsLayoutBase>
|
|
<ChangePasswordModal
|
|
v-if="showChangePasswordModal"
|
|
v-model="showChangePasswordModal"
|
|
/>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import Link from "@/components/frappe-ui/Link.vue";
|
|
import SettingsLayoutBase from "@/components/layouts/SettingsLayoutBase.vue";
|
|
import TimezoneControl from "@/components/TimezoneControl.vue";
|
|
import { useAuthStore } from "@/stores/auth";
|
|
import { __ } from "@/translation";
|
|
import { HDAgent } from "@/types/doctypes";
|
|
import { computed, nextTick, ref, watch, useTemplateRef } from "vue";
|
|
import {
|
|
Avatar,
|
|
Badge,
|
|
Button,
|
|
FileUploader,
|
|
LoadingIndicator,
|
|
toast,
|
|
createDocumentResource,
|
|
createResource,
|
|
} from "frappe-ui";
|
|
import { computed, ref } from "vue";
|
|
import CameraIcon from "~icons/lucide/camera";
|
|
import { disableSettingModalOutsideClick } from "../settingsModal";
|
|
import ChangePasswordModal from "./components/ChangePasswordModal.vue";
|
|
import { disableSettingModalOutsideClick } from "../settingsModal";
|
|
import EditIcon from "~icons/lucide/edit";
|
|
const emit = defineEmits(["updateStep"]);
|
|
import SettingsLayoutBase from "@/components/layouts/SettingsLayoutBase.vue";
|
|
import Link from "@/components/frappe-ui/Link.vue";
|
|
import { HDAgent } from "@/types/doctypes";
|
|
import UnsavedBadge from "@/components/UnsavedBadge.vue";
|
|
const auth = useAuthStore();
|
|
const profile = ref({
|
|
fullName: auth.userName,
|
|
userImage: auth.userImage,
|
|
firstName: auth.userFirstName,
|
|
lastName: auth.userLastName,
|
|
});
|
|
|
|
|
|
|
|
const showChangePasswordModal = ref(false);
|
|
const language = ref(auth.language);
|
|
const timezone = ref(auth.timezone);
|
|
const isLanguageChanged = computed(() => {
|
|
return language.value !== auth?.language;
|
|
},
|
|
});
|
|
|
|
const { userId } = useAuthStore();
|
|
const user = createDocumentResource({ doctype: "User", name: userId });
|
|
|
|
const isHoveringRemove = ref(false);
|
|
const editName = ref(false);
|
|
|
|
const profileTooltipText = computed(() => {
|
|
if (isHoveringRemove.value) return __("Remove Photo");
|
|
return user.doc?.user_image ? __("Change Photo") : __("Upload Photo");
|
|
});
|
|
|
|
const fullNameRef = useTemplateRef("fullNameRef");
|
|
const fullName = computed({
|
|
get: () => user.doc?.full_name ?? "",
|
|
set: (val) => {
|
|
if (!user.doc) return;
|
|
const [firstName, ...lastName] = val.split(" ");
|
|
user.doc.first_name = firstName;
|
|
user.doc.last_name = lastName.join(" ");
|
|
},
|
|
});
|
|
|
|
function editFullName() {
|
|
editName.value = true;
|
|
nextTick(() => fullNameRef.value?.el?.focus());
|
|
}
|
|
|
|
const isDirty = computed(() => {
|
|
if (!user.originalDoc) return false;
|
|
return user.doc?.time_zone !== user.originalDoc?.time_zone ||
|
|
user.doc?.language !== user.originalDoc?.language
|
|
? true
|
|
: false;
|
|
});
|
|
|
|
const isNameDirty = computed(() => {
|
|
return (
|
|
user.doc?.first_name !== user.originalDoc?.first_name ||
|
|
user.doc?.last_name !== user.originalDoc?.last_name
|
|
);
|
|
});
|
|
|
|
function save() {
|
|
refreshRequired.value =
|
|
user.doc?.language !== user.originalDoc?.language ||
|
|
user.doc?.time_zone !== user.originalDoc?.time_zone;
|
|
|
|
user.save.submit(null, {
|
|
onSuccess: () => {
|
|
editName.value = false;
|
|
toast.success(__("Profile updated successfully."));
|
|
if (refreshRequired.value) {
|
|
window.location.reload();
|
|
}
|
|
},
|
|
onError: (err: { message: string; messages: string[] }) => {
|
|
toast.error(err.message + ": " + err.messages[0]);
|
|
},
|
|
});
|
|
}
|
|
|
|
function updateImage(fileUrl = "") {
|
|
isHoveringRemove.value = false;
|
|
user.doc.user_image = fileUrl;
|
|
save();
|
|
}
|
|
|
|
function updateLanguage(val: string | null) {
|
|
if (!user.doc) return;
|
|
user.doc.language = val || user.originalDoc?.language;
|
|
}
|
|
|
|
function updateTimezone(val: { label: string; value: string } | null) {
|
|
if (!user.doc) return;
|
|
user.doc.time_zone = val?.value || user.originalDoc?.time_zone;
|
|
}
|
|
|
|
const timezoneOptions = ref([]);
|
|
const timezoneData = createResource({
|
|
url: "frappe.core.doctype.user.user.get_timezones",
|
|
auto: true,
|
|
onSuccess(data) {
|
|
timezoneOptions.value = data.timezones.map((tz) => ({
|
|
label: tz,
|
|
value: tz,
|
|
}));
|
|
},
|
|
});
|
|
|
|
const language = ref(null);
|
|
const timezone = ref(null);
|
|
|
|
const refreshRequired = ref(false);
|
|
|
|
watch(
|
|
() => user.doc,
|
|
(doc) => {
|
|
if (!doc) return;
|
|
if (!language.value) language.value = doc.language;
|
|
if (!timezone.value) timezone.value = doc.time_zone;
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
watch(isDirty, (val) => {
|
|
disableSettingModalOutsideClick.value = val;
|
|
});
|
|
</script>
|