Some checks failed
New resend_invitation() service method regenerates the token and
resends the invitation email for pending members.
Available on all frontends:
- Merchant: POST /merchants/account/team/stores/{sid}/members/{uid}/resend
- Store: POST /store/team/members/{uid}/resend
UI: paper-airplane icon appears on pending members in both merchant
and store team pages.
i18n: resend_invitation + invitation_resent keys in 4 locales.
Also translated previously untranslated invitation_sent_successfully
in fr/de/lb.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
298 lines
9.2 KiB
JavaScript
298 lines
9.2 KiB
JavaScript
// static/store/js/team.js
|
|
/**
|
|
* Store team management page logic
|
|
* Manage team members, invitations, and roles
|
|
*/
|
|
|
|
const storeTeamLog = window.LogConfig.loggers.storeTeam ||
|
|
window.LogConfig.createLogger('storeTeam', false);
|
|
|
|
storeTeamLog.info('Loading...');
|
|
|
|
function storeTeam() {
|
|
storeTeamLog.info('storeTeam() called');
|
|
|
|
return {
|
|
// Inherit base layout state
|
|
...data(),
|
|
|
|
// Set page identifier
|
|
currentPage: 'team',
|
|
|
|
// Loading states
|
|
loading: true,
|
|
error: '',
|
|
saving: false,
|
|
|
|
// Team data
|
|
members: [],
|
|
roles: [],
|
|
stats: {
|
|
total: 0,
|
|
active_count: 0,
|
|
pending_invitations: 0
|
|
},
|
|
|
|
// Modal states
|
|
showInviteModal: false,
|
|
showEditModal: false,
|
|
showRemoveModal: false,
|
|
selectedMember: null,
|
|
|
|
// Invite form
|
|
inviteForm: {
|
|
email: '',
|
|
first_name: '',
|
|
last_name: '',
|
|
role_name: 'staff'
|
|
},
|
|
|
|
// Edit form
|
|
editForm: {
|
|
role_id: null,
|
|
is_active: true
|
|
},
|
|
|
|
// Available role names for invite
|
|
roleOptions: [
|
|
{ value: 'owner', label: 'Owner', description: 'Full access to all features' },
|
|
{ value: 'manager', label: 'Manager', description: 'Manage orders, products, and team' },
|
|
{ value: 'staff', label: 'Staff', description: 'Handle orders and products' },
|
|
{ value: 'support', label: 'Support', description: 'Customer support access' },
|
|
{ value: 'viewer', label: 'Viewer', description: 'Read-only access' },
|
|
{ value: 'marketing', label: 'Marketing', description: 'Content and promotions' }
|
|
],
|
|
|
|
async init() {
|
|
// Load i18n translations
|
|
await I18n.loadModule('tenancy');
|
|
|
|
storeTeamLog.info('Team init() called');
|
|
|
|
// Guard against multiple initialization
|
|
if (window._storeTeamInitialized) {
|
|
storeTeamLog.warn('Already initialized, skipping');
|
|
return;
|
|
}
|
|
window._storeTeamInitialized = true;
|
|
|
|
// IMPORTANT: Call parent init first to set storeCode from URL
|
|
const parentInit = data().init;
|
|
if (parentInit) {
|
|
await parentInit.call(this);
|
|
}
|
|
|
|
try {
|
|
await Promise.all([
|
|
this.loadMembers(),
|
|
this.loadRoles()
|
|
]);
|
|
} catch (error) {
|
|
storeTeamLog.error('Init failed:', error);
|
|
this.error = 'Failed to initialize team page';
|
|
}
|
|
|
|
storeTeamLog.info('Team initialization complete');
|
|
},
|
|
|
|
/**
|
|
* Load team members
|
|
*/
|
|
async loadMembers() {
|
|
this.loading = true;
|
|
this.error = '';
|
|
|
|
try {
|
|
const response = await apiClient.get(`/store/team/members?include_inactive=true`);
|
|
|
|
this.members = response.members || [];
|
|
this.stats = {
|
|
total: response.total || 0,
|
|
active_count: response.active_count || 0,
|
|
pending_invitations: response.pending_invitations || 0
|
|
};
|
|
|
|
storeTeamLog.info('Loaded team members:', this.members.length);
|
|
} catch (error) {
|
|
storeTeamLog.error('Failed to load team members:', error);
|
|
this.error = error.message || 'Failed to load team members';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load available roles
|
|
*/
|
|
async loadRoles() {
|
|
try {
|
|
const response = await apiClient.get(`/store/team/roles`);
|
|
this.roles = response.roles || [];
|
|
storeTeamLog.info('Loaded roles:', this.roles.length);
|
|
} catch (error) {
|
|
storeTeamLog.error('Failed to load roles:', error);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Open invite modal
|
|
*/
|
|
openInviteModal() {
|
|
this.inviteForm = {
|
|
email: '',
|
|
first_name: '',
|
|
last_name: '',
|
|
role_name: 'staff'
|
|
};
|
|
this.showInviteModal = true;
|
|
},
|
|
|
|
/**
|
|
* Send invitation
|
|
*/
|
|
async sendInvitation() {
|
|
if (!this.inviteForm.email) {
|
|
Utils.showToast(I18n.t('tenancy.messages.email_is_required'), 'error');
|
|
return;
|
|
}
|
|
|
|
this.saving = true;
|
|
try {
|
|
await apiClient.post(`/store/team/invite`, this.inviteForm);
|
|
|
|
Utils.showToast(I18n.t('tenancy.messages.invitation_sent_successfully'), 'success');
|
|
storeTeamLog.info('Invitation sent to:', this.inviteForm.email);
|
|
|
|
this.showInviteModal = false;
|
|
await this.loadMembers();
|
|
} catch (error) {
|
|
storeTeamLog.error('Failed to send invitation:', error);
|
|
Utils.showToast(error.message || 'Failed to send invitation', 'error');
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Open edit member modal
|
|
*/
|
|
openEditModal(member) {
|
|
this.selectedMember = member;
|
|
this.editForm = {
|
|
role_id: member.role_id,
|
|
is_active: member.is_active
|
|
};
|
|
this.showEditModal = true;
|
|
},
|
|
|
|
/**
|
|
* Update team member
|
|
*/
|
|
async updateMember() {
|
|
if (!this.selectedMember) return;
|
|
|
|
this.saving = true;
|
|
try {
|
|
await apiClient.put(
|
|
`/store/team/members/${this.selectedMember.id}`,
|
|
this.editForm
|
|
);
|
|
|
|
Utils.showToast(I18n.t('tenancy.messages.team_member_updated'), 'success');
|
|
storeTeamLog.info('Updated team member:', this.selectedMember.user_id);
|
|
|
|
this.showEditModal = false;
|
|
this.selectedMember = null;
|
|
await this.loadMembers();
|
|
} catch (error) {
|
|
storeTeamLog.error('Failed to update team member:', error);
|
|
Utils.showToast(error.message || 'Failed to update team member', 'error');
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Resend invitation to a pending member
|
|
*/
|
|
async resendInvitation(member) {
|
|
this.saving = true;
|
|
try {
|
|
await apiClient.post(`/store/team/members/${member.id}/resend`);
|
|
|
|
Utils.showToast(I18n.t('tenancy.messages.invitation_resent'), 'success');
|
|
storeTeamLog.info('Resent invitation to:', member.email);
|
|
await this.loadMembers();
|
|
} catch (error) {
|
|
storeTeamLog.error('Failed to resend invitation:', error);
|
|
Utils.showToast(error.message || 'Failed to resend invitation', 'error');
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Confirm remove member
|
|
*/
|
|
confirmRemove(member) {
|
|
this.selectedMember = member;
|
|
this.showRemoveModal = true;
|
|
},
|
|
|
|
/**
|
|
* Remove team member
|
|
*/
|
|
async removeMember() {
|
|
if (!this.selectedMember) return;
|
|
|
|
this.saving = true;
|
|
try {
|
|
await apiClient.delete(`/store/team/members/${this.selectedMember.id}`);
|
|
|
|
Utils.showToast(I18n.t('tenancy.messages.team_member_removed'), 'success');
|
|
storeTeamLog.info('Removed team member:', this.selectedMember.user_id);
|
|
|
|
this.showRemoveModal = false;
|
|
this.selectedMember = null;
|
|
await this.loadMembers();
|
|
} catch (error) {
|
|
storeTeamLog.error('Failed to remove team member:', error);
|
|
Utils.showToast(error.message || 'Failed to remove team member', 'error');
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get role display name
|
|
*/
|
|
getRoleName(member) {
|
|
if (member.role_name) return member.role_name;
|
|
const role = this.roles.find(r => r.id === member.role_id);
|
|
return role ? role.name : 'Unknown';
|
|
},
|
|
|
|
/**
|
|
* Get member initials for avatar
|
|
*/
|
|
getInitials(member) {
|
|
const first = member.first_name || member.email?.charAt(0) || '';
|
|
const last = member.last_name || '';
|
|
return (first.charAt(0) + last.charAt(0)).toUpperCase() || '?';
|
|
},
|
|
|
|
/**
|
|
* 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'
|
|
});
|
|
}
|
|
};
|
|
}
|