diff --git a/docs/FEATURES/user-profile-settings.md b/docs/FEATURES/user-profile-settings.md new file mode 100644 index 0000000..d98b6b0 --- /dev/null +++ b/docs/FEATURES/user-profile-settings.md @@ -0,0 +1,34 @@ +# Feature: User Profile Settings + +## Status + +Draft + +## Goal + +Allow authenticated users to manage the profile information shown inside the application shell and workspace activity. + +## User Stories + +- As an authenticated user, I want to update my name, alias, email, and portrait so that other workspace members see accurate profile information. + +## Frontend Areas + +- `/app/settings/user-information` +- `frontend/src/features/user-profile/` + +## Backend Modules + +- Identity + +## Domain Rules + +- Profile updates apply only to the authenticated user. +- Portrait uploads flow through the existing blob storage abstraction. +- Email changes use the identity module endpoint and should remain auditable through backend identity behavior. + +## Done When + +- [ ] User information settings show editable name, alias, and email fields. +- [ ] Portrait upload remains available from the settings page. +- [ ] Successful updates refresh the user profile state used by the app shell. diff --git a/docs/TASKS/user-profile-settings/001-edit-user-information.md b/docs/TASKS/user-profile-settings/001-edit-user-information.md new file mode 100644 index 0000000..9f22efe --- /dev/null +++ b/docs/TASKS/user-profile-settings/001-edit-user-information.md @@ -0,0 +1,23 @@ +# Task: Edit user information settings + +## Goal + +Allow users to edit their profile details from the user information settings page. + +## Feature Spec + +- `docs/FEATURES/user-profile-settings.md` + +## Scope + +- Replace read-only user information details with editable first name, last name, alias, and email fields. +- Keep portrait upload available on the page. +- Use the existing Identity endpoints for full name, alias, email, and portrait updates. +- Keep the profile store as the source of truth for app-shell user identity. + +## Validation + +```bash +cd frontend +npm run build +``` diff --git a/frontend/src/features/user-profile/stores/userProfileStore.js b/frontend/src/features/user-profile/stores/userProfileStore.js index e1b0a13..ff33688 100644 --- a/frontend/src/features/user-profile/stores/userProfileStore.js +++ b/frontend/src/features/user-profile/stores/userProfileStore.js @@ -1,4 +1,4 @@ -import {computed, watch} from 'vue' +import {computed, ref, watch} from 'vue' import {defineStore} from 'pinia' import {useAuthStore} from "@/features/auth/stores/authStore.js"; import {useClient} from "@/plugins/api.js"; @@ -9,6 +9,9 @@ export const useUserProfileStore = defineStore( () => { const authStore = useAuthStore() + const isUpdating = ref(false) + const isUploadingPortrait = ref(false) + const error = ref(null) const authWatcher = watch( () => authStore.isAuthenticated, @@ -64,12 +67,15 @@ export const useUserProfileStore = defineStore( const client = useClient() const userResponse = await client.get("/api/users/profile"); value.value = userResponse.data - } catch (error) { - console.error(error) + } catch (fetchError) { + console.error(fetchError) } } async function changeFullname(firstname, lastname) { + isUpdating.value = true + error.value = null + try { const client = useClient() await client.post( @@ -80,12 +86,19 @@ export const useUserProfileStore = defineStore( }) value.value.firstname = firstname; value.value.lastname = lastname; - } catch (error) { - console.error(error) + } catch (updateError) { + console.error(updateError) + error.value = 'Failed to update profile.' + throw updateError + } finally { + isUpdating.value = false } } async function changeAlias(alias) { + isUpdating.value = true + error.value = null + try { const client = useClient() await client.post( @@ -94,8 +107,12 @@ export const useUserProfileStore = defineStore( alias: alias }) value.value.alias = alias; - } catch (error) { - console.error(error) + } catch (updateError) { + console.error(updateError) + error.value = 'Failed to update profile.' + throw updateError + } finally { + isUpdating.value = false } } @@ -128,6 +145,9 @@ export const useUserProfileStore = defineStore( } async function changeEmail(email) { + isUpdating.value = true + error.value = null + try { const client = useClient() await client.post( @@ -136,8 +156,12 @@ export const useUserProfileStore = defineStore( email: email }) value.value.email = email; - } catch (error) { - console.error(error) + } catch (updateError) { + console.error(updateError) + error.value = 'Failed to update profile.' + throw updateError + } finally { + isUpdating.value = false } } @@ -156,6 +180,9 @@ export const useUserProfileStore = defineStore( } async function changePortrait(selectedFile) { + isUploadingPortrait.value = true + error.value = null + try { const client = useClient() const formData = new FormData(); @@ -166,8 +193,12 @@ export const useUserProfileStore = defineStore( formData) value.value.portraitUrl = `${response.data.blobUrl}?${Date.now()}` // the Date.now() is for cache-busting - } catch (error) { - console.error(error) + } catch (uploadError) { + console.error(uploadError) + error.value = 'Failed to update portrait.' + throw uploadError + } finally { + isUploadingPortrait.value = false } } @@ -176,6 +207,9 @@ export const useUserProfileStore = defineStore( alias, fullname, portraitUrl, + isUpdating, + isUploadingPortrait, + error, roles, persona, authorizedWorkspaceIds, diff --git a/frontend/src/features/user-profile/views/UserSettingsView.vue b/frontend/src/features/user-profile/views/UserSettingsView.vue index 4cf341c..33e923d 100644 --- a/frontend/src/features/user-profile/views/UserSettingsView.vue +++ b/frontend/src/features/user-profile/views/UserSettingsView.vue @@ -1,5 +1,5 @@ @@ -50,6 +115,7 @@ {{ t('userSettings.updatePortrait') }} @@ -62,20 +128,77 @@ {{ t('userSettings.accountDetailsDescription') }} - - - {{ t('userSettings.alias') }} - {{ alias }} - - - {{ t('userSettings.fullName') }} - {{ fullname }} - - - {{ t('userSettings.email') }} - {{ email }} - + + {{ settingsError }} + + + {{ settingsStatus }} + + + + + + {{ t('userSettings.firstname') }} + + + + + {{ t('userSettings.lastname') }} + + + + + {{ t('userSettings.alias') }} + + + + + {{ t('userSettings.email') }} + + + + + + + {{ userProfileStore.isUpdating ? t('common.saving') : t('userSettings.saveDetails') }} + + + @@ -107,8 +231,7 @@ .page-header p, .panel-heading span, .hero-identity span, - .hero-identity small, - .detail-row span { + .hero-identity small { @apply text-sm leading-6; color: #526178; } @@ -128,8 +251,7 @@ } .hero-identity strong, - .panel-heading strong, - .detail-row strong { + .panel-heading strong { color: #172033; } @@ -149,10 +271,45 @@ @apply grid gap-4 md:grid-cols-2; } - .detail-row { - @apply flex flex-col gap-1 rounded-[1.25rem] border p-4; + .form-stack { + @apply flex flex-col gap-4; + } + + .field { + @apply flex flex-col gap-2; + } + + .field span { + @apply text-sm font-semibold; + color: #172033; + } + + .field input { + @apply rounded-[1rem] border px-4 py-3 text-sm; background: #fffaf2; border-color: rgba(23, 32, 51, 0.08); + color: #172033; + } + + .field input:disabled { + opacity: 0.7; + } + + .form-actions { + @apply flex justify-end; + } + + .page-message { + @apply rounded-[1rem] border px-4 py-3 text-sm font-semibold; + background: rgba(15, 118, 110, 0.08); + border-color: rgba(15, 118, 110, 0.18); + color: #0f766e; + } + + .page-message.error { + background: rgba(185, 28, 28, 0.08); + border-color: rgba(185, 28, 28, 0.16); + color: #b91c1c; } .primary-button { @@ -160,4 +317,9 @@ background: #172033; color: #fffaf2; } + + .primary-button:disabled { + cursor: not-allowed; + opacity: 0.55; + } diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 2f4f028..427b7d3 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -310,14 +310,24 @@ "description": "Manage the portrait and account details shown inside the workspace.", "updatePortrait": "Update portrait", "accountDetails": "Account details", - "accountDetailsDescription": "Additional account editing fields can be added here next.", + "accountDetailsDescription": "Edit the profile details other workspace members see.", + "saveDetails": "Save details", + "saved": "Profile details saved", + "portraitSaved": "Portrait saved", "alias": "Alias", + "firstname": "First name", + "lastname": "Last name", "fullName": "Full name", "email": "Email", "noEmail": "No email set", "cropperTitle": "Update user portrait", "savePortrait": "Save portrait", - "choosePortrait": "Choose portrait" + "choosePortrait": "Choose portrait", + "errors": { + "emailRequired": "Email is required.", + "saveFailed": "Profile details could not be saved.", + "portraitFailed": "Portrait could not be saved." + } }, "workspaceSettings": { "eyebrow": "Settings", diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json index 98033ce..8994c0f 100644 --- a/frontend/src/locales/fr.json +++ b/frontend/src/locales/fr.json @@ -310,14 +310,24 @@ "description": "Gérez le portrait et les informations du compte affichés dans l'espace.", "updatePortrait": "Mettre à jour le portrait", "accountDetails": "Détails du compte", - "accountDetailsDescription": "Des champs supplémentaires d'édition du compte peuvent être ajoutés ici ensuite.", + "accountDetailsDescription": "Modifiez les informations de profil visibles par les autres membres.", + "saveDetails": "Enregistrer les détails", + "saved": "Informations de profil enregistrées", + "portraitSaved": "Portrait enregistré", "alias": "Alias", + "firstname": "Prénom", + "lastname": "Nom", "fullName": "Nom complet", "email": "Email", "noEmail": "Aucun email défini", "cropperTitle": "Mettre à jour le portrait utilisateur", "savePortrait": "Enregistrer le portrait", - "choosePortrait": "Choisir un portrait" + "choosePortrait": "Choisir un portrait", + "errors": { + "emailRequired": "L'email est requis.", + "saveFailed": "Les informations de profil n'ont pas pu être enregistrées.", + "portraitFailed": "Le portrait n'a pas pu être enregistré." + } }, "workspaceSettings": { "eyebrow": "Paramètres",