// static/merchant/js/merchant-team.js /** * Merchant team management page logic * Manage team members across stores, invitations, and roles */ const merchantTeamLog = window.LogConfig.createLogger('merchantTeam'); merchantTeamLog.info('Loading...'); function merchantTeam() { merchantTeamLog.info('merchantTeam() called'); return { // Inherit base layout state ...data(), // Set page identifier currentPage: 'team', // Team data members: [], stores: [], stats: { total_members: 0, total_active: 0, total_pending: 0 }, // Loading states loading: false, error: null, saving: false, // Filters storeFilter: '', // Expanded member rows expandedMembers: [], // Modal states showInviteModal: false, showEditModal: false, showRemoveModal: false, showViewModal: false, selectedMember: null, // Invite form inviteForm: { email: '', first_name: '', last_name: '', store_ids: [], role_name: 'staff', }, // Role options (preset) roleOptions: [ { value: 'manager', label: 'Manager' }, { value: 'staff', label: 'Staff' }, { value: 'support', label: 'Support' }, { value: 'viewer', label: 'Viewer' }, { value: 'marketing', label: 'Marketing' }, ], async init() { // Load i18n translations await I18n.loadModule('tenancy'); merchantTeamLog.info('Team init() called'); // Guard against multiple initialization if (window._merchantTeamInitialized) { merchantTeamLog.warn('Already initialized, skipping'); return; } window._merchantTeamInitialized = true; // Call parent init first const parentInit = data().init; if (parentInit) { await parentInit.call(this); } // Load dynamic menu this.loadMenuConfig(); try { await this.loadTeamData(); } catch (error) { merchantTeamLog.error('Init failed:', error); this.error = 'Failed to initialize team page'; } merchantTeamLog.info('Team initialization complete'); }, /** * Load team data (members, stores, stats) */ async loadTeamData() { this.loading = true; this.error = null; try { const response = await apiClient.get('/merchants/account/team'); this.members = response.members || []; this.stores = response.stores || []; this.stats = { total_members: response.total_members || 0, total_active: response.total_active || 0, total_pending: response.total_pending || 0, }; merchantTeamLog.info('Loaded team data:', this.members.length, 'members,', this.stores.length, 'stores'); } catch (error) { merchantTeamLog.error('Failed to load team data:', error); this.error = error.message || 'Failed to load team data'; } finally { this.loading = false; } }, /** * Filter members by store */ get filteredMembers() { if (!this.storeFilter) { return this.members; } const storeId = parseInt(this.storeFilter); return this.members.filter(member => member.stores && member.stores.some(s => s.store_id === storeId) ); }, /** * Toggle expand/collapse for a member's store rows */ toggleMemberExpand(userId) { const idx = this.expandedMembers.indexOf(userId); if (idx > -1) { this.expandedMembers.splice(idx, 1); } else { this.expandedMembers.push(userId); } }, /** * Resend invitation for a specific store membership */ async resendStoreInvitation(storeId, userId) { this.saving = true; try { await apiClient.post( `/merchants/account/team/stores/${storeId}/members/${userId}/resend` ); Utils.showToast(I18n.t('tenancy.messages.invitation_resent'), 'success'); merchantTeamLog.info('Resent invitation for store:', storeId, 'user:', userId); await this.loadTeamData(); } catch (error) { merchantTeamLog.error('Failed to resend invitation:', error); Utils.showToast(error.message || 'Failed to resend invitation', 'error'); } finally { this.saving = false; } }, /** * Open invite modal with reset form */ openInviteModal() { this.inviteForm = { email: '', first_name: '', last_name: '', store_ids: this.stores.map(s => s.id), role_name: 'staff', }; this.showInviteModal = true; }, /** * Toggle store in invite form store_ids */ toggleStoreSelection(storeId) { const idx = this.inviteForm.store_ids.indexOf(storeId); if (idx > -1) { this.inviteForm.store_ids.splice(idx, 1); } else { this.inviteForm.store_ids.push(storeId); } }, /** * Send invitation */ async sendInvitation() { if (!this.inviteForm.email) { Utils.showToast(I18n.t('tenancy.messages.email_is_required'), 'error'); return; } if (this.inviteForm.store_ids.length === 0) { Utils.showToast(I18n.t('tenancy.messages.select_at_least_one_store'), 'error'); return; } this.saving = true; try { await apiClient.post('/merchants/account/team/invite', this.inviteForm); Utils.showToast(I18n.t('tenancy.messages.invitation_sent_successfully'), 'success'); merchantTeamLog.info('Invitation sent to:', this.inviteForm.email); this.showInviteModal = false; await this.loadTeamData(); } catch (error) { merchantTeamLog.error('Failed to send invitation:', error); Utils.showToast(error.message || 'Failed to send invitation', 'error'); } finally { this.saving = false; } }, /** * Open view modal for a member (read-only detail) */ openViewModal(member) { this.selectedMember = JSON.parse(JSON.stringify(member)); this.showViewModal = true; }, /** * Open edit modal for a member */ openEditModal(member) { this.selectedMember = JSON.parse(JSON.stringify(member)); 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; } }, /** * Resend invitation to a pending member */ async resendInvitation(member) { if (!member.stores || member.stores.length === 0) return; this.saving = true; try { // Resend for the first pending store const pendingStore = member.stores.find(s => s.is_pending) || member.stores[0]; await apiClient.post( `/merchants/account/team/stores/${pendingStore.store_id}/members/${member.user_id}/resend` ); Utils.showToast(I18n.t('tenancy.messages.invitation_resent'), 'success'); merchantTeamLog.info('Resent invitation to:', member.email); await this.loadTeamData(); } catch (error) { merchantTeamLog.error('Failed to resend invitation:', error); Utils.showToast(error.message || 'Failed to resend invitation', 'error'); } finally { this.saving = false; } }, /** * Update member role for a specific store */ async updateMemberRole(storeId, userId, roleName) { this.saving = true; try { await apiClient.put( `/merchants/account/team/stores/${storeId}/members/${userId}?role_name=${encodeURIComponent(roleName)}` ); Utils.showToast(I18n.t('tenancy.messages.team_member_updated'), 'success'); merchantTeamLog.info('Updated member role:', userId, 'store:', storeId, 'role:', roleName); this.showEditModal = false; this.selectedMember = null; await this.loadTeamData(); } catch (error) { merchantTeamLog.error('Failed to update member role:', error); Utils.showToast(error.message || 'Failed to update member role', 'error'); } finally { this.saving = false; } }, /** * Open remove confirmation modal */ openRemoveModal(member) { this.selectedMember = JSON.parse(JSON.stringify(member)); this.showRemoveModal = true; }, /** * Remove member from a specific store */ async removeMember(storeId, userId) { this.saving = true; try { await apiClient.delete(`/merchants/account/team/stores/${storeId}/members/${userId}`); Utils.showToast(I18n.t('tenancy.messages.team_member_removed'), 'success'); merchantTeamLog.info('Removed member:', userId, 'from store:', storeId); this.showRemoveModal = false; this.selectedMember = null; await this.loadTeamData(); } catch (error) { merchantTeamLog.error('Failed to remove member:', error); Utils.showToast(error.message || 'Failed to remove member', 'error'); } finally { this.saving = false; } }, /** * Remove member from all stores */ async removeFromAllStores(member) { if (!member || !member.stores || member.stores.length === 0) return; this.saving = true; try { for (const store of member.stores) { await apiClient.delete( `/merchants/account/team/stores/${store.store_id}/members/${member.user_id}` ); } Utils.showToast(I18n.t('tenancy.messages.team_member_removed'), 'success'); merchantTeamLog.info('Removed member from all stores:', member.user_id); this.showRemoveModal = false; this.selectedMember = null; await this.loadTeamData(); } catch (error) { merchantTeamLog.error('Failed to remove member from all stores:', error); Utils.showToast(error.message || 'Failed to remove member', 'error'); } finally { this.saving = false; } }, /** * Get initials for avatar display */ getInitials(member) { const first = member.first_name || member.email?.charAt(0) || ''; const last = member.last_name || ''; return (first.charAt(0) + last.charAt(0)).toUpperCase() || '?'; }, /** * Get member status based on their store memberships */ getMemberStatus(member) { if (!member.stores || member.stores.length === 0) return 'inactive'; if (member.stores.some(s => s.is_active && !s.is_pending)) return 'active'; if (member.stores.some(s => s.is_pending)) return 'pending'; return 'inactive'; }, /** * Format date for display */ formatDate(dateStr) { if (!dateStr) return '-'; const locale = window.STORE_CONFIG?.locale || 'en-GB'; return new Date(dateStr).toLocaleDateString(locale, { year: 'numeric', month: 'short', day: 'numeric' }); } }; }