Files
orion/app/modules/tenancy/static/vendor/js/team.js
Samir Boulahtit d7a0ff8818 refactor: complete module-driven architecture migration
This commit completes the migration to a fully module-driven architecture:

## Models Migration
- Moved all domain models from models/database/ to their respective modules:
  - tenancy: User, Admin, Vendor, Company, Platform, VendorDomain, etc.
  - cms: MediaFile, VendorTheme
  - messaging: Email, VendorEmailSettings, VendorEmailTemplate
  - core: AdminMenuConfig
- models/database/ now only contains Base and TimestampMixin (infrastructure)

## Schemas Migration
- Moved all domain schemas from models/schema/ to their respective modules:
  - tenancy: company, vendor, admin, team, vendor_domain
  - cms: media, image, vendor_theme
  - messaging: email
- models/schema/ now only contains base.py and auth.py (infrastructure)

## Routes Migration
- Moved admin routes from app/api/v1/admin/ to modules:
  - menu_config.py -> core module
  - modules.py -> tenancy module
  - module_config.py -> tenancy module
- app/api/v1/admin/ now only aggregates auto-discovered module routes

## Menu System
- Implemented module-driven menu system with MenuDiscoveryService
- Extended FrontendType enum: PLATFORM, ADMIN, VENDOR, STOREFRONT
- Added MenuItemDefinition and MenuSectionDefinition dataclasses
- Each module now defines its own menu items in definition.py
- MenuService integrates with MenuDiscoveryService for template rendering

## Documentation
- Updated docs/architecture/models-structure.md
- Updated docs/architecture/menu-management.md
- Updated architecture validation rules for new exceptions

## Architecture Validation
- Updated MOD-019 rule to allow base.py in models/schema/
- Created core module exceptions.py and schemas/ directory
- All validation errors resolved (only warnings remain)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 21:02:56 +01:00

279 lines
8.5 KiB
JavaScript

// static/vendor/js/team.js
/**
* Vendor team management page logic
* Manage team members, invitations, and roles
*/
const vendorTeamLog = window.LogConfig.loggers.vendorTeam ||
window.LogConfig.createLogger('vendorTeam', false);
vendorTeamLog.info('Loading...');
function vendorTeam() {
vendorTeamLog.info('vendorTeam() 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');
vendorTeamLog.info('Team init() called');
// Guard against multiple initialization
if (window._vendorTeamInitialized) {
vendorTeamLog.warn('Already initialized, skipping');
return;
}
window._vendorTeamInitialized = true;
// IMPORTANT: Call parent init first to set vendorCode from URL
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
try {
await Promise.all([
this.loadMembers(),
this.loadRoles()
]);
} catch (error) {
vendorTeamLog.error('Init failed:', error);
this.error = 'Failed to initialize team page';
}
vendorTeamLog.info('Team initialization complete');
},
/**
* Load team members
*/
async loadMembers() {
this.loading = true;
this.error = '';
try {
const response = await apiClient.get(`/vendor/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
};
vendorTeamLog.info('Loaded team members:', this.members.length);
} catch (error) {
vendorTeamLog.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(`/vendor/team/roles`);
this.roles = response.roles || [];
vendorTeamLog.info('Loaded roles:', this.roles.length);
} catch (error) {
vendorTeamLog.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(`/vendor/team/invite`, this.inviteForm);
Utils.showToast(I18n.t('tenancy.messages.invitation_sent_successfully'), 'success');
vendorTeamLog.info('Invitation sent to:', this.inviteForm.email);
this.showInviteModal = false;
await this.loadMembers();
} catch (error) {
vendorTeamLog.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(
`/vendor/${this.vendorCode}/team/members/${this.selectedMember.user_id}`,
this.editForm
);
Utils.showToast(I18n.t('tenancy.messages.team_member_updated'), 'success');
vendorTeamLog.info('Updated team member:', this.selectedMember.user_id);
this.showEditModal = false;
this.selectedMember = null;
await this.loadMembers();
} catch (error) {
vendorTeamLog.error('Failed to update team member:', error);
Utils.showToast(error.message || 'Failed to update team member', '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(`/vendor/team/members/${this.selectedMember.user_id}`);
Utils.showToast(I18n.t('tenancy.messages.team_member_removed'), 'success');
vendorTeamLog.info('Removed team member:', this.selectedMember.user_id);
this.showRemoveModal = false;
this.selectedMember = null;
await this.loadMembers();
} catch (error) {
vendorTeamLog.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.VENDOR_CONFIG?.locale || 'en-GB';
return new Date(dateStr).toLocaleDateString(locale, {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
};
}