Vla_DOS

modal

Oct 1st, 2025
171
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. import * as React from "react";
  2. import {
  3.   Modal,
  4.   Form,
  5.   Input,
  6.   Button,
  7.   message,
  8.   Row,
  9.   Col,
  10.   Spin,
  11.   DatePicker,
  12.   Card,
  13.   Space,
  14.   Divider,
  15. } from "antd";
  16. import { PlusOutlined } from "@ant-design/icons";
  17. import { useEmployeeSkillsDirectoryStore } from "../EmployeeSkillsDirectoryStore";
  18. import { User } from "@microsoft/microsoft-graph-types";
  19. import { EnhancedUser } from "../../../services/MSGraphService";
  20. import { globalContext } from "../../../common/GlobalContext";
  21. import CropModal from "./CropModal";
  22. import dayjs from "dayjs";
  23. import {
  24.   SkillEntry,
  25.   ExperienceEntry,
  26.   EducationEntry,
  27. } from "../../../services/SPServices/DAO/StaffListDAO";
  28. import { SkillDefinition } from "../../../services/SPServices/DAO/SkillsListDAO";
  29.  
  30. import PhotoEditor from "./EditUser/PhotoEditor";
  31. import TagEditor from "./EditUser/TagEditor";
  32. import SkillsSection from "./EditUser/SkillsSection";
  33. import ExperienceSection from "./EditUser/ExperienceSection";
  34. import EducationSection from "./EditUser/EducationSection";
  35.  
  36. const { TextArea } = Input;
  37.  
  38. type Props = {
  39.   isVisible: boolean;
  40.   selectedUser: EnhancedUser | null;
  41.   onClose: () => void;
  42. };
  43.  
  44. export default function EditUserModal({
  45.   isVisible,
  46.   selectedUser,
  47.   onClose,
  48. }: Props) {
  49.   const [form] = Form.useForm();
  50.  
  51.   const skillsSectionRef = React.useRef<any>(null);
  52.   const experienceSectionRef = React.useRef<any>(null);
  53.   const educationSectionRef = React.useRef<any>(null);
  54.  
  55.   const {
  56.     updateUser,
  57.     getUserDetails,
  58.     getCurrentUserPhotoBlob,
  59.     updateCurrentUserPhoto,
  60.     deleteCurrentUserPhoto,
  61.     getSkillsDictionary,
  62.     getStaffItemById,
  63.     saveProfile,
  64.   } = useEmployeeSkillsDirectoryStore();
  65.  
  66.   const { spfxContext } = React.useContext(globalContext);
  67.  
  68.   const [loading, setLoading] = React.useState(false);
  69.   const [detailsLoading, setDetailsLoading] = React.useState(false);
  70.   const [photoLoading, setPhotoLoading] = React.useState(false);
  71.   const [skillsLoading, setSkillsLoading] = React.useState(false);
  72.  
  73.   const [availableSkills, setAvailableSkills] = React.useState<
  74.     SkillDefinition[]
  75.   >([]);
  76.   const [userSkills, setUserSkills] = React.useState<SkillEntry[]>([]);
  77.   const [experience, setExperience] = React.useState<ExperienceEntry[]>([]);
  78.   const [education, setEducation] = React.useState<EducationEntry[]>([]);
  79.  
  80.   const [interests, setInterests] = React.useState<string[]>([]);
  81.   const [projects, setProjects] = React.useState<string[]>([]);
  82.   const [schools, setSchools] = React.useState<string[]>([]);
  83.   const [skills, setSkills] = React.useState<string[]>([]);
  84.   const [userEmail, setUserEmail] = React.useState<string>("");
  85.   const [imageFile, setImageFile] = React.useState<File | null>(null);
  86.   const [currentPhotoUrl, setCurrentPhotoUrl] = React.useState<string | null>(
  87.     null
  88.   );
  89.   const [isCropModalOpen, setIsCropModalOpen] = React.useState(false);
  90.   const [tempImageUrl, setTempImageUrl] = React.useState<string | null>(null);
  91.   const [isAdmin, setIsAdmin] = React.useState(false);
  92.   const context = React.useContext(globalContext);
  93.  
  94.  
  95. const canEdit = React.useMemo(() => {
  96.   if (!selectedUser) return false;
  97.   const currentUserEmail = (spfxContext.pageContext.user.email || "").toLowerCase();
  98.   const selMail = (selectedUser.mail || selectedUser.userPrincipalName || "").toLowerCase();
  99.   const isOwner = selMail && currentUserEmail && (selMail === currentUserEmail);
  100.   return isOwner || isAdmin;
  101. }, [selectedUser, spfxContext, isAdmin]);
  102.  
  103.  
  104.   const uniq = (arr: readonly string[]) =>
  105.     Array.from(new Set(arr.map((x) => (x ?? "").trim()).filter(Boolean)));
  106.   const titlesFromUserSkills = (items: readonly SkillEntry[]) =>
  107.     items.map((s) => s.title || "").filter((t) => t.trim().length > 0);
  108.   const schoolsFromEducation = (items: readonly EducationEntry[]) =>
  109.     items.map((e) => e.institution || "").filter((t) => t.trim().length > 0);
  110.  
  111. React.useEffect(() => {
  112.   if (!selectedUser || !isVisible) return;
  113.   let mounted = true;
  114.  
  115.   (async () => {
  116.     try {
  117.       setDetailsLoading(true);
  118.       setSkillsLoading(true);
  119.  
  120.       // 1. Skills dictionary
  121.       const dict = await getSkillsDictionary();
  122.       if (!mounted) return;
  123.       setAvailableSkills(dict || []);
  124.       setSkillsLoading(false);
  125.  
  126.       // 2. User details
  127.       const details = await getUserDetails(selectedUser.id!);
  128.  
  129.       if (!mounted) return;
  130.       form.setFieldsValue({
  131.         aboutMe: details.aboutMe || "",
  132.         birthday: details.birthday ? dayjs(details.birthday) : null,
  133.         mobilePhone: details.mobilePhone || "",
  134.         businessPhone: details.businessPhones?.[0] || "",
  135.         jobTitle: details.jobTitle || "",
  136.         companyName: details.companyName || "",
  137.         department: details.department || "",
  138.         city: details.city || "",
  139.       });
  140.       setInterests(details.interests || []);
  141.       setSkills(uniq(details.skills || []));
  142.       setSchools(uniq(details.schools || []));
  143.       setCurrentPhotoUrl(details.photoUrl);
  144.       setUserEmail(details.userPrincipalName || details.mail || "");
  145.  
  146.       const staff = await getStaffItemById(details.id!);
  147.       if (!mounted) return;
  148.       if (staff) {
  149.         setUserSkills(staff.Skills || []);
  150.         setExperience(staff.Experience || []);
  151.         setEducation(staff.Education || []);
  152.         setProjects(uniq(staff.Projects || []));
  153.       }
  154.      
  155.       console.log('details', details);
  156.       try {
  157.         const adminsRes: any = await (context as any).sp.adminDAO.getAdmins();
  158.         if (!mounted) return;
  159.  
  160.         const adminsArr: any[] = Array.isArray(adminsRes)
  161.           ? adminsRes
  162.           : Array.isArray(adminsRes?.PromiseResult)
  163.           ? adminsRes.PromiseResult
  164.           : [];
  165.  
  166.         const currentEmail = (spfxContext.pageContext.user.email || "").toLowerCase();
  167.         const currentId = (spfxContext.pageContext.user.loginName || "") || null;
  168.  
  169.         const found = adminsArr.some((a: any) => {
  170.           const adminEmail = (a.AdminUserEmail ?? a.Title ?? "").toLowerCase();
  171.           const adminId = a.AdminUserId ?? a.Id ?? null;
  172.           if (adminEmail && currentEmail && adminEmail === currentEmail) return true;
  173.           if (adminId && currentId && String(adminId) === String(currentId)) return true;
  174.           return false;
  175.         });
  176.  
  177.         if (!mounted) return;
  178.         setIsAdmin(Boolean(found));
  179.       } catch (admErr) {
  180.         console.error("Failed to load admins", admErr);
  181.         if (mounted) setIsAdmin(false);
  182.       }
  183.  
  184.       // 5. Photo: якщо власник (canEdit за власником) — підвантажуємо поточне фото користувача,
  185.       // якщо адмін і редагує чужий профіль — намагаємось отримати фото selectedUser (якщо API підтримує)
  186.       if (canEdit) {
  187.         setPhotoLoading(true);
  188.         const url = await getCurrentUserPhotoBlob();
  189.         if (mounted) setCurrentPhotoUrl(url);
  190.       } else if (isAdmin) {
  191.         // пробуємо отримати фото для selectedUser через getUserDetails response або інший API
  192.         // якщо у вас є метод отримати фото за id/email для будь-якого користувача — викличте його тут.
  193.         // Інакше залишаємо photoUrl з details (встановлено вище).
  194.         try {
  195.           setPhotoLoading(true);
  196.           // приклад: const url = await getPhotoForUser(selectedUser.id || selectedUser.userPrincipalName);
  197.           // якщо такого методу немає — пропускаємо
  198.           // const url = await getPhotoForUser(selectedUser.id || selectedUser.userPrincipalName || "");
  199.           // if (mounted && url) setCurrentPhotoUrl(url);
  200.         } catch (photoErr) {
  201.           console.warn("Could not load photo for selected user", photoErr);
  202.         } finally {
  203.           if (mounted) setPhotoLoading(false);
  204.         }
  205.       }
  206.     } catch (e) {
  207.       console.error(e);
  208.       message.error("Не вдалося завантажити дані користувача");
  209.     } finally {
  210.       if (mounted) {
  211.         setDetailsLoading(false);
  212.         setPhotoLoading(false);
  213.         setSkillsLoading(false);
  214.       }
  215.     }
  216.   })();
  217.  
  218.   return () => {
  219.     mounted = false;
  220.   };
  221. }, [selectedUser, isVisible, canEdit, getSkillsDictionary, getUserDetails, getStaffItemById, getCurrentUserPhotoBlob, spfxContext, context]);
  222.  
  223.   const syncGraphSkills = async (nextUserSkills: SkillEntry[] = userSkills) => {
  224.     const merged = uniq(titlesFromUserSkills(nextUserSkills));
  225.     setSkills(merged);
  226.     await updateUser(selectedUser?.id || "", {
  227.       skills: merged,
  228.     } as Partial<User>);
  229.   };
  230.   const syncGraphSchools = async (
  231.     nextEducation: EducationEntry[] = education
  232.   ) => {
  233.     const merged = uniq(schoolsFromEducation(nextEducation));
  234.     setSchools(merged);
  235.     await updateUser(selectedUser?.id || "", {
  236.       schools: merged,
  237.     } as Partial<User>);
  238.   };
  239.   const syncGraphProjects = async (nextProjects: string[] = projects) => {
  240.     const merged = uniq(nextProjects);
  241.     await updateUser(selectedUser?.id || "", {
  242.       pastProjects: merged,
  243.     } as Partial<User>);
  244.   };
  245.  
  246.   const handleSubmit = async () => {
  247.     try {
  248.       setLoading(true);
  249.       const v = await form.validateFields();
  250.  
  251.       // Створюємо об'єкт лише з заповненими полями
  252.       const profileData: any = {
  253.         userId: selectedUser?.id || selectedUser?.userPrincipalName || "",
  254.         email: userEmail,
  255.         interests,
  256.         projects,
  257.         staffSkills: userSkills,
  258.         education,
  259.         experience,
  260.       };
  261.  
  262.       // Додаємо лише заповнені поля
  263.       if (v.aboutMe?.trim()) profileData.aboutMe = v.aboutMe.trim();
  264.       if (v.birthday) profileData.birthday = v.birthday.format("YYYY-MM-DD");
  265.       if (v.mobilePhone?.trim()) profileData.mobilePhone = v.mobilePhone.trim();
  266.       if (v.businessPhone?.trim()) profileData.businessPhone = v.businessPhone.trim();
  267.       if (v.jobTitle?.trim()) profileData.jobTitle = v.jobTitle.trim();
  268.       if (v.companyName?.trim()) profileData.companyName = v.companyName.trim();
  269.       if (v.department?.trim()) profileData.department = v.department.trim();
  270.       if (v.city?.trim()) profileData.city = v.city.trim();
  271.  
  272.       await saveProfile(profileData);
  273.  
  274.       if (imageFile && canEdit) {
  275.         await updateCurrentUserPhoto(imageFile);
  276.       }
  277.  
  278.       message.success("Profile updated!");
  279.       onClose();
  280.     } catch (e) {
  281.       console.error(e);
  282.       message.error("Error updating profile!");
  283.     } finally {
  284.       setLoading(false);
  285.     }
  286.   };
  287.  
  288.   const handleCancel = () => {
  289.     form.resetFields();
  290.     setSkills([]);
  291.     setSchools([]);
  292.     setInterests([]);
  293.     setProjects([]);
  294.     setUserSkills([]);
  295.     setExperience([]);
  296.     setEducation([]);
  297.     setImageFile(null);
  298.     setCurrentPhotoUrl(null);
  299.     onClose();
  300.   };
  301.  
  302.   const formatDate = (s: string) => dayjs(s).format("MMM YYYY");
  303.  
  304.   const cardStyle = {
  305.     marginBottom: 4,
  306.     borderRadius: 8,
  307.     boxShadow: "0 1px 3px rgba(0, 0, 0, 0.1)",
  308.   };
  309.  
  310.   const sectionHeaderStyle = {
  311.     backgroundColor: "#fafafa",
  312.     borderRadius: "8px 8px 0 0",
  313.     padding: "8px",
  314.     margin: "-1px -1px 16px -1px",
  315.     borderBottom: "1px solid #f0f0f0",
  316.     display: "flex",
  317.     justifyContent: "space-between",
  318.     alignItems: "center",
  319.   };
  320.  
  321.   return (
  322.     <Modal
  323.       title={
  324.         <div style={{ fontSize: 18, fontWeight: 600 }}>
  325.           {canEdit ? "Edit your profile" : "View Profile"}
  326.         </div>
  327.       }
  328.       open={isVisible}
  329.       onCancel={handleCancel}
  330.       footer={
  331.         canEdit
  332.           ? [
  333.               <Button key="cancel" onClick={handleCancel} size="large">
  334.                 Cancel
  335.               </Button>,
  336.               <Button
  337.                 key="submit"
  338.                 type="primary"
  339.                 loading={loading}
  340.                 onClick={handleSubmit}
  341.                 size="large"
  342.               >
  343.                 Save Changes
  344.               </Button>,
  345.             ]
  346.           : [
  347.               <Button key="close" onClick={handleCancel} size="large">
  348.                 Close
  349.               </Button>,
  350.             ]
  351.       }
  352.       width={1400}
  353.       destroyOnHidden
  354.     >
  355.       <Spin spinning={detailsLoading}>
  356.         <Row gutter={[6, 6]}>
  357.           <Col span={6}>
  358.             <PhotoEditor
  359.               url={currentPhotoUrl}
  360.               canEdit={canEdit}
  361.               loading={photoLoading}
  362.               onOpenCrop={() => {
  363.                 setTempImageUrl(currentPhotoUrl);
  364.                 setIsCropModalOpen(true);
  365.               }}
  366.             />
  367.             <Card style={cardStyle} size="small">
  368.               <div
  369.                 style={{ ...sectionHeaderStyle, justifyContent: "flex-start" }}
  370.               >
  371.                 <h3 style={{ margin: 0, fontSize: 16, color: "#262626" }}>
  372.                   Basic Information
  373.                 </h3>
  374.               </div>
  375.               <Form form={form} layout="vertical" name="editUserForm">
  376.                 <Form.Item
  377.                   name="aboutMe"
  378.                   label="About Me"
  379.                   style={{ marginBottom: 16 }}
  380.                 >
  381.                   <TextArea
  382.                     rows={4}
  383.                     placeholder="Tell us a bit about yourself..."
  384.                     disabled={!canEdit}
  385.                     style={{ borderRadius: 6 }}
  386.                   />
  387.                 </Form.Item>
  388.  
  389.                 <Divider style={{ margin: "16px 0" }} />
  390.  
  391.                 <Space direction="vertical" size={12} style={{ width: "100%" }}>
  392.                   <Form.Item
  393.                     name="mobilePhone"
  394.                     label="Mobile Phone"
  395.                     style={{ marginBottom: 0 }}
  396.                   >
  397.                     <Input disabled={!canEdit} style={{ borderRadius: 6 }} />
  398.                   </Form.Item>
  399.                   <Form.Item
  400.                     name="businessPhone"
  401.                     label="Business Phone"
  402.                     style={{ marginBottom: 0 }}
  403.                   >
  404.                     <Input disabled={!canEdit} style={{ borderRadius: 6 }} />
  405.                   </Form.Item>
  406.                 </Space>
  407.  
  408.                 <Divider style={{ margin: "16px 0" }} />
  409.  
  410.                 <Space direction="vertical" size={12} style={{ width: "100%" }}>
  411.                   <Form.Item
  412.                     name="jobTitle"
  413.                     label="Job Title"
  414.                     style={{ marginBottom: 0 }}
  415.                   >
  416.                     <Input disabled={!canEdit} style={{ borderRadius: 6 }} />
  417.                   </Form.Item>
  418.                   <Form.Item
  419.                     name="companyName"
  420.                     label="Company"
  421.                     style={{ marginBottom: 0 }}
  422.                   >
  423.                     <Input disabled={!canEdit} style={{ borderRadius: 6 }} />
  424.                   </Form.Item>
  425.                   <Form.Item
  426.                     name="department"
  427.                     label="Department"
  428.                     style={{ marginBottom: 0 }}
  429.                   >
  430.                     <Input disabled={!canEdit} style={{ borderRadius: 6 }} />
  431.                   </Form.Item>
  432.                   <Form.Item
  433.                     name="city"
  434.                     label="City"
  435.                     style={{ marginBottom: 0 }}
  436.                   >
  437.                     <Input disabled={!canEdit} style={{ borderRadius: 6 }} />
  438.                   </Form.Item>
  439.                 </Space>
  440.  
  441.                 <Divider style={{ margin: "16px 0" }} />
  442.  
  443.                 <Form.Item
  444.                   name="birthday"
  445.                   label="Birthday"
  446.                   style={{ marginBottom: 0 }}
  447.                 >
  448.                   <DatePicker
  449.                     style={{ width: "100%", borderRadius: 6 }}
  450.                     format="YYYY-MM-DD"
  451.                     disabled={!canEdit}
  452.                   />
  453.                 </Form.Item>
  454.               </Form>
  455.             </Card>
  456.           </Col>
  457.  
  458.           <Col span={18}>
  459.             <Card style={cardStyle} size="small">
  460.               <div
  461.                 style={{ ...sectionHeaderStyle, justifyContent: "flex-start" }}
  462.               >
  463.                 <h3 style={{ margin: 0, fontSize: 16, color: "#262626" }}>
  464.                   Interests & Projects
  465.                 </h3>
  466.               </div>
  467.               <Row gutter={[16, 0]}>
  468.                 <Col span={12}>
  469.                   <TagEditor
  470.                     title="Interests"
  471.                     value={interests}
  472.                     onChange={setInterests}
  473.                     canEdit={canEdit}
  474.                     placeholder="Add an interest..."
  475.                   />
  476.                 </Col>
  477.                 <Col span={12}>
  478.                   <TagEditor
  479.                     title="Projects"
  480.                     value={projects}
  481.                     onChange={(n) => {
  482.                       setProjects(n);
  483.                       syncGraphProjects(n);
  484.                     }}
  485.                     canEdit={canEdit}
  486.                     placeholder="Add a project..."
  487.                   />
  488.                 </Col>
  489.               </Row>
  490.             </Card>
  491.  
  492.             <Card style={cardStyle} size="small">
  493.               <div style={sectionHeaderStyle}>
  494.                 <h3 style={{ margin: 0, fontSize: 16, color: "#262626" }}>
  495.                   Professional Skills
  496.                 </h3>
  497.                 {canEdit && (
  498.                   <Button
  499.                     type="primary"
  500.                     icon={<PlusOutlined />}
  501.                     onClick={() => skillsSectionRef.current?.openAddModal?.()}
  502.                     disabled={!availableSkills.length}
  503.                     size="small"
  504.                   >
  505.                     Add Skill
  506.                   </Button>
  507.                 )}
  508.               </div>
  509.               <SkillsSection
  510.                 ref={skillsSectionRef}
  511.                 availableSkills={availableSkills}
  512.                 skillsLoading={skillsLoading}
  513.                 userSkills={userSkills}
  514.                 setUserSkills={setUserSkills}
  515.                 canEdit={canEdit}
  516.                 onChange={syncGraphSkills}
  517.                 showAddButton={false}
  518.               />
  519.             </Card>
  520.  
  521.             <Card style={cardStyle} size="small">
  522.               <div style={sectionHeaderStyle}>
  523.                 <h3 style={{ margin: 0, fontSize: 16, color: "#262626" }}>
  524.                   Work Experience
  525.                 </h3>
  526.                 {canEdit && (
  527.                   <Button
  528.                     type="primary"
  529.                     icon={<PlusOutlined />}
  530.                     onClick={() =>
  531.                       experienceSectionRef.current?.openAddModal?.()
  532.                     }
  533.                     size="small"
  534.                   >
  535.                     Add Experience
  536.                   </Button>
  537.                 )}
  538.               </div>
  539.               <ExperienceSection
  540.                 ref={experienceSectionRef}
  541.                 value={experience}
  542.                 onChange={setExperience}
  543.                 canEdit={canEdit}
  544.                 formatDate={formatDate}
  545.                 showAddButton={false}
  546.               />
  547.             </Card>
  548.  
  549.             <Card style={cardStyle} size="small">
  550.               <div style={sectionHeaderStyle}>
  551.                 <h3 style={{ margin: 0, fontSize: 16, color: "#262626" }}>
  552.                   Education
  553.                 </h3>
  554.                 {canEdit && (
  555.                   <Button
  556.                     type="primary"
  557.                     icon={<PlusOutlined />}
  558.                     onClick={() =>
  559.                       educationSectionRef.current?.openAddModal?.()
  560.                     }
  561.                     size="small"
  562.                   >
  563.                     Add Education
  564.                   </Button>
  565.                 )}
  566.               </div>
  567.               <EducationSection
  568.                 ref={educationSectionRef}
  569.                 value={education}
  570.                 onChange={(n) => {
  571.                   setEducation(n);
  572.                   syncGraphSchools(n);
  573.                 }}
  574.                 canEdit={canEdit}
  575.                 formatDate={formatDate}
  576.                 showAddButton={false}
  577.               />
  578.             </Card>
  579.           </Col>
  580.         </Row>
  581.       </Spin>
  582.  
  583.       <CropModal
  584.         open={isCropModalOpen}
  585.         imageUrl={tempImageUrl}
  586.         onCancel={() => setIsCropModalOpen(false)}
  587.         onApply={(croppedUrl, file) => {
  588.           setCurrentPhotoUrl(croppedUrl);
  589.           setImageFile(file);
  590.           setIsCropModalOpen(false);
  591.         }}
  592.         onDelete={async () => {
  593.           try {
  594.             if (canEdit) await deleteCurrentUserPhoto();
  595.             setCurrentPhotoUrl(null);
  596.             setImageFile(null);
  597.             setIsCropModalOpen(false);
  598.             message.success("Photo deleted!");
  599.           } catch {
  600.             message.error("Error deleting photo!");
  601.           }
  602.         }}
  603.       />
  604.     </Modal>
  605.   );
  606. }
  607.  
Advertisement
Add Comment
Please, Sign In to add comment