Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import * as React from "react";
- import {
- Modal,
- Form,
- Input,
- Button,
- message,
- Row,
- Col,
- Spin,
- DatePicker,
- Card,
- Space,
- Divider,
- } from "antd";
- import { PlusOutlined } from "@ant-design/icons";
- import { useEmployeeSkillsDirectoryStore } from "../EmployeeSkillsDirectoryStore";
- import { User } from "@microsoft/microsoft-graph-types";
- import { EnhancedUser } from "../../../services/MSGraphService";
- import { globalContext } from "../../../common/GlobalContext";
- import CropModal from "./CropModal";
- import dayjs from "dayjs";
- import {
- SkillEntry,
- ExperienceEntry,
- EducationEntry,
- } from "../../../services/SPServices/DAO/StaffListDAO";
- import { SkillDefinition } from "../../../services/SPServices/DAO/SkillsListDAO";
- import PhotoEditor from "./EditUser/PhotoEditor";
- import TagEditor from "./EditUser/TagEditor";
- import SkillsSection from "./EditUser/SkillsSection";
- import ExperienceSection from "./EditUser/ExperienceSection";
- import EducationSection from "./EditUser/EducationSection";
- const { TextArea } = Input;
- type Props = {
- isVisible: boolean;
- selectedUser: EnhancedUser | null;
- onClose: () => void;
- };
- export default function EditUserModal({
- isVisible,
- selectedUser,
- onClose,
- }: Props) {
- const [form] = Form.useForm();
- const skillsSectionRef = React.useRef<any>(null);
- const experienceSectionRef = React.useRef<any>(null);
- const educationSectionRef = React.useRef<any>(null);
- const {
- updateUser,
- getUserDetails,
- getCurrentUserPhotoBlob,
- updateCurrentUserPhoto,
- deleteCurrentUserPhoto,
- getSkillsDictionary,
- getStaffItemById,
- saveProfile,
- } = useEmployeeSkillsDirectoryStore();
- const { spfxContext } = React.useContext(globalContext);
- const [loading, setLoading] = React.useState(false);
- const [detailsLoading, setDetailsLoading] = React.useState(false);
- const [photoLoading, setPhotoLoading] = React.useState(false);
- const [skillsLoading, setSkillsLoading] = React.useState(false);
- const [availableSkills, setAvailableSkills] = React.useState<
- SkillDefinition[]
- >([]);
- const [userSkills, setUserSkills] = React.useState<SkillEntry[]>([]);
- const [experience, setExperience] = React.useState<ExperienceEntry[]>([]);
- const [education, setEducation] = React.useState<EducationEntry[]>([]);
- const [interests, setInterests] = React.useState<string[]>([]);
- const [projects, setProjects] = React.useState<string[]>([]);
- const [schools, setSchools] = React.useState<string[]>([]);
- const [skills, setSkills] = React.useState<string[]>([]);
- const [userEmail, setUserEmail] = React.useState<string>("");
- const [imageFile, setImageFile] = React.useState<File | null>(null);
- const [currentPhotoUrl, setCurrentPhotoUrl] = React.useState<string | null>(
- null
- );
- const [isCropModalOpen, setIsCropModalOpen] = React.useState(false);
- const [tempImageUrl, setTempImageUrl] = React.useState<string | null>(null);
- const [isAdmin, setIsAdmin] = React.useState(false);
- const context = React.useContext(globalContext);
- const canEdit = React.useMemo(() => {
- if (!selectedUser) return false;
- const currentUserEmail = (spfxContext.pageContext.user.email || "").toLowerCase();
- const selMail = (selectedUser.mail || selectedUser.userPrincipalName || "").toLowerCase();
- const isOwner = selMail && currentUserEmail && (selMail === currentUserEmail);
- return isOwner || isAdmin;
- }, [selectedUser, spfxContext, isAdmin]);
- const uniq = (arr: readonly string[]) =>
- Array.from(new Set(arr.map((x) => (x ?? "").trim()).filter(Boolean)));
- const titlesFromUserSkills = (items: readonly SkillEntry[]) =>
- items.map((s) => s.title || "").filter((t) => t.trim().length > 0);
- const schoolsFromEducation = (items: readonly EducationEntry[]) =>
- items.map((e) => e.institution || "").filter((t) => t.trim().length > 0);
- React.useEffect(() => {
- if (!selectedUser || !isVisible) return;
- let mounted = true;
- (async () => {
- try {
- setDetailsLoading(true);
- setSkillsLoading(true);
- // 1. Skills dictionary
- const dict = await getSkillsDictionary();
- if (!mounted) return;
- setAvailableSkills(dict || []);
- setSkillsLoading(false);
- // 2. User details
- const details = await getUserDetails(selectedUser.id!);
- if (!mounted) return;
- form.setFieldsValue({
- aboutMe: details.aboutMe || "",
- birthday: details.birthday ? dayjs(details.birthday) : null,
- mobilePhone: details.mobilePhone || "",
- businessPhone: details.businessPhones?.[0] || "",
- jobTitle: details.jobTitle || "",
- companyName: details.companyName || "",
- department: details.department || "",
- city: details.city || "",
- });
- setInterests(details.interests || []);
- setSkills(uniq(details.skills || []));
- setSchools(uniq(details.schools || []));
- setCurrentPhotoUrl(details.photoUrl);
- setUserEmail(details.userPrincipalName || details.mail || "");
- const staff = await getStaffItemById(details.id!);
- if (!mounted) return;
- if (staff) {
- setUserSkills(staff.Skills || []);
- setExperience(staff.Experience || []);
- setEducation(staff.Education || []);
- setProjects(uniq(staff.Projects || []));
- }
- console.log('details', details);
- try {
- const adminsRes: any = await (context as any).sp.adminDAO.getAdmins();
- if (!mounted) return;
- const adminsArr: any[] = Array.isArray(adminsRes)
- ? adminsRes
- : Array.isArray(adminsRes?.PromiseResult)
- ? adminsRes.PromiseResult
- : [];
- const currentEmail = (spfxContext.pageContext.user.email || "").toLowerCase();
- const currentId = (spfxContext.pageContext.user.loginName || "") || null;
- const found = adminsArr.some((a: any) => {
- const adminEmail = (a.AdminUserEmail ?? a.Title ?? "").toLowerCase();
- const adminId = a.AdminUserId ?? a.Id ?? null;
- if (adminEmail && currentEmail && adminEmail === currentEmail) return true;
- if (adminId && currentId && String(adminId) === String(currentId)) return true;
- return false;
- });
- if (!mounted) return;
- setIsAdmin(Boolean(found));
- } catch (admErr) {
- console.error("Failed to load admins", admErr);
- if (mounted) setIsAdmin(false);
- }
- // 5. Photo: якщо власник (canEdit за власником) — підвантажуємо поточне фото користувача,
- // якщо адмін і редагує чужий профіль — намагаємось отримати фото selectedUser (якщо API підтримує)
- if (canEdit) {
- setPhotoLoading(true);
- const url = await getCurrentUserPhotoBlob();
- if (mounted) setCurrentPhotoUrl(url);
- } else if (isAdmin) {
- // пробуємо отримати фото для selectedUser через getUserDetails response або інший API
- // якщо у вас є метод отримати фото за id/email для будь-якого користувача — викличте його тут.
- // Інакше залишаємо photoUrl з details (встановлено вище).
- try {
- setPhotoLoading(true);
- // приклад: const url = await getPhotoForUser(selectedUser.id || selectedUser.userPrincipalName);
- // якщо такого методу немає — пропускаємо
- // const url = await getPhotoForUser(selectedUser.id || selectedUser.userPrincipalName || "");
- // if (mounted && url) setCurrentPhotoUrl(url);
- } catch (photoErr) {
- console.warn("Could not load photo for selected user", photoErr);
- } finally {
- if (mounted) setPhotoLoading(false);
- }
- }
- } catch (e) {
- console.error(e);
- message.error("Не вдалося завантажити дані користувача");
- } finally {
- if (mounted) {
- setDetailsLoading(false);
- setPhotoLoading(false);
- setSkillsLoading(false);
- }
- }
- })();
- return () => {
- mounted = false;
- };
- }, [selectedUser, isVisible, canEdit, getSkillsDictionary, getUserDetails, getStaffItemById, getCurrentUserPhotoBlob, spfxContext, context]);
- const syncGraphSkills = async (nextUserSkills: SkillEntry[] = userSkills) => {
- const merged = uniq(titlesFromUserSkills(nextUserSkills));
- setSkills(merged);
- await updateUser(selectedUser?.id || "", {
- skills: merged,
- } as Partial<User>);
- };
- const syncGraphSchools = async (
- nextEducation: EducationEntry[] = education
- ) => {
- const merged = uniq(schoolsFromEducation(nextEducation));
- setSchools(merged);
- await updateUser(selectedUser?.id || "", {
- schools: merged,
- } as Partial<User>);
- };
- const syncGraphProjects = async (nextProjects: string[] = projects) => {
- const merged = uniq(nextProjects);
- await updateUser(selectedUser?.id || "", {
- pastProjects: merged,
- } as Partial<User>);
- };
- const handleSubmit = async () => {
- try {
- setLoading(true);
- const v = await form.validateFields();
- // Створюємо об'єкт лише з заповненими полями
- const profileData: any = {
- userId: selectedUser?.id || selectedUser?.userPrincipalName || "",
- email: userEmail,
- interests,
- projects,
- staffSkills: userSkills,
- education,
- experience,
- };
- // Додаємо лише заповнені поля
- if (v.aboutMe?.trim()) profileData.aboutMe = v.aboutMe.trim();
- if (v.birthday) profileData.birthday = v.birthday.format("YYYY-MM-DD");
- if (v.mobilePhone?.trim()) profileData.mobilePhone = v.mobilePhone.trim();
- if (v.businessPhone?.trim()) profileData.businessPhone = v.businessPhone.trim();
- if (v.jobTitle?.trim()) profileData.jobTitle = v.jobTitle.trim();
- if (v.companyName?.trim()) profileData.companyName = v.companyName.trim();
- if (v.department?.trim()) profileData.department = v.department.trim();
- if (v.city?.trim()) profileData.city = v.city.trim();
- await saveProfile(profileData);
- if (imageFile && canEdit) {
- await updateCurrentUserPhoto(imageFile);
- }
- message.success("Profile updated!");
- onClose();
- } catch (e) {
- console.error(e);
- message.error("Error updating profile!");
- } finally {
- setLoading(false);
- }
- };
- const handleCancel = () => {
- form.resetFields();
- setSkills([]);
- setSchools([]);
- setInterests([]);
- setProjects([]);
- setUserSkills([]);
- setExperience([]);
- setEducation([]);
- setImageFile(null);
- setCurrentPhotoUrl(null);
- onClose();
- };
- const formatDate = (s: string) => dayjs(s).format("MMM YYYY");
- const cardStyle = {
- marginBottom: 4,
- borderRadius: 8,
- boxShadow: "0 1px 3px rgba(0, 0, 0, 0.1)",
- };
- const sectionHeaderStyle = {
- backgroundColor: "#fafafa",
- borderRadius: "8px 8px 0 0",
- padding: "8px",
- margin: "-1px -1px 16px -1px",
- borderBottom: "1px solid #f0f0f0",
- display: "flex",
- justifyContent: "space-between",
- alignItems: "center",
- };
- return (
- <Modal
- title={
- <div style={{ fontSize: 18, fontWeight: 600 }}>
- {canEdit ? "Edit your profile" : "View Profile"}
- </div>
- }
- open={isVisible}
- onCancel={handleCancel}
- footer={
- canEdit
- ? [
- <Button key="cancel" onClick={handleCancel} size="large">
- Cancel
- </Button>,
- <Button
- key="submit"
- type="primary"
- loading={loading}
- onClick={handleSubmit}
- size="large"
- >
- Save Changes
- </Button>,
- ]
- : [
- <Button key="close" onClick={handleCancel} size="large">
- Close
- </Button>,
- ]
- }
- width={1400}
- destroyOnHidden
- >
- <Spin spinning={detailsLoading}>
- <Row gutter={[6, 6]}>
- <Col span={6}>
- <PhotoEditor
- url={currentPhotoUrl}
- canEdit={canEdit}
- loading={photoLoading}
- onOpenCrop={() => {
- setTempImageUrl(currentPhotoUrl);
- setIsCropModalOpen(true);
- }}
- />
- <Card style={cardStyle} size="small">
- <div
- style={{ ...sectionHeaderStyle, justifyContent: "flex-start" }}
- >
- <h3 style={{ margin: 0, fontSize: 16, color: "#262626" }}>
- Basic Information
- </h3>
- </div>
- <Form form={form} layout="vertical" name="editUserForm">
- <Form.Item
- name="aboutMe"
- label="About Me"
- style={{ marginBottom: 16 }}
- >
- <TextArea
- rows={4}
- placeholder="Tell us a bit about yourself..."
- disabled={!canEdit}
- style={{ borderRadius: 6 }}
- />
- </Form.Item>
- <Divider style={{ margin: "16px 0" }} />
- <Space direction="vertical" size={12} style={{ width: "100%" }}>
- <Form.Item
- name="mobilePhone"
- label="Mobile Phone"
- style={{ marginBottom: 0 }}
- >
- <Input disabled={!canEdit} style={{ borderRadius: 6 }} />
- </Form.Item>
- <Form.Item
- name="businessPhone"
- label="Business Phone"
- style={{ marginBottom: 0 }}
- >
- <Input disabled={!canEdit} style={{ borderRadius: 6 }} />
- </Form.Item>
- </Space>
- <Divider style={{ margin: "16px 0" }} />
- <Space direction="vertical" size={12} style={{ width: "100%" }}>
- <Form.Item
- name="jobTitle"
- label="Job Title"
- style={{ marginBottom: 0 }}
- >
- <Input disabled={!canEdit} style={{ borderRadius: 6 }} />
- </Form.Item>
- <Form.Item
- name="companyName"
- label="Company"
- style={{ marginBottom: 0 }}
- >
- <Input disabled={!canEdit} style={{ borderRadius: 6 }} />
- </Form.Item>
- <Form.Item
- name="department"
- label="Department"
- style={{ marginBottom: 0 }}
- >
- <Input disabled={!canEdit} style={{ borderRadius: 6 }} />
- </Form.Item>
- <Form.Item
- name="city"
- label="City"
- style={{ marginBottom: 0 }}
- >
- <Input disabled={!canEdit} style={{ borderRadius: 6 }} />
- </Form.Item>
- </Space>
- <Divider style={{ margin: "16px 0" }} />
- <Form.Item
- name="birthday"
- label="Birthday"
- style={{ marginBottom: 0 }}
- >
- <DatePicker
- style={{ width: "100%", borderRadius: 6 }}
- format="YYYY-MM-DD"
- disabled={!canEdit}
- />
- </Form.Item>
- </Form>
- </Card>
- </Col>
- <Col span={18}>
- <Card style={cardStyle} size="small">
- <div
- style={{ ...sectionHeaderStyle, justifyContent: "flex-start" }}
- >
- <h3 style={{ margin: 0, fontSize: 16, color: "#262626" }}>
- Interests & Projects
- </h3>
- </div>
- <Row gutter={[16, 0]}>
- <Col span={12}>
- <TagEditor
- title="Interests"
- value={interests}
- onChange={setInterests}
- canEdit={canEdit}
- placeholder="Add an interest..."
- />
- </Col>
- <Col span={12}>
- <TagEditor
- title="Projects"
- value={projects}
- onChange={(n) => {
- setProjects(n);
- syncGraphProjects(n);
- }}
- canEdit={canEdit}
- placeholder="Add a project..."
- />
- </Col>
- </Row>
- </Card>
- <Card style={cardStyle} size="small">
- <div style={sectionHeaderStyle}>
- <h3 style={{ margin: 0, fontSize: 16, color: "#262626" }}>
- Professional Skills
- </h3>
- {canEdit && (
- <Button
- type="primary"
- icon={<PlusOutlined />}
- onClick={() => skillsSectionRef.current?.openAddModal?.()}
- disabled={!availableSkills.length}
- size="small"
- >
- Add Skill
- </Button>
- )}
- </div>
- <SkillsSection
- ref={skillsSectionRef}
- availableSkills={availableSkills}
- skillsLoading={skillsLoading}
- userSkills={userSkills}
- setUserSkills={setUserSkills}
- canEdit={canEdit}
- onChange={syncGraphSkills}
- showAddButton={false}
- />
- </Card>
- <Card style={cardStyle} size="small">
- <div style={sectionHeaderStyle}>
- <h3 style={{ margin: 0, fontSize: 16, color: "#262626" }}>
- Work Experience
- </h3>
- {canEdit && (
- <Button
- type="primary"
- icon={<PlusOutlined />}
- onClick={() =>
- experienceSectionRef.current?.openAddModal?.()
- }
- size="small"
- >
- Add Experience
- </Button>
- )}
- </div>
- <ExperienceSection
- ref={experienceSectionRef}
- value={experience}
- onChange={setExperience}
- canEdit={canEdit}
- formatDate={formatDate}
- showAddButton={false}
- />
- </Card>
- <Card style={cardStyle} size="small">
- <div style={sectionHeaderStyle}>
- <h3 style={{ margin: 0, fontSize: 16, color: "#262626" }}>
- Education
- </h3>
- {canEdit && (
- <Button
- type="primary"
- icon={<PlusOutlined />}
- onClick={() =>
- educationSectionRef.current?.openAddModal?.()
- }
- size="small"
- >
- Add Education
- </Button>
- )}
- </div>
- <EducationSection
- ref={educationSectionRef}
- value={education}
- onChange={(n) => {
- setEducation(n);
- syncGraphSchools(n);
- }}
- canEdit={canEdit}
- formatDate={formatDate}
- showAddButton={false}
- />
- </Card>
- </Col>
- </Row>
- </Spin>
- <CropModal
- open={isCropModalOpen}
- imageUrl={tempImageUrl}
- onCancel={() => setIsCropModalOpen(false)}
- onApply={(croppedUrl, file) => {
- setCurrentPhotoUrl(croppedUrl);
- setImageFile(file);
- setIsCropModalOpen(false);
- }}
- onDelete={async () => {
- try {
- if (canEdit) await deleteCurrentUserPhoto();
- setCurrentPhotoUrl(null);
- setImageFile(null);
- setIsCropModalOpen(false);
- message.success("Photo deleted!");
- } catch {
- message.error("Error deleting photo!");
- }
- }}
- />
- </Modal>
- );
- }
Advertisement
Add Comment
Please, Sign In to add comment