From 157b4c6ec39f7753654e353b79902eb77d926807 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Sun, 29 Mar 2026 13:31:23 +0200 Subject: [PATCH] feat(tenancy): add profile editing in merchant team edit modal Edit modal now has editable first_name, last_name, email fields with a "Save Profile" button, alongside the existing per-store role management. New: - PUT /merchants/account/team/members/{user_id}/profile endpoint - MerchantTeamProfileUpdate schema - update_team_member_profile() service method with ownership validation - 2 new i18n keys across 4 locales (personal_info, save_profile) Co-Authored-By: Claude Opus 4.6 (1M context) --- app/modules/tenancy/locales/de.json | 2 + app/modules/tenancy/locales/en.json | 2 + app/modules/tenancy/locales/fr.json | 2 + app/modules/tenancy/locales/lb.json | 2 + app/modules/tenancy/routes/api/merchant.py | 25 ++++++++- app/modules/tenancy/schemas/team.py | 8 +++ .../services/merchant_store_service.py | 53 +++++++++++++++++++ .../static/merchant/js/merchant-team.js | 25 +++++++++ .../templates/tenancy/merchant/team.html | 43 ++++++++++----- 9 files changed, 147 insertions(+), 15 deletions(-) diff --git a/app/modules/tenancy/locales/de.json b/app/modules/tenancy/locales/de.json index 081cf1f1..f132713b 100644 --- a/app/modules/tenancy/locales/de.json +++ b/app/modules/tenancy/locales/de.json @@ -50,6 +50,8 @@ "stores_and_roles": "Filialen & Rollen", "title": "Team", "total_members": "Mitglieder gesamt", + "personal_info": "Persönliche Informationen", + "save_profile": "Profil speichern", "view_member": "Mitglied anzeigen", "account_information": "Kontoinformationen", "username": "Benutzername", diff --git a/app/modules/tenancy/locales/en.json b/app/modules/tenancy/locales/en.json index 1977fcb3..b98cdde0 100644 --- a/app/modules/tenancy/locales/en.json +++ b/app/modules/tenancy/locales/en.json @@ -50,6 +50,8 @@ "stores_and_roles": "Stores & Roles", "title": "Team", "total_members": "Total Members", + "personal_info": "Personal Information", + "save_profile": "Save Profile", "view_member": "View Member", "account_information": "Account Information", "username": "Username", diff --git a/app/modules/tenancy/locales/fr.json b/app/modules/tenancy/locales/fr.json index 5c01af51..a7f54483 100644 --- a/app/modules/tenancy/locales/fr.json +++ b/app/modules/tenancy/locales/fr.json @@ -50,6 +50,8 @@ "stores_and_roles": "Magasins et rôles", "title": "Équipe", "total_members": "Membres totaux", + "personal_info": "Informations personnelles", + "save_profile": "Enregistrer le profil", "view_member": "Voir le membre", "account_information": "Informations du compte", "username": "Nom d'utilisateur", diff --git a/app/modules/tenancy/locales/lb.json b/app/modules/tenancy/locales/lb.json index da0b4c53..8ccd5a08 100644 --- a/app/modules/tenancy/locales/lb.json +++ b/app/modules/tenancy/locales/lb.json @@ -50,6 +50,8 @@ "stores_and_roles": "Geschäfter & Rollen", "title": "Team", "total_members": "Memberen total", + "personal_info": "Perséinlech Informatiounen", + "save_profile": "Profil späicheren", "view_member": "Member kucken", "account_information": "Konto Informatiounen", "username": "Benotzernumm", diff --git a/app/modules/tenancy/routes/api/merchant.py b/app/modules/tenancy/routes/api/merchant.py index 4247477e..2dc9b169 100644 --- a/app/modules/tenancy/routes/api/merchant.py +++ b/app/modules/tenancy/routes/api/merchant.py @@ -25,7 +25,10 @@ from app.modules.tenancy.schemas import ( MerchantStoreUpdate, ) from app.modules.tenancy.schemas.auth import UserContext -from app.modules.tenancy.schemas.team import MerchantTeamInvite +from app.modules.tenancy.schemas.team import ( + MerchantTeamInvite, + MerchantTeamProfileUpdate, +) from app.modules.tenancy.services.merchant_service import merchant_service from app.modules.tenancy.services.merchant_store_service import merchant_store_service @@ -252,6 +255,26 @@ async def merchant_team_invite( ) +@_account_router.put("/team/members/{user_id}/profile") +async def merchant_team_update_profile( + user_id: int, + data: MerchantTeamProfileUpdate, + current_user: UserContext = Depends(get_current_merchant_api), + merchant=Depends(get_merchant_for_current_user), + db: Session = Depends(get_db), +): + """Update a team member's profile (first name, last name, email).""" + merchant_store_service.update_team_member_profile( + db, merchant.id, user_id, data.model_dump(exclude_unset=True) + ) + db.commit() + logger.info( + f"Merchant {merchant.id} updated profile for user {user_id} " + f"by {current_user.username}" + ) + return {"message": "Profile updated successfully"} + + @_account_router.put("/team/stores/{store_id}/members/{user_id}") async def merchant_team_update_role( store_id: int, diff --git a/app/modules/tenancy/schemas/team.py b/app/modules/tenancy/schemas/team.py index 145368c9..efe48717 100644 --- a/app/modules/tenancy/schemas/team.py +++ b/app/modules/tenancy/schemas/team.py @@ -401,3 +401,11 @@ class MerchantTeamInviteResponse(BaseModel): message: str email: EmailStr results: list[MerchantTeamInviteResult] + + +class MerchantTeamProfileUpdate(BaseModel): + """Schema for updating a team member's profile.""" + + first_name: str | None = Field(None, max_length=100) + last_name: str | None = Field(None, max_length=100) + email: EmailStr | None = None diff --git a/app/modules/tenancy/services/merchant_store_service.py b/app/modules/tenancy/services/merchant_store_service.py index 5f90f460..fecee81f 100644 --- a/app/modules/tenancy/services/merchant_store_service.py +++ b/app/modules/tenancy/services/merchant_store_service.py @@ -26,6 +26,7 @@ from app.modules.tenancy.models.merchant import Merchant from app.modules.tenancy.models.platform import Platform from app.modules.tenancy.models.store import Role, Store from app.modules.tenancy.models.store_platform import StorePlatform +from app.modules.tenancy.models.user import User logger = logging.getLogger(__name__) @@ -456,6 +457,58 @@ class MerchantStoreService: raise StoreNotFoundException(store_id, identifier_type="id") return store + def update_team_member_profile( + self, + db: Session, + merchant_id: int, + user_id: int, + update_data: dict, + ) -> None: + """ + Update a team member's profile (first_name, last_name, email). + + Validates that the user is a team member of one of the merchant's stores. + """ + from app.modules.tenancy.models.store import StoreUser + + # Verify user is a team member in at least one of the merchant's stores + stores = ( + db.query(Store) + .filter(Store.merchant_id == merchant_id) + .all() + ) + store_ids = [s.id for s in stores] + + membership = ( + db.query(StoreUser) + .filter( + StoreUser.store_id.in_(store_ids), + StoreUser.user_id == user_id, + ) + .first() + ) + + if not membership: + # Also allow updating the owner + merchant = db.query(Merchant).filter(Merchant.id == merchant_id).first() + if not merchant or merchant.owner_user_id != user_id: + from app.modules.tenancy.exceptions import UserNotFoundException + raise UserNotFoundException(str(user_id)) + + user = db.query(User).filter(User.id == user_id).first() + if not user: + from app.modules.tenancy.exceptions import UserNotFoundException + raise UserNotFoundException(str(user_id)) + + if "first_name" in update_data: + user.first_name = update_data["first_name"] + if "last_name" in update_data: + user.last_name = update_data["last_name"] + if "email" in update_data and update_data["email"]: + user.email = update_data["email"] + + db.flush() + def get_merchant_team_members(self, db: Session, merchant_id: int) -> dict: """ Get team members across all merchant stores in a member-centric view. diff --git a/app/modules/tenancy/static/merchant/js/merchant-team.js b/app/modules/tenancy/static/merchant/js/merchant-team.js index 132747bc..82aa2f42 100644 --- a/app/modules/tenancy/static/merchant/js/merchant-team.js +++ b/app/modules/tenancy/static/merchant/js/merchant-team.js @@ -201,6 +201,31 @@ function merchantTeam() { this.showEditModal = true; }, + /** + * Update member profile (first name, last name, email) + */ + async updateMemberProfile(userId, firstName, lastName, email) { + this.saving = true; + try { + await apiClient.put( + `/merchants/account/team/members/${userId}/profile`, + { first_name: firstName, last_name: lastName, email: email } + ); + + Utils.showToast(I18n.t('tenancy.messages.team_member_updated'), 'success'); + merchantTeamLog.info('Updated member profile:', userId); + + this.showEditModal = false; + this.selectedMember = null; + await this.loadTeamData(); + } catch (error) { + merchantTeamLog.error('Failed to update member profile:', error); + Utils.showToast(error.message || 'Failed to update member profile', 'error'); + } finally { + this.saving = false; + } + }, + /** * Update member role for a specific store */ diff --git a/app/modules/tenancy/templates/tenancy/merchant/team.html b/app/modules/tenancy/templates/tenancy/merchant/team.html index e03d84a9..09ab0789 100644 --- a/app/modules/tenancy/templates/tenancy/merchant/team.html +++ b/app/modules/tenancy/templates/tenancy/merchant/team.html @@ -271,26 +271,41 @@ {% call modal('editModal', _('tenancy.team.edit_member'), 'showEditModal', size='md', show_footer=false) %}
- -
-
- + +
+

{{ _('tenancy.team.personal_info') }}

+
+
+ + +
+
+ + +
-

- - - -

-

+ + +
+
+
-
+

{{ _('tenancy.team.store_roles') }}