Merge pull request #1 from Novaloop-AG/feature/addadditionalfields

feat: improve profile view with additional fields and validation
This commit is contained in:
2025-04-11 20:33:53 +02:00
committed by GitHub
7 changed files with 412 additions and 53 deletions
@@ -32,6 +32,37 @@
} }
} }
/* Business contact field specific layouts */
.mx_ExtendedProfileSettings_address_row {
.mx_ExtendedProfileSettings_field_business_street {
flex: 1;
}
.mx_ExtendedProfileSettings_field_business_street_nr {
flex: 1;
}
}
.mx_ExtendedProfileSettings_postal_row {
.mx_ExtendedProfileSettings_field_business_plz {
flex: 1;
}
.mx_ExtendedProfileSettings_field_business_city {
flex: 1;
}
}
.mx_ExtendedProfileSettings_contact_row {
.mx_ExtendedProfileSettings_field_business_email {
flex: 1;
}
.mx_ExtendedProfileSettings_field_business_tel {
flex: 1;
}
}
.mx_ExtendedProfileSettings_buttons { .mx_ExtendedProfileSettings_buttons {
display: flex; display: flex;
gap: 8px; gap: 8px;
@@ -18,7 +18,6 @@ import { SdkContextClass } from "../../../contexts/SDKContext";
import { Container } from "../right_panel/UserInfo"; import { Container } from "../right_panel/UserInfo";
import InlineSpinner from "../elements/InlineSpinner"; import InlineSpinner from "../elements/InlineSpinner";
import { import {
EXT_PROFILE_KEY_ADDRESS,
EXT_PROFILE_KEY_FURTHER_INFO, EXT_PROFILE_KEY_FURTHER_INFO,
EXT_PROFILE_KEY_SPECIALIZATION, EXT_PROFILE_KEY_SPECIALIZATION,
EXT_PROFILE_KEY_TITLE, EXT_PROFILE_KEY_TITLE,
@@ -61,13 +60,11 @@ const ExtendedProfileView: React.FC<ExtendedProfileViewProps> = ({ userId }) =>
const [ const [
titleValue, titleValue,
specializationValue, specializationValue,
addressValue,
websiteValue, websiteValue,
furtherInfoValue, furtherInfoValue,
] = await Promise.all([ ] = await Promise.all([
userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_TITLE), userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_TITLE),
userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_SPECIALIZATION), userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_SPECIALIZATION),
userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_ADDRESS),
userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_WEBSITE), userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_WEBSITE),
userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_FURTHER_INFO), userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_FURTHER_INFO),
]); ]);
@@ -75,7 +72,6 @@ const ExtendedProfileView: React.FC<ExtendedProfileViewProps> = ({ userId }) =>
const data = { const data = {
title: titleValue as string | null, title: titleValue as string | null,
specialization: specializationValue as string | null, specialization: specializationValue as string | null,
address: addressValue as string | null,
website: websiteValue as string | null, website: websiteValue as string | null,
furtherInfo: furtherInfoValue as string | null, furtherInfo: furtherInfoValue as string | null,
}; };
@@ -15,7 +15,13 @@ import { _t } from "../../../languageHandler";
import { import {
EXT_PROFILE_KEY_TITLE, EXT_PROFILE_KEY_TITLE,
EXT_PROFILE_KEY_SPECIALIZATION, EXT_PROFILE_KEY_SPECIALIZATION,
EXT_PROFILE_KEY_ADDRESS, EXT_PROFILE_KEY_PRACTICE_NAME,
EXT_PROFILE_KEY_BUSINESS_STREET,
EXT_PROFILE_KEY_BUSINESS_STREET_NR,
EXT_PROFILE_KEY_BUSINESS_CITY,
EXT_PROFILE_KEY_BUSINESS_PLZ,
EXT_PROFILE_KEY_BUSINESS_EMAIL,
EXT_PROFILE_KEY_BUSINESS_TEL,
EXT_PROFILE_KEY_WEBSITE, EXT_PROFILE_KEY_WEBSITE,
EXT_PROFILE_KEY_FURTHER_INFO, EXT_PROFILE_KEY_FURTHER_INFO,
} from "../settings/ExtendedProfileSettings"; } from "../settings/ExtendedProfileSettings";
@@ -41,7 +47,13 @@ const UserExtendedProfileInfo: React.FC<UserExtendedProfileInfoProps> = ({ userI
const [title, setTitle] = useState<string>(""); const [title, setTitle] = useState<string>("");
const [specialization, setSpecialization] = useState<string>(""); const [specialization, setSpecialization] = useState<string>("");
const [address, setAddress] = useState<string>(""); const [practiceName, setPracticeName] = useState<string>("");
const [businessStreet, setBusinessStreet] = useState<string>("");
const [businessStreetNr, setBusinessStreetNr] = useState<string>("");
const [businessCity, setBusinessCity] = useState<string>("");
const [businessPlz, setBusinessPlz] = useState<string>("");
const [businessEmail, setBusinessEmail] = useState<string>("");
const [businessTel, setBusinessTel] = useState<string>("");
const [website, setWebsite] = useState<string>(""); const [website, setWebsite] = useState<string>("");
const [furtherInfo, setFurtherInfo] = useState<string>(""); const [furtherInfo, setFurtherInfo] = useState<string>("");
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
@@ -55,13 +67,25 @@ const UserExtendedProfileInfo: React.FC<UserExtendedProfileInfoProps> = ({ userI
const titleValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_TITLE); const titleValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_TITLE);
const specializationValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_SPECIALIZATION); const specializationValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_SPECIALIZATION);
const addressValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_ADDRESS); const practiceNameValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_PRACTICE_NAME);
const businessStreetValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_STREET);
const businessStreetNrValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_STREET_NR);
const businessCityValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_CITY);
const businessPlzValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_PLZ);
const businessEmailValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_EMAIL);
const businessTelValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_TEL);
const websiteValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_WEBSITE); const websiteValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_WEBSITE);
const furtherInfoValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_FURTHER_INFO); const furtherInfoValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_FURTHER_INFO);
setTitle(titleValue as string || ""); setTitle(titleValue as string || "");
setSpecialization(specializationValue as string || ""); setSpecialization(specializationValue as string || "");
setAddress(addressValue as string || ""); setPracticeName(practiceNameValue as string || "");
setBusinessStreet(businessStreetValue as string || "");
setBusinessStreetNr(businessStreetNrValue as string || "");
setBusinessCity(businessCityValue as string || "");
setBusinessPlz(businessPlzValue as string || "");
setBusinessEmail(businessEmailValue as string || "");
setBusinessTel(businessTelValue as string || "");
setWebsite(websiteValue as string || ""); setWebsite(websiteValue as string || "");
setFurtherInfo(furtherInfoValue as string || ""); setFurtherInfo(furtherInfoValue as string || "");
} catch { } catch {
@@ -83,15 +107,12 @@ const UserExtendedProfileInfo: React.FC<UserExtendedProfileInfoProps> = ({ userI
} }
// Don't render anything if there's no data // Don't render anything if there's no data
if (!title && !specialization && !address && !website && !furtherInfo) { if (!title && !specialization && !practiceName && !businessStreet && !businessStreetNr && !businessCity && !businessPlz && !businessEmail && !businessTel && !website && !furtherInfo) {
return null; return null;
} }
return ( return (
<div className="mx_UserInfo_container mx_UserInfo_profileInfo"> <div className="mx_UserInfo_container mx_UserInfo_profileInfo">
{/* <div className="mx_UserInfo_container_header">
<Text weight="semibold">{_t("user_info|extended_profile|header")}</Text>
</div> */}
{title && ( {title && (
<div className="mx_UserInfo_profileField"> <div className="mx_UserInfo_profileField">
<div className="mx_UserInfo_fieldLabel"> <div className="mx_UserInfo_fieldLabel">
@@ -116,7 +137,19 @@ const UserExtendedProfileInfo: React.FC<UserExtendedProfileInfoProps> = ({ userI
</div> </div>
</div> </div>
)} )}
{address && ( {practiceName && (
<div className="mx_UserInfo_profileField">
<div className="mx_UserInfo_fieldLabel">
<Text size="sm" weight="semibold">
{_t("user_info|extended_profile|practice_name")}
</Text>
</div>
<div className="mx_UserInfo_fieldValue">
<Text size="sm">{practiceName}</Text>
</div>
</div>
)}
{businessStreet && businessCity && (
<div className="mx_UserInfo_profileField"> <div className="mx_UserInfo_profileField">
<div className="mx_UserInfo_fieldLabel"> <div className="mx_UserInfo_fieldLabel">
<Text size="sm" weight="semibold"> <Text size="sm" weight="semibold">
@@ -124,7 +157,39 @@ const UserExtendedProfileInfo: React.FC<UserExtendedProfileInfoProps> = ({ userI
</Text> </Text>
</div> </div>
<div className="mx_UserInfo_fieldValue"> <div className="mx_UserInfo_fieldValue">
<Text size="sm">{renderMultilineText(address)}</Text> <Text size="sm">
{[businessStreet, businessStreetNr].filter(Boolean).join(" ")}
{(businessStreet || businessStreetNr) && (businessPlz || businessCity) && ", "}
{[businessPlz, businessCity].filter(Boolean).join(" ")}
</Text>
</div>
</div>
)}
{businessEmail && (
<div className="mx_UserInfo_profileField">
<div className="mx_UserInfo_fieldLabel">
<Text size="sm" weight="semibold">
{_t("user_info|extended_profile|business_email")}
</Text>
</div>
<div className="mx_UserInfo_fieldValue">
<Text size="sm">
<a href={`mailto:${businessEmail}`}>{businessEmail}</a>
</Text>
</div>
</div>
)}
{businessTel && (
<div className="mx_UserInfo_profileField">
<div className="mx_UserInfo_fieldLabel">
<Text size="sm" weight="semibold">
{_t("user_info|extended_profile|business_tel")}
</Text>
</div>
<div className="mx_UserInfo_fieldValue">
<Text size="sm">
<a href={`tel:${businessTel}`}>{businessTel}</a>
</Text>
</div> </div>
</div> </div>
)} )}
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
import React, { type ChangeEvent, useCallback, useEffect, useState } from "react"; import React, { type ChangeEvent, useCallback, useEffect, useState } from "react";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { Alert, ErrorMessage } from "@vector-im/compound-web"; import { Alert } from "@vector-im/compound-web";
import classNames from "classnames"; import classNames from "classnames";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
@@ -28,9 +28,15 @@ const SpinnerToast: React.FC<{ children?: React.ReactNode }> = ({ children }) =>
// Extended profile property keys // Extended profile property keys
export const EXT_PROFILE_KEY_TITLE = "io.element.profile.title"; export const EXT_PROFILE_KEY_TITLE = "io.element.profile.title";
export const EXT_PROFILE_KEY_SPECIALIZATION = "io.element.profile.specialization"; export const EXT_PROFILE_KEY_SPECIALIZATION = "io.element.profile.specialization";
export const EXT_PROFILE_KEY_ADDRESS = "io.element.profile.address";
export const EXT_PROFILE_KEY_WEBSITE = "io.element.profile.website"; export const EXT_PROFILE_KEY_WEBSITE = "io.element.profile.website";
export const EXT_PROFILE_KEY_FURTHER_INFO = "io.element.profile.further_info"; export const EXT_PROFILE_KEY_FURTHER_INFO = "io.element.profile.further_info";
export const EXT_PROFILE_KEY_PRACTICE_NAME = "io.element.profile.practice_name";
export const EXT_PROFILE_KEY_BUSINESS_STREET = "io.element.profile.business_street";
export const EXT_PROFILE_KEY_BUSINESS_STREET_NR = "io.element.profile.business_street_nr";
export const EXT_PROFILE_KEY_BUSINESS_CITY = "io.element.profile.business_city";
export const EXT_PROFILE_KEY_BUSINESS_PLZ = "io.element.profile.business_plz";
export const EXT_PROFILE_KEY_BUSINESS_EMAIL = "io.element.profile.business_email";
export const EXT_PROFILE_KEY_BUSINESS_TEL = "io.element.profile.business_tel";
// Unstable feature key for extended profiles // Unstable feature key for extended profiles
export const UNSTABLE_MSC4133_EXTENDED_PROFILES = "org.matrix.msc4133.extended_profiles"; export const UNSTABLE_MSC4133_EXTENDED_PROFILES = "org.matrix.msc4133.extended_profiles";
@@ -41,21 +47,41 @@ interface ExtendedProfileSettingsProps {
/** /**
* Component for managing extended profile fields for medical professionals. * Component for managing extended profile fields for medical professionals.
* Allows editing title, specialization, address, website, and further information. * Allows editing title, specialization, website, and further information.
*/ */
const ExtendedProfileSettings: React.FC<ExtendedProfileSettingsProps> = ({ className }) => { const ExtendedProfileSettings: React.FC<ExtendedProfileSettingsProps> = ({ className }) => {
const [title, setTitle] = useState<string>(""); const [title, setTitle] = useState<string>("");
const [specialization, setSpecialization] = useState<string>(""); const [specialization, setSpecialization] = useState<string>("");
const [address, setAddress] = useState<string>("");
const [website, setWebsite] = useState<string>(""); const [website, setWebsite] = useState<string>("");
const [furtherInfo, setFurtherInfo] = useState<string>(""); const [furtherInfo, setFurtherInfo] = useState<string>("");
const [practiceName, setPracticeName] = useState<string>("");
// Business contact field states
const [businessStreet, setBusinessStreet] = useState<string>("");
const [businessStreetNr, setBusinessStreetNr] = useState<string>("");
const [businessCity, setBusinessCity] = useState<string>("");
const [businessPlz, setBusinessPlz] = useState<string>("");
const [businessEmail, setBusinessEmail] = useState<string>("");
const [businessTel, setBusinessTel] = useState<string>("");
// Validation states for email and phone
const [businessEmailValid, setBusinessEmailValid] = useState<boolean>(true);
const [businessTelValid, setBusinessTelValid] = useState<boolean>(true);
// Store original values for cancel functionality // Store original values for cancel functionality
const [originalTitle, setOriginalTitle] = useState<string>(""); const [originalTitle, setOriginalTitle] = useState<string>("");
const [originalSpecialization, setOriginalSpecialization] = useState<string>(""); const [originalSpecialization, setOriginalSpecialization] = useState<string>("");
const [originalAddress, setOriginalAddress] = useState<string>("");
const [originalWebsite, setOriginalWebsite] = useState<string>(""); const [originalWebsite, setOriginalWebsite] = useState<string>("");
const [originalFurtherInfo, setOriginalFurtherInfo] = useState<string>(""); const [originalFurtherInfo, setOriginalFurtherInfo] = useState<string>("");
const [originalPracticeName, setOriginalPracticeName] = useState<string>("");
// Original business contact field values
const [originalBusinessStreet, setOriginalBusinessStreet] = useState<string>("");
const [originalBusinessStreetNr, setOriginalBusinessStreetNr] = useState<string>("");
const [originalBusinessCity, setOriginalBusinessCity] = useState<string>("");
const [originalBusinessPlz, setOriginalBusinessPlz] = useState<string>("");
const [originalBusinessEmail, setOriginalBusinessEmail] = useState<string>("");
const [originalBusinessTel, setOriginalBusinessTel] = useState<string>("");
const [isModified, setIsModified] = useState<boolean>(false); const [isModified, setIsModified] = useState<boolean>(false);
const [isSaving, setIsSaving] = useState<boolean>(false); const [isSaving, setIsSaving] = useState<boolean>(false);
@@ -76,28 +102,60 @@ const ExtendedProfileSettings: React.FC<ExtendedProfileSettingsProps> = ({ class
try { try {
const titleValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_TITLE); const titleValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_TITLE);
const specializationValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_SPECIALIZATION); const specializationValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_SPECIALIZATION);
const addressValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_ADDRESS);
const websiteValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_WEBSITE); const websiteValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_WEBSITE);
const furtherInfoValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_FURTHER_INFO); const furtherInfoValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_FURTHER_INFO);
const practiceNameValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_PRACTICE_NAME);
// Fetch business contact fields
const businessStreetValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_STREET);
const businessStreetNrValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_STREET_NR);
const businessCityValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_CITY);
const businessPlzValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_PLZ);
const businessEmailValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_EMAIL);
const businessTelValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_TEL);
const titleStr = titleValue as string || ""; const titleStr = titleValue as string || "";
const specializationStr = specializationValue as string || ""; const specializationStr = specializationValue as string || "";
const addressStr = addressValue as string || "";
const websiteStr = websiteValue as string || ""; const websiteStr = websiteValue as string || "";
const furtherInfoStr = furtherInfoValue as string || ""; const furtherInfoStr = furtherInfoValue as string || "";
const practiceNameStr = practiceNameValue as string || "";
// Convert business contact field values to strings
const businessStreetStr = businessStreetValue as string || "";
const businessStreetNrStr = businessStreetNrValue as string || "";
const businessCityStr = businessCityValue as string || "";
const businessPlzStr = businessPlzValue as string || "";
const businessEmailStr = businessEmailValue as string || "";
const businessTelStr = businessTelValue as string || "";
setTitle(titleStr); setTitle(titleStr);
setSpecialization(specializationStr); setSpecialization(specializationStr);
setAddress(addressStr);
setWebsite(websiteStr); setWebsite(websiteStr);
setFurtherInfo(furtherInfoStr); setFurtherInfo(furtherInfoStr);
setPracticeName(practiceNameStr);
// Set business contact fields
setBusinessStreet(businessStreetStr);
setBusinessStreetNr(businessStreetNrStr);
setBusinessCity(businessCityStr);
setBusinessPlz(businessPlzStr);
setBusinessEmail(businessEmailStr);
setBusinessTel(businessTelStr);
// Store original values for cancel functionality // Store original values for cancel functionality
setOriginalTitle(titleStr); setOriginalTitle(titleStr);
setOriginalSpecialization(specializationStr); setOriginalSpecialization(specializationStr);
setOriginalAddress(addressStr);
setOriginalWebsite(websiteStr); setOriginalWebsite(websiteStr);
setOriginalFurtherInfo(furtherInfoStr); setOriginalFurtherInfo(furtherInfoStr);
setOriginalPracticeName(practiceNameStr);
// Store original business contact field values
setOriginalBusinessStreet(businessStreetStr);
setOriginalBusinessStreetNr(businessStreetNrStr);
setOriginalBusinessCity(businessCityStr);
setOriginalBusinessPlz(businessPlzStr);
setOriginalBusinessEmail(businessEmailStr);
setOriginalBusinessTel(businessTelStr);
} catch (e) { } catch (e) {
// Just log the error but don't set loadError to true // Just log the error but don't set loadError to true
// as we want the fields to show up even when empty // as we want the fields to show up even when empty
@@ -117,13 +175,24 @@ const ExtendedProfileSettings: React.FC<ExtendedProfileSettingsProps> = ({ class
const hasChanges = const hasChanges =
title !== originalTitle || title !== originalTitle ||
specialization !== originalSpecialization || specialization !== originalSpecialization ||
address !== originalAddress ||
website !== originalWebsite || website !== originalWebsite ||
furtherInfo !== originalFurtherInfo; furtherInfo !== originalFurtherInfo ||
practiceName !== originalPracticeName ||
businessStreet !== originalBusinessStreet ||
businessStreetNr !== originalBusinessStreetNr ||
businessCity !== originalBusinessCity ||
businessPlz !== originalBusinessPlz ||
businessEmail !== originalBusinessEmail ||
businessTel !== originalBusinessTel;
setIsModified(hasChanges); setIsModified(hasChanges);
}, [title, specialization, address, website, furtherInfo, }, [
originalTitle, originalSpecialization, originalAddress, originalWebsite, originalFurtherInfo]); title, specialization, website, furtherInfo, practiceName,
originalTitle, originalSpecialization, originalWebsite, originalFurtherInfo, originalPracticeName,
businessStreet, businessStreetNr, businessCity, businessPlz, businessEmail, businessTel,
originalBusinessStreet, originalBusinessStreetNr, originalBusinessCity, originalBusinessPlz,
originalBusinessEmail, originalBusinessTel
]);
// Field change handlers // Field change handlers
const onTitleChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => { const onTitleChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
@@ -134,10 +203,6 @@ const ExtendedProfileSettings: React.FC<ExtendedProfileSettingsProps> = ({ class
setSpecialization(e.target.value); setSpecialization(e.target.value);
}, []); }, []);
const onAddressChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setAddress(e.target.value);
}, []);
const onWebsiteChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => { const onWebsiteChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setWebsite(e.target.value); setWebsite(e.target.value);
}, []); }, []);
@@ -146,19 +211,80 @@ const ExtendedProfileSettings: React.FC<ExtendedProfileSettingsProps> = ({ class
setFurtherInfo(e.target.value); setFurtherInfo(e.target.value);
}, []); }, []);
const onPracticeNameChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setPracticeName(e.target.value);
}, []);
// Business contact field change handlers
const onBusinessStreetChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setBusinessStreet(e.target.value);
}, []);
const onBusinessStreetNrChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setBusinessStreetNr(e.target.value);
}, []);
const onBusinessCityChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setBusinessCity(e.target.value);
}, []);
const onBusinessPlzChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setBusinessPlz(e.target.value);
}, []);
// Email validation with simple regex
const onBusinessEmailChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setBusinessEmail(value);
// Email format validation - basic check for something@something.something
const emailValid = value === "" || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
setBusinessEmailValid(emailValid);
}, []);
// Phone validation
const onBusinessTelChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setBusinessTel(value);
// Phone format validation - allow digits, spaces, plus, minus, parentheses
const phoneValid = value === "" || /^[0-9+\-() ]+$/.test(value);
setBusinessTelValid(phoneValid);
}, []);
// Cancel handler - restore all original values // Cancel handler - restore all original values
const onCancel = useCallback(() => { const onCancel = useCallback(() => {
setTitle(originalTitle); setTitle(originalTitle);
setSpecialization(originalSpecialization); setSpecialization(originalSpecialization);
setAddress(originalAddress);
setWebsite(originalWebsite); setWebsite(originalWebsite);
setFurtherInfo(originalFurtherInfo); setFurtherInfo(originalFurtherInfo);
}, [originalTitle, originalSpecialization, originalAddress, originalWebsite, originalFurtherInfo]); setPracticeName(originalPracticeName);
// Reset business contact fields to original values
setBusinessStreet(originalBusinessStreet);
setBusinessStreetNr(originalBusinessStreetNr);
setBusinessCity(originalBusinessCity);
setBusinessPlz(originalBusinessPlz);
setBusinessEmail(originalBusinessEmail);
setBusinessTel(originalBusinessTel);
// Reset validation states
setBusinessEmailValid(true);
setBusinessTelValid(true);
}, [
originalTitle, originalSpecialization, originalWebsite, originalFurtherInfo, originalPracticeName,
originalBusinessStreet, originalBusinessStreetNr, originalBusinessCity, originalBusinessPlz,
originalBusinessEmail, originalBusinessTel
]);
// Save handler - save all fields at once // Save handler - save all fields at once
const onSave = useCallback(async (): Promise<void> => { const onSave = useCallback(async (): Promise<void> => {
if (!isModified) return; if (!isModified) return;
// Validate email and phone fields before saving
if (!businessEmailValid || !businessTelValid) {
setSaveError(true);
return;
}
setIsSaving(true); setIsSaving(true);
setSaveError(false); setSaveError(false);
const removeToast = toastRack.displayToast( const removeToast = toastRack.displayToast(
@@ -177,10 +303,6 @@ const ExtendedProfileSettings: React.FC<ExtendedProfileSettingsProps> = ({ class
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_SPECIALIZATION, specialization)); savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_SPECIALIZATION, specialization));
} }
if (address !== originalAddress) {
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_ADDRESS, address));
}
if (website !== originalWebsite) { if (website !== originalWebsite) {
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_WEBSITE, website)); savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_WEBSITE, website));
} }
@@ -189,14 +311,51 @@ const ExtendedProfileSettings: React.FC<ExtendedProfileSettingsProps> = ({ class
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_FURTHER_INFO, furtherInfo)); savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_FURTHER_INFO, furtherInfo));
} }
if (practiceName !== originalPracticeName) {
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_PRACTICE_NAME, practiceName));
}
// Save business contact fields if modified
if (businessStreet !== originalBusinessStreet) {
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_BUSINESS_STREET, businessStreet));
}
if (businessStreetNr !== originalBusinessStreetNr) {
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_BUSINESS_STREET_NR, businessStreetNr));
}
if (businessCity !== originalBusinessCity) {
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_BUSINESS_CITY, businessCity));
}
if (businessPlz !== originalBusinessPlz) {
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_BUSINESS_PLZ, businessPlz));
}
if (businessEmail !== originalBusinessEmail) {
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_BUSINESS_EMAIL, businessEmail));
}
if (businessTel !== originalBusinessTel) {
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_BUSINESS_TEL, businessTel));
}
await Promise.all(savePromises); await Promise.all(savePromises);
// Update original values after successful save // Update original values after successful save
setOriginalTitle(title); setOriginalTitle(title);
setOriginalSpecialization(specialization); setOriginalSpecialization(specialization);
setOriginalAddress(address);
setOriginalWebsite(website); setOriginalWebsite(website);
setOriginalFurtherInfo(furtherInfo); setOriginalFurtherInfo(furtherInfo);
setOriginalPracticeName(practiceName);
// Update original business contact field values
setOriginalBusinessStreet(businessStreet);
setOriginalBusinessStreetNr(businessStreetNr);
setOriginalBusinessCity(businessCity);
setOriginalBusinessPlz(businessPlz);
setOriginalBusinessEmail(businessEmail);
setOriginalBusinessTel(businessTel);
} catch (e) { } catch (e) {
logger.error("Failed to save profile information", e); logger.error("Failed to save profile information", e);
@@ -207,8 +366,11 @@ const ExtendedProfileSettings: React.FC<ExtendedProfileSettingsProps> = ({ class
removeToast(); removeToast();
} }
}, [ }, [
isModified, title, specialization, address, website, furtherInfo, isModified, title, specialization, website, furtherInfo, practiceName,
originalTitle, originalSpecialization, originalAddress, originalWebsite, originalFurtherInfo, originalTitle, originalSpecialization, originalWebsite, originalFurtherInfo, originalPracticeName,
businessStreet, businessStreetNr, businessCity, businessPlz, businessEmail, businessTel,
originalBusinessStreet, originalBusinessStreetNr, originalBusinessCity, originalBusinessPlz,
originalBusinessEmail, originalBusinessTel, businessEmailValid, businessTelValid,
toastRack, userProfilesStore toastRack, userProfilesStore
]); ]);
@@ -248,14 +410,85 @@ const ExtendedProfileSettings: React.FC<ExtendedProfileSettingsProps> = ({ class
<div className="mx_ExtendedProfileSettings_field"> <div className="mx_ExtendedProfileSettings_field">
<Field <Field
label={_t("settings|extended_profile_settings|address")} label={_t("settings|extended_profile_settings|practice_name")}
value={address} value={practiceName}
onChange={onAddressChanged} onChange={onPracticeNameChanged}
disabled={isSaving} disabled={isSaving}
autoComplete="off" autoComplete="off"
/> />
</div> </div>
{/* Street and number in one row, street takes 2/3 width */}
<div className="mx_ExtendedProfileSettings_fieldsRow mx_ExtendedProfileSettings_address_row">
<div className="mx_ExtendedProfileSettings_field mx_ExtendedProfileSettings_field_business_street">
<Field
label={_t("settings|extended_profile_settings|business_street")}
value={businessStreet}
onChange={onBusinessStreetChanged}
disabled={isSaving}
autoComplete="off"
/>
</div>
<div className="mx_ExtendedProfileSettings_field mx_ExtendedProfileSettings_field_business_street_nr">
<Field
label={_t("settings|extended_profile_settings|business_street_nr")}
value={businessStreetNr}
onChange={onBusinessStreetNrChanged}
disabled={isSaving}
autoComplete="off"
/>
</div>
</div>
{/* PLZ and City in one row, PLZ takes 1/3 width */}
<div className="mx_ExtendedProfileSettings_fieldsRow mx_ExtendedProfileSettings_postal_row">
<div className="mx_ExtendedProfileSettings_field mx_ExtendedProfileSettings_field_business_plz">
<Field
label={_t("settings|extended_profile_settings|business_plz")}
value={businessPlz}
onChange={onBusinessPlzChanged}
disabled={isSaving}
autoComplete="off"
/>
</div>
<div className="mx_ExtendedProfileSettings_field mx_ExtendedProfileSettings_field_business_city">
<Field
label={_t("settings|extended_profile_settings|business_city")}
value={businessCity}
onChange={onBusinessCityChanged}
disabled={isSaving}
autoComplete="off"
/>
</div>
</div>
{/* Email field with validation */}
<div className="mx_ExtendedProfileSettings_fieldsRow mx_ExtendedProfileSettings_contact_row">
<div className="mx_ExtendedProfileSettings_field">
<Field
label={_t("settings|extended_profile_settings|business_email")}
value={businessEmail}
onChange={onBusinessEmailChanged}
disabled={isSaving}
autoComplete="off"
type="email"
/>
</div>
{/* Telephone field with validation */}
<div className="mx_ExtendedProfileSettings_field">
<Field
label={_t("settings|extended_profile_settings|business_tel")}
value={businessTel}
onChange={onBusinessTelChanged}
disabled={isSaving}
autoComplete="off"
type="tel"
/>
</div>
</div>
<div className="mx_ExtendedProfileSettings_field"> <div className="mx_ExtendedProfileSettings_field">
<Field <Field
label={_t("settings|extended_profile_settings|website")} label={_t("settings|extended_profile_settings|website")}
@@ -288,11 +521,12 @@ const ExtendedProfileSettings: React.FC<ExtendedProfileSettingsProps> = ({ class
</AccessibleButton> </AccessibleButton>
</div> </div>
</div> </div>
{saveError && ( {saveError && (
<div className="mx_ExtendedProfileSettings_error"> <Alert title={_t("settings|extended_profile_settings|failed_to_save")} type="critical">
<ErrorMessage>{_t("settings|extended_profile_settings|failed_to_save")}</ErrorMessage> {businessEmailValid ? null : _t("settings|extended_profile_settings|invalid_email")}
</div> <span> </span>
{businessTelValid ? null : _t("settings|extended_profile_settings|invalid_phone")}
</Alert>
)} )}
</div> </div>
</div> </div>
+13 -2
View File
@@ -2487,13 +2487,21 @@
"additional_information": "Zusätzliche Informationen", "additional_information": "Zusätzliche Informationen",
"professional_title": "Titel", "professional_title": "Titel",
"specialization": "Fachgebiet", "specialization": "Fachgebiet",
"address": "Adresse", "practice_name": "Praxisname / Firmenname",
"business_street": "Strasse",
"business_street_nr": "Hausnummer",
"business_plz": "PLZ",
"business_city": "Ort",
"business_email": "Email (geschäftlich)",
"business_tel": "Telefon (geschäftlich)",
"website": "Webseite", "website": "Webseite",
"further_information": "Weitere Informationen / Qualifikationen", "further_information": "Weitere Informationen / Qualifikationen",
"saving_profile": "Profil wird gespeichert...", "saving_profile": "Profil wird gespeichert...",
"failed_to_load": "Fehler beim Laden der erweiterten Profileinstellungen.", "failed_to_load": "Fehler beim Laden der erweiterten Profileinstellungen.",
"failed_to_save": "Fehler beim Speichern der erweiterten Profileinstellungen.", "failed_to_save": "Fehler beim Speichern der erweiterten Profileinstellungen.",
"server_not_supported": "Dieser Server unterstützt keine erweiterten Profileinstellungen." "server_not_supported": "Dieser Server unterstützt keine erweiterten Profileinstellungen.",
"invalid_email": "Ungültige E-Mail-Adresse.",
"invalid_phone": "Ungültige Telefonnummer."
}, },
"account": { "account": {
"dialog_title": "<strong>Einstellungen:</strong> Konto", "dialog_title": "<strong>Einstellungen:</strong> Konto",
@@ -3881,7 +3889,10 @@
"header": "Weitere Informationen", "header": "Weitere Informationen",
"title": "Titel", "title": "Titel",
"specialization": "Fachgebiet", "specialization": "Fachgebiet",
"practice_name": "Praxisname / Firmenname",
"address": "Adresse", "address": "Adresse",
"business_email": "E-Mail (geschäftlich)",
"business_tel": "Telefon (geschäftlich)",
"website": "Webseite", "website": "Webseite",
"further_info": "Weitere Infos / Qualifikationen", "further_info": "Weitere Infos / Qualifikationen",
"not_provided": "Nicht angegeben", "not_provided": "Nicht angegeben",
+13 -2
View File
@@ -2491,13 +2491,21 @@
"additional_information": "Additional Information", "additional_information": "Additional Information",
"professional_title": "Title", "professional_title": "Title",
"specialization": "Specialization", "specialization": "Specialization",
"address": "Address", "practice_name": "Practice Name",
"business_street": "Strasse",
"business_street_nr": "Street Number",
"business_plz": "ZIP Code",
"business_city": "City",
"business_email": "Business Email",
"business_tel": "Business Phone",
"website": "Website", "website": "Website",
"further_information": "Further Information / Qualifications", "further_information": "Further Information / Qualifications",
"saving_profile": "Saving profile...", "saving_profile": "Saving profile...",
"failed_to_load": "Failed to load extended profile settings.", "failed_to_load": "Failed to load extended profile settings.",
"failed_to_save": "Failed to save extended profile settings.", "failed_to_save": "Failed to save extended profile settings.",
"server_not_supported": "Your server does not support extended profile settings." "server_not_supported": "Your server does not support extended profile settings.",
"invalid_email": "Invalid email.",
"invalid_phone": "Invalid phone number."
}, },
"account": { "account": {
"dialog_title": "<strong>Settings:</strong> Account", "dialog_title": "<strong>Settings:</strong> Account",
@@ -3885,7 +3893,10 @@
"header": "Additional infos", "header": "Additional infos",
"title": "Title", "title": "Title",
"specialization": "Speciality", "specialization": "Speciality",
"practice_name": "Practice name",
"address": "Address", "address": "Address",
"business_email": "Business email",
"business_tel": "Business phone",
"website": "Website", "website": "Website",
"further_info": "Further infos / qualifications", "further_info": "Further infos / qualifications",
"not_provided": "Not provided", "not_provided": "Not provided",
+13 -2
View File
@@ -2487,13 +2487,21 @@
"additional_information": "Informations supplémentaires", "additional_information": "Informations supplémentaires",
"professional_title": "Titre", "professional_title": "Titre",
"specialization": "Spécialisation", "specialization": "Spécialisation",
"address": "Adresse", "practice_name": "Nom de l’entreprise",
"business_street": "Rue",
"business_street_nr": "Numéro de rue",
"business_plz": "Code postal",
"business_city": "Ville",
"business_email": "E-mail professionnel",
"business_tel": "Téléphone professionnel",
"website": "Site web", "website": "Site web",
"further_information": "Informations supplémentaires / qualifications", "further_information": "Informations supplémentaires / qualifications",
"saving_profile": "Enregistrement des paramètres de profil étendus…", "saving_profile": "Enregistrement des paramètres de profil étendus…",
"failed_to_load": "Erreur lors du chargement des paramètres de profil étendus.", "failed_to_load": "Erreur lors du chargement des paramètres de profil étendus.",
"failed_to_save": "Erreur lors de l'enregistrement des paramètres de profil étendus.", "failed_to_save": "Erreur lors de l'enregistrement des paramètres de profil étendus.",
"server_not_supported": "Le serveur d’accueil ne prend pas en charge les paramètres de profil étendus." "server_not_supported": "Le serveur d’accueil ne prend pas en charge les paramètres de profil étendus.",
"invalid_email": "Adresse e-mail invalide.",
"invalid_phone": "Numéro de téléphone invalide."
}, },
"account": { "account": {
"dialog_title": "<strong>Paramètres : </strong> Compte", "dialog_title": "<strong>Paramètres : </strong> Compte",
@@ -3881,7 +3889,10 @@
"header": "Informations supplémentaires", "header": "Informations supplémentaires",
"title": "Titre", "title": "Titre",
"specialization": "Spécialisation", "specialization": "Spécialisation",
"practice_name": "Nom de l’entreprise",
"address": "Adresse", "address": "Adresse",
"business_email": "Email (professionnel)",
"business_tel": "Téléphone (professionnel)",
"website": "Site web", "website": "Site web",
"further_info": "Informations supplémentaires / qualifications", "further_info": "Informations supplémentaires / qualifications",
"not_provided": "Non fourni", "not_provided": "Non fourni",