@noatgnu/cupcake-core 1.2.5 → 1.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1688,8 +1688,14 @@ class LabGroupService extends BaseApiService {
1688
1688
  deleteLabGroup(id) {
1689
1689
  return this.delete(`${this.apiUrl}/lab-groups/${id}/`);
1690
1690
  }
1691
- getLabGroupMembers(id) {
1692
- return this.get(`${this.apiUrl}/lab-groups/${id}/members/`);
1691
+ getLabGroupMembers(id, params) {
1692
+ const httpParams = this.buildHttpParams({
1693
+ direct_only: params?.directOnly?.toString(),
1694
+ page_size: params?.pageSize?.toString(),
1695
+ limit: params?.limit?.toString(),
1696
+ offset: params?.offset?.toString()
1697
+ });
1698
+ return this.get(`${this.apiUrl}/lab-groups/${id}/members/`, { params: httpParams });
1693
1699
  }
1694
1700
  inviteUserToLabGroup(id, invitation) {
1695
1701
  return this.post(`${this.apiUrl}/lab-groups/${id}/invite_user/`, invitation);
@@ -2578,6 +2584,11 @@ class LabGroupsComponent {
2578
2584
  showCreateForm = signal(false, ...(ngDevMode ? [{ debugName: "showCreateForm" }] : []));
2579
2585
  showInviteForm = signal(false, ...(ngDevMode ? [{ debugName: "showInviteForm" }] : []));
2580
2586
  selectedGroupForMembers = signal(null, ...(ngDevMode ? [{ debugName: "selectedGroupForMembers" }] : []));
2587
+ // Member pagination
2588
+ memberTotal = signal(0, ...(ngDevMode ? [{ debugName: "memberTotal" }] : []));
2589
+ memberPage = signal(1, ...(ngDevMode ? [{ debugName: "memberPage" }] : []));
2590
+ memberPageSize = signal(10, ...(ngDevMode ? [{ debugName: "memberPageSize" }] : []));
2591
+ directMembersOnly = signal(false, ...(ngDevMode ? [{ debugName: "directMembersOnly" }] : []));
2581
2592
  // Computed values
2582
2593
  hasLabGroups = computed(() => this.labGroupsData().results.length > 0, ...(ngDevMode ? [{ debugName: "hasLabGroups" }] : []));
2583
2594
  showPagination = computed(() => this.labGroupsData().count > this.pageSize(), ...(ngDevMode ? [{ debugName: "showPagination" }] : []));
@@ -2588,8 +2599,11 @@ class LabGroupsComponent {
2588
2599
  canInviteToCurrentGroup = computed(() => this.selectedGroupForMembers()?.canInvite || false, ...(ngDevMode ? [{ debugName: "canInviteToCurrentGroup" }] : []));
2589
2600
  canManageCurrentGroup = computed(() => this.selectedGroupForMembers()?.canManage || false, ...(ngDevMode ? [{ debugName: "canManageCurrentGroup" }] : []));
2590
2601
  currentGroupName = computed(() => this.selectedGroupForMembers()?.name || '', ...(ngDevMode ? [{ debugName: "currentGroupName" }] : []));
2591
- groupMembersCount = computed(() => this.groupMembers().length, ...(ngDevMode ? [{ debugName: "groupMembersCount" }] : []));
2602
+ groupMembersCount = computed(() => this.memberTotal(), ...(ngDevMode ? [{ debugName: "groupMembersCount" }] : []));
2592
2603
  pendingInvitationsCount = computed(() => this.pendingInvitations().length, ...(ngDevMode ? [{ debugName: "pendingInvitationsCount" }] : []));
2604
+ showMemberPagination = computed(() => this.memberTotal() > this.memberPageSize(), ...(ngDevMode ? [{ debugName: "showMemberPagination" }] : []));
2605
+ memberTotalPages = computed(() => Math.ceil(this.memberTotal() / this.memberPageSize()), ...(ngDevMode ? [{ debugName: "memberTotalPages" }] : []));
2606
+ hasSubGroups = computed(() => (this.selectedGroupForMembers()?.subGroupsCount ?? 0) > 0, ...(ngDevMode ? [{ debugName: "hasSubGroups" }] : []));
2593
2607
  constructor() {
2594
2608
  this.searchForm = this.fb.group({
2595
2609
  search: ['']
@@ -2688,16 +2702,25 @@ class LabGroupsComponent {
2688
2702
  }
2689
2703
  viewGroupMembers(group) {
2690
2704
  this.selectedGroupForMembers.set(group);
2705
+ this.memberPage.set(1);
2706
+ this.directMembersOnly.set(false);
2691
2707
  this.loadGroupMembers(group.id);
2692
2708
  this.loadPendingInvitations(group.id);
2693
2709
  }
2694
2710
  loadGroupMembers(groupId) {
2695
- this.labGroupService.getLabGroupMembers(groupId).subscribe({
2696
- next: (members) => {
2697
- this.groupMembers.set(members);
2711
+ const offset = (this.memberPage() - 1) * this.memberPageSize();
2712
+ this.labGroupService.getLabGroupMembers(groupId, {
2713
+ directOnly: this.directMembersOnly(),
2714
+ limit: this.memberPageSize(),
2715
+ offset: offset
2716
+ }).subscribe({
2717
+ next: (response) => {
2718
+ this.groupMembers.set(response.results);
2719
+ this.memberTotal.set(response.count);
2698
2720
  },
2699
2721
  error: (error) => {
2700
2722
  this.groupMembers.set([]);
2723
+ this.memberTotal.set(0);
2701
2724
  console.error('Error loading group members:', error);
2702
2725
  }
2703
2726
  });
@@ -2825,15 +2848,32 @@ class LabGroupsComponent {
2825
2848
  closeGroupDetails() {
2826
2849
  this.selectedGroupForMembers.set(null);
2827
2850
  this.groupMembers.set([]);
2851
+ this.memberTotal.set(0);
2852
+ this.memberPage.set(1);
2828
2853
  this.pendingInvitations.set([]);
2829
2854
  this.showInviteForm.set(false);
2830
2855
  }
2856
+ toggleDirectMembersOnly() {
2857
+ this.directMembersOnly.update(v => !v);
2858
+ this.memberPage.set(1);
2859
+ const group = this.selectedGroupForMembers();
2860
+ if (group) {
2861
+ this.loadGroupMembers(group.id);
2862
+ }
2863
+ }
2864
+ onMemberPageChange(page) {
2865
+ this.memberPage.set(page);
2866
+ const group = this.selectedGroupForMembers();
2867
+ if (group) {
2868
+ this.loadGroupMembers(group.id);
2869
+ }
2870
+ }
2831
2871
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: LabGroupsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2832
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.3", type: LabGroupsComponent, isStandalone: true, selector: "ccc-lab-groups", ngImport: i0, template: "<div class=\"lab-groups-container\">\n <!-- Header Section -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100\">\n <div class=\"d-flex align-items-center\">\n <h4 class=\"navbar-brand m-0 fw-bold\">\n <i class=\"bi bi-people me-2\"></i>Lab Groups\n </h4>\n </div>\n <div class=\"d-flex gap-2\">\n <button type=\"button\" class=\"btn btn-primary btn-sm\" (click)=\"toggleCreateForm()\">\n <i class=\"bi bi-plus me-1\"></i>New Lab Group\n </button>\n </div>\n </div>\n </div>\n </nav>\n\n <div class=\"groups-content p-4\">\n <!-- Create Group Form -->\n @if (showCreateForm()) {\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-plus-circle me-2\"></i>Create New Lab Group\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"createGroupForm\" (ngSubmit)=\"createLabGroup()\" class=\"row g-3\">\n <div class=\"col-md-6\">\n <div class=\"form-floating\">\n <input \n type=\"text\" \n class=\"form-control\" \n id=\"groupName\" \n formControlName=\"name\"\n [class.is-invalid]=\"createGroupForm.get('name')?.invalid && createGroupForm.get('name')?.touched\"\n placeholder=\"Group name\">\n <label for=\"groupName\">Group Name *</label>\n </div>\n @if (createGroupForm.get('name')?.invalid && createGroupForm.get('name')?.touched) {\n <div class=\"invalid-feedback\">\n @if (createGroupForm.get('name')?.errors?.['required']) {\n Group name is required\n }\n @if (createGroupForm.get('name')?.errors?.['minlength']) {\n Group name must be at least 2 characters\n }\n </div>\n }\n </div>\n <div class=\"col-md-6\">\n <div class=\"form-check form-switch mt-3\">\n <input \n class=\"form-check-input\" \n type=\"checkbox\" \n id=\"allowInvites\" \n formControlName=\"allowMemberInvites\">\n <label class=\"form-check-label\" for=\"allowInvites\">\n <i class=\"bi bi-envelope me-1\"></i>Allow members to invite others\n </label>\n </div>\n </div>\n <div class=\"col-12\">\n <div class=\"form-floating\">\n <textarea \n class=\"form-control\" \n id=\"groupDescription\" \n formControlName=\"description\"\n style=\"height: 100px\"\n placeholder=\"Description\"></textarea>\n <label for=\"groupDescription\">Description (optional)</label>\n </div>\n </div>\n <div class=\"col-12\">\n <div class=\"d-flex gap-2\">\n <button \n type=\"submit\" \n class=\"btn btn-primary\"\n [disabled]=\"createGroupForm.invalid || isCreatingGroup()\">\n @if (isCreatingGroup()) {\n <span class=\"spinner-border spinner-border-sm me-2\" role=\"status\"></span>\n }\n Create Group\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"toggleCreateForm()\">\n Cancel\n </button>\n </div>\n </div>\n </form>\n </div>\n </div>\n }\n\n <!-- Search & Filters Section -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-search me-2\"></i>Search & Filters\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"searchForm\" class=\"row g-3\">\n <div class=\"col-md-12\">\n <div class=\"form-floating\">\n <input \n type=\"search\" \n class=\"form-control\" \n id=\"groupSearch\" \n formControlName=\"search\"\n placeholder=\"Search groups...\">\n <label for=\"groupSearch\">\n <i class=\"bi bi-search me-1\"></i>Search Groups\n </label>\n </div>\n </div>\n </form>\n </div>\n </div>\n\n <!-- Groups List -->\n <div class=\"card\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-people me-2\"></i>Lab Groups\n <span class=\"badge bg-primary ms-2\">{{ labGroupsData().count }}</span>\n </h5>\n @if (isLoading()) {\n <div class=\"spinner-border spinner-border-sm text-primary\" role=\"status\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n }\n </div>\n <div class=\"card-body p-0\">\n @if (hasLabGroups()) {\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\"><i class=\"bi bi-tag me-1\"></i>Group Name</th>\n <th scope=\"col\"><i class=\"bi bi-person me-1\"></i>Creator</th>\n <th scope=\"col\"><i class=\"bi bi-people me-1\"></i>Members</th>\n <th scope=\"col\"><i class=\"bi bi-check-circle me-1\"></i>Status</th>\n <th scope=\"col\"><i class=\"bi bi-calendar me-1\"></i>Created</th>\n <th scope=\"col\" style=\"width: 120px;\"><i class=\"bi bi-gear me-1\"></i>Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (group of labGroupsData().results; track group.id) {\n <tr>\n <td>\n <strong>{{ group.name }}</strong>\n @if (group.description) {\n <br><small class=\"text-muted\">{{ group.description }}</small>\n }\n </td>\n <td>{{ group.creatorName || 'Unknown' }}</td>\n <td>\n <span class=\"badge bg-info\">{{ group.memberCount }}</span>\n </td>\n <td>\n @if (group.isActive) {\n <span class=\"badge bg-success\">Active</span>\n } @else {\n <span class=\"badge bg-secondary\">Inactive</span>\n }\n </td>\n <td>\n <small>{{ group.createdAt | date:'short' }}</small>\n </td>\n <td>\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n <button \n type=\"button\"\n class=\"btn btn-outline-info\" \n (click)=\"viewGroupMembers(group)\"\n title=\"View members\">\n <i class=\"bi bi-people\"></i>\n </button>\n @if (group.isMember && !group.isCreator) {\n <button \n type=\"button\"\n class=\"btn btn-outline-warning\" \n (click)=\"leaveGroup(group)\"\n title=\"Leave group\">\n <i class=\"bi bi-box-arrow-right\"></i>\n </button>\n }\n @if (group.canManage) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger\" \n (click)=\"deleteGroup(group)\"\n title=\"Delete group\">\n <i class=\"bi bi-trash\"></i>\n </button>\n }\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <!-- Pagination -->\n @if (showPagination()) {\n <div class=\"d-flex justify-content-center mt-3 p-3\">\n <ngb-pagination\n [collectionSize]=\"labGroupsData().count\"\n [page]=\"currentPage()\"\n [pageSize]=\"pageSize()\"\n [maxSize]=\"5\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onPageChange($event)\"\n class=\"pagination-sm\">\n </ngb-pagination>\n </div>\n }\n } @else {\n <div class=\"text-center py-5\">\n <div class=\"text-muted\">\n <i class=\"bi bi-people display-1 mb-3\"></i>\n <h5>No Lab Groups Found</h5>\n <p>\n @if (hasSearchValue()) {\n Try adjusting your search criteria or create a new lab group.\n } @else {\n Get started by creating your first lab group.\n }\n </p>\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"toggleCreateForm()\">\n <i class=\"bi bi-plus me-1\"></i>Create First Lab Group\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n\n<!-- Group Members Modal -->\n@if (selectedGroupForMembers()) {\n <div class=\"modal-backdrop fade show\"></div>\n <div class=\"modal fade show d-block\" tabindex=\"-1\">\n <div class=\"modal-dialog modal-lg\">\n <div class=\"modal-content\">\n <div class=\"modal-header\">\n <h5 class=\"modal-title\">\n <i class=\"bi bi-people me-2\"></i>{{ currentGroupName() }} - Members\n </h5>\n <button type=\"button\" class=\"btn-close\" (click)=\"closeGroupDetails()\"></button>\n </div>\n <div class=\"modal-body\">\n <!-- Invite Form -->\n @if (canInviteToCurrentGroup()) {\n <div class=\"card mb-4\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-envelope me-2\"></i>Invite Member\n </h6>\n <button \n type=\"button\"\n class=\"btn btn-sm btn-outline-primary\" \n (click)=\"toggleInviteForm()\">\n @if (showInviteForm()) {\n Cancel\n } @else {\n <i class=\"bi bi-plus me-1\"></i>Invite\n }\n </button>\n </div>\n @if (showInviteForm()) {\n <div class=\"card-body\">\n <form [formGroup]=\"inviteForm\" (ngSubmit)=\"inviteMember()\" class=\"row g-3\">\n <div class=\"col-md-8\">\n <div class=\"form-floating\">\n <input \n type=\"email\" \n class=\"form-control\" \n id=\"inviteEmail\" \n formControlName=\"invitedEmail\"\n [class.is-invalid]=\"inviteForm.get('invitedEmail')?.invalid && inviteForm.get('invitedEmail')?.touched\"\n placeholder=\"Email address\">\n <label for=\"inviteEmail\">Email Address *</label>\n </div>\n @if (inviteForm.get('invitedEmail')?.invalid && inviteForm.get('invitedEmail')?.touched) {\n <div class=\"invalid-feedback\">\n @if (inviteForm.get('invitedEmail')?.errors?.['required']) {\n Email is required\n }\n @if (inviteForm.get('invitedEmail')?.errors?.['email']) {\n Please enter a valid email address\n }\n </div>\n }\n </div>\n <div class=\"col-md-4\">\n <button \n type=\"submit\" \n class=\"btn btn-primary w-100 h-100\"\n [disabled]=\"inviteForm.invalid || isInviting()\">\n @if (isInviting()) {\n <span class=\"spinner-border spinner-border-sm me-2\" role=\"status\"></span>\n }\n Send Invite\n </button>\n </div>\n <div class=\"col-12\">\n <div class=\"form-floating\">\n <textarea \n class=\"form-control\" \n id=\"inviteMessage\" \n formControlName=\"message\"\n style=\"height: 80px\"\n placeholder=\"Message\"></textarea>\n <label for=\"inviteMessage\">Message (optional)</label>\n </div>\n </div>\n </form>\n </div>\n }\n </div>\n }\n\n <!-- Current Members -->\n <div class=\"card mb-3\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-people me-2\"></i>Current Members ({{ groupMembersCount() }})\n </h6>\n </div>\n <div class=\"card-body p-0\">\n @if (hasGroupMembers()) {\n <div class=\"list-group list-group-flush\">\n @for (member of groupMembers(); track member.id) {\n <div class=\"list-group-item d-flex justify-content-between align-items-center\">\n <div>\n <strong>{{ member.firstName }} {{ member.lastName }}</strong>\n <br>\n <small class=\"text-muted\">{{ member.email }}</small>\n @if (selectedGroupForMembers() && member.id === selectedGroupForMembers()!.creator) {\n <span class=\"badge bg-warning ms-2\">Creator</span>\n }\n </div>\n @if (canManageCurrentGroup() && selectedGroupForMembers() && member.id !== selectedGroupForMembers()!.creator) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger btn-sm\" \n (click)=\"removeMember(member.id)\"\n title=\"Remove member\">\n <i class=\"bi bi-person-dash\"></i>\n </button>\n }\n </div>\n }\n </div>\n } @else {\n <div class=\"text-center py-3 text-muted\">\n <i class=\"bi bi-people fs-4 mb-2\"></i>\n <div>No members found</div>\n </div>\n }\n </div>\n </div>\n\n <!-- Pending Invitations -->\n @if (hasPendingInvitations()) {\n <div class=\"card\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-clock me-2\"></i>Pending Invitations ({{ pendingInvitationsCount() }})\n </h6>\n </div>\n <div class=\"card-body p-0\">\n <div class=\"list-group list-group-flush\">\n @for (invitation of pendingInvitations(); track invitation.id) {\n <div class=\"list-group-item d-flex justify-content-between align-items-center\">\n <div>\n <strong>{{ invitation.invitedEmail }}</strong>\n <br>\n <small class=\"text-muted\">\n Invited by {{ invitation.inviterName }} on {{ invitation.createdAt | date:'short' }}\n </small>\n @if (invitation.message) {\n <br>\n <small class=\"text-info\">{{ invitation.message }}</small>\n }\n </div>\n @if (canManageCurrentGroup()) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger btn-sm\" \n (click)=\"cancelInvitation(invitation.id)\"\n title=\"Cancel invitation\">\n <i class=\"bi bi-x-circle\"></i>\n </button>\n }\n </div>\n }\n </div>\n </div>\n </div>\n }\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"closeGroupDetails()\">\n Close\n </button>\n </div>\n </div>\n </div>\n </div>\n}", styles: [".lab-groups-container .groups-content{max-width:1200px;margin:0 auto}@media (max-width: 768px){.lab-groups-container .groups-content{padding:1rem!important}.lab-groups-container .btn-group{flex-direction:column;width:100%}.lab-groups-container .btn-group .btn{border-radius:.25rem!important;margin-bottom:.25rem}.lab-groups-container .modal-dialog{margin:.5rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: NgbModule }, { kind: "component", type: i2$1.NgbPagination, selector: "ngb-pagination", inputs: ["disabled", "boundaryLinks", "directionLinks", "ellipses", "rotate", "collectionSize", "maxSize", "page", "pageSize", "size"], outputs: ["pageChange"] }, { kind: "pipe", type: i2.DatePipe, name: "date" }] });
2872
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.3", type: LabGroupsComponent, isStandalone: true, selector: "ccc-lab-groups", ngImport: i0, template: "<div class=\"lab-groups-container\">\n <!-- Header Section -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100\">\n <div class=\"d-flex align-items-center\">\n <h4 class=\"navbar-brand m-0 fw-bold\">\n <i class=\"bi bi-people me-2\"></i>Lab Groups\n </h4>\n </div>\n <div class=\"d-flex gap-2\">\n <button type=\"button\" class=\"btn btn-primary btn-sm\" (click)=\"toggleCreateForm()\">\n <i class=\"bi bi-plus me-1\"></i>New Lab Group\n </button>\n </div>\n </div>\n </div>\n </nav>\n\n <div class=\"groups-content p-4\">\n <!-- Create Group Form -->\n @if (showCreateForm()) {\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-plus-circle me-2\"></i>Create New Lab Group\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"createGroupForm\" (ngSubmit)=\"createLabGroup()\" class=\"row g-3\">\n <div class=\"col-md-6\">\n <div class=\"form-floating\">\n <input \n type=\"text\" \n class=\"form-control\" \n id=\"groupName\" \n formControlName=\"name\"\n [class.is-invalid]=\"createGroupForm.get('name')?.invalid && createGroupForm.get('name')?.touched\"\n placeholder=\"Group name\">\n <label for=\"groupName\">Group Name *</label>\n </div>\n @if (createGroupForm.get('name')?.invalid && createGroupForm.get('name')?.touched) {\n <div class=\"invalid-feedback\">\n @if (createGroupForm.get('name')?.errors?.['required']) {\n Group name is required\n }\n @if (createGroupForm.get('name')?.errors?.['minlength']) {\n Group name must be at least 2 characters\n }\n </div>\n }\n </div>\n <div class=\"col-md-6\">\n <div class=\"form-check form-switch mt-3\">\n <input \n class=\"form-check-input\" \n type=\"checkbox\" \n id=\"allowInvites\" \n formControlName=\"allowMemberInvites\">\n <label class=\"form-check-label\" for=\"allowInvites\">\n <i class=\"bi bi-envelope me-1\"></i>Allow members to invite others\n </label>\n </div>\n </div>\n <div class=\"col-12\">\n <div class=\"form-floating\">\n <textarea \n class=\"form-control\" \n id=\"groupDescription\" \n formControlName=\"description\"\n style=\"height: 100px\"\n placeholder=\"Description\"></textarea>\n <label for=\"groupDescription\">Description (optional)</label>\n </div>\n </div>\n <div class=\"col-12\">\n <div class=\"d-flex gap-2\">\n <button \n type=\"submit\" \n class=\"btn btn-primary\"\n [disabled]=\"createGroupForm.invalid || isCreatingGroup()\">\n @if (isCreatingGroup()) {\n <span class=\"spinner-border spinner-border-sm me-2\" role=\"status\"></span>\n }\n Create Group\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"toggleCreateForm()\">\n Cancel\n </button>\n </div>\n </div>\n </form>\n </div>\n </div>\n }\n\n <!-- Search & Filters Section -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-search me-2\"></i>Search & Filters\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"searchForm\" class=\"row g-3\">\n <div class=\"col-md-12\">\n <div class=\"form-floating\">\n <input \n type=\"search\" \n class=\"form-control\" \n id=\"groupSearch\" \n formControlName=\"search\"\n placeholder=\"Search groups...\">\n <label for=\"groupSearch\">\n <i class=\"bi bi-search me-1\"></i>Search Groups\n </label>\n </div>\n </div>\n </form>\n </div>\n </div>\n\n <!-- Groups List -->\n <div class=\"card\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-people me-2\"></i>Lab Groups\n <span class=\"badge bg-primary ms-2\">{{ labGroupsData().count }}</span>\n </h5>\n @if (isLoading()) {\n <div class=\"spinner-border spinner-border-sm text-primary\" role=\"status\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n }\n </div>\n <div class=\"card-body p-0\">\n @if (hasLabGroups()) {\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\"><i class=\"bi bi-tag me-1\"></i>Group Name</th>\n <th scope=\"col\"><i class=\"bi bi-person me-1\"></i>Creator</th>\n <th scope=\"col\"><i class=\"bi bi-people me-1\"></i>Members</th>\n <th scope=\"col\"><i class=\"bi bi-check-circle me-1\"></i>Status</th>\n <th scope=\"col\"><i class=\"bi bi-calendar me-1\"></i>Created</th>\n <th scope=\"col\" style=\"width: 120px;\"><i class=\"bi bi-gear me-1\"></i>Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (group of labGroupsData().results; track group.id) {\n <tr>\n <td>\n <strong>{{ group.name }}</strong>\n @if (group.description) {\n <br><small class=\"text-muted\">{{ group.description }}</small>\n }\n </td>\n <td>{{ group.creatorName || 'Unknown' }}</td>\n <td>\n <span class=\"badge bg-info\">{{ group.memberCount }}</span>\n </td>\n <td>\n @if (group.isActive) {\n <span class=\"badge bg-success\">Active</span>\n } @else {\n <span class=\"badge bg-secondary\">Inactive</span>\n }\n </td>\n <td>\n <small>{{ group.createdAt | date:'short' }}</small>\n </td>\n <td>\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n <button \n type=\"button\"\n class=\"btn btn-outline-info\" \n (click)=\"viewGroupMembers(group)\"\n title=\"View members\">\n <i class=\"bi bi-people\"></i>\n </button>\n @if (group.isMember && !group.isCreator) {\n <button \n type=\"button\"\n class=\"btn btn-outline-warning\" \n (click)=\"leaveGroup(group)\"\n title=\"Leave group\">\n <i class=\"bi bi-box-arrow-right\"></i>\n </button>\n }\n @if (group.canManage) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger\" \n (click)=\"deleteGroup(group)\"\n title=\"Delete group\">\n <i class=\"bi bi-trash\"></i>\n </button>\n }\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <!-- Pagination -->\n @if (showPagination()) {\n <div class=\"d-flex justify-content-center mt-3 p-3\">\n <ngb-pagination\n [collectionSize]=\"labGroupsData().count\"\n [page]=\"currentPage()\"\n [pageSize]=\"pageSize()\"\n [maxSize]=\"5\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onPageChange($event)\"\n class=\"pagination-sm\">\n </ngb-pagination>\n </div>\n }\n } @else {\n <div class=\"text-center py-5\">\n <div class=\"text-muted\">\n <i class=\"bi bi-people display-1 mb-3\"></i>\n <h5>No Lab Groups Found</h5>\n <p>\n @if (hasSearchValue()) {\n Try adjusting your search criteria or create a new lab group.\n } @else {\n Get started by creating your first lab group.\n }\n </p>\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"toggleCreateForm()\">\n <i class=\"bi bi-plus me-1\"></i>Create First Lab Group\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n\n<!-- Group Members Modal -->\n@if (selectedGroupForMembers()) {\n <div class=\"modal-backdrop fade show\"></div>\n <div class=\"modal fade show d-block\" tabindex=\"-1\">\n <div class=\"modal-dialog modal-lg\">\n <div class=\"modal-content\">\n <div class=\"modal-header\">\n <h5 class=\"modal-title\">\n <i class=\"bi bi-people me-2\"></i>{{ currentGroupName() }} - Members\n </h5>\n <button type=\"button\" class=\"btn-close\" (click)=\"closeGroupDetails()\"></button>\n </div>\n <div class=\"modal-body\">\n <!-- Invite Form -->\n @if (canInviteToCurrentGroup()) {\n <div class=\"card mb-4\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-envelope me-2\"></i>Invite Member\n </h6>\n <button \n type=\"button\"\n class=\"btn btn-sm btn-outline-primary\" \n (click)=\"toggleInviteForm()\">\n @if (showInviteForm()) {\n Cancel\n } @else {\n <i class=\"bi bi-plus me-1\"></i>Invite\n }\n </button>\n </div>\n @if (showInviteForm()) {\n <div class=\"card-body\">\n <form [formGroup]=\"inviteForm\" (ngSubmit)=\"inviteMember()\" class=\"row g-3\">\n <div class=\"col-md-8\">\n <div class=\"form-floating\">\n <input \n type=\"email\" \n class=\"form-control\" \n id=\"inviteEmail\" \n formControlName=\"invitedEmail\"\n [class.is-invalid]=\"inviteForm.get('invitedEmail')?.invalid && inviteForm.get('invitedEmail')?.touched\"\n placeholder=\"Email address\">\n <label for=\"inviteEmail\">Email Address *</label>\n </div>\n @if (inviteForm.get('invitedEmail')?.invalid && inviteForm.get('invitedEmail')?.touched) {\n <div class=\"invalid-feedback\">\n @if (inviteForm.get('invitedEmail')?.errors?.['required']) {\n Email is required\n }\n @if (inviteForm.get('invitedEmail')?.errors?.['email']) {\n Please enter a valid email address\n }\n </div>\n }\n </div>\n <div class=\"col-md-4\">\n <button \n type=\"submit\" \n class=\"btn btn-primary w-100 h-100\"\n [disabled]=\"inviteForm.invalid || isInviting()\">\n @if (isInviting()) {\n <span class=\"spinner-border spinner-border-sm me-2\" role=\"status\"></span>\n }\n Send Invite\n </button>\n </div>\n <div class=\"col-12\">\n <div class=\"form-floating\">\n <textarea \n class=\"form-control\" \n id=\"inviteMessage\" \n formControlName=\"message\"\n style=\"height: 80px\"\n placeholder=\"Message\"></textarea>\n <label for=\"inviteMessage\">Message (optional)</label>\n </div>\n </div>\n </form>\n </div>\n }\n </div>\n }\n\n <!-- Current Members -->\n <div class=\"card mb-3\">\n <div class=\"card-header\">\n <div class=\"d-flex justify-content-between align-items-center\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-people me-2\"></i>Current Members ({{ groupMembersCount() }})\n </h6>\n @if (hasSubGroups()) {\n <div class=\"form-check form-switch\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"directMembersOnly\"\n [checked]=\"directMembersOnly()\"\n (change)=\"toggleDirectMembersOnly()\">\n <label class=\"form-check-label small\" for=\"directMembersOnly\">\n Direct only\n </label>\n </div>\n }\n </div>\n </div>\n <div class=\"card-body p-0\">\n @if (hasGroupMembers()) {\n <div class=\"list-group list-group-flush\">\n @for (member of groupMembers(); track member.id) {\n <div class=\"list-group-item d-flex justify-content-between align-items-center\">\n <div>\n <strong>{{ member.firstName }} {{ member.lastName }}</strong>\n <br>\n <small class=\"text-muted\">{{ member.email }}</small>\n @if (selectedGroupForMembers() && member.id === selectedGroupForMembers()!.creator) {\n <span class=\"badge bg-warning ms-2\">Creator</span>\n }\n </div>\n @if (canManageCurrentGroup() && selectedGroupForMembers() && member.id !== selectedGroupForMembers()!.creator) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger btn-sm\" \n (click)=\"removeMember(member.id)\"\n title=\"Remove member\">\n <i class=\"bi bi-person-dash\"></i>\n </button>\n }\n </div>\n }\n </div>\n } @else {\n <div class=\"text-center py-3 text-muted\">\n <i class=\"bi bi-people fs-4 mb-2\"></i>\n <div>No members found</div>\n </div>\n }\n </div>\n @if (showMemberPagination()) {\n <div class=\"card-footer\">\n <div class=\"d-flex justify-content-center\">\n <ngb-pagination\n [collectionSize]=\"memberTotal()\"\n [page]=\"memberPage()\"\n [pageSize]=\"memberPageSize()\"\n [maxSize]=\"5\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onMemberPageChange($event)\"\n class=\"pagination-sm mb-0\">\n </ngb-pagination>\n </div>\n </div>\n }\n </div>\n\n <!-- Pending Invitations -->\n @if (hasPendingInvitations()) {\n <div class=\"card\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-clock me-2\"></i>Pending Invitations ({{ pendingInvitationsCount() }})\n </h6>\n </div>\n <div class=\"card-body p-0\">\n <div class=\"list-group list-group-flush\">\n @for (invitation of pendingInvitations(); track invitation.id) {\n <div class=\"list-group-item d-flex justify-content-between align-items-center\">\n <div>\n <strong>{{ invitation.invitedEmail }}</strong>\n <br>\n <small class=\"text-muted\">\n Invited by {{ invitation.inviterName }} on {{ invitation.createdAt | date:'short' }}\n </small>\n @if (invitation.message) {\n <br>\n <small class=\"text-info\">{{ invitation.message }}</small>\n }\n </div>\n @if (canManageCurrentGroup()) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger btn-sm\" \n (click)=\"cancelInvitation(invitation.id)\"\n title=\"Cancel invitation\">\n <i class=\"bi bi-x-circle\"></i>\n </button>\n }\n </div>\n }\n </div>\n </div>\n </div>\n }\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"closeGroupDetails()\">\n Close\n </button>\n </div>\n </div>\n </div>\n </div>\n}", styles: [".lab-groups-container .groups-content{max-width:1200px;margin:0 auto}@media (max-width: 768px){.lab-groups-container .groups-content{padding:1rem!important}.lab-groups-container .btn-group{flex-direction:column;width:100%}.lab-groups-container .btn-group .btn{border-radius:.25rem!important;margin-bottom:.25rem}.lab-groups-container .modal-dialog{margin:.5rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: NgbModule }, { kind: "component", type: i2$1.NgbPagination, selector: "ngb-pagination", inputs: ["disabled", "boundaryLinks", "directionLinks", "ellipses", "rotate", "collectionSize", "maxSize", "page", "pageSize", "size"], outputs: ["pageChange"] }, { kind: "pipe", type: i2.DatePipe, name: "date" }] });
2833
2873
  }
2834
2874
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: LabGroupsComponent, decorators: [{
2835
2875
  type: Component,
2836
- args: [{ selector: 'ccc-lab-groups', standalone: true, imports: [CommonModule, ReactiveFormsModule, NgbModule], template: "<div class=\"lab-groups-container\">\n <!-- Header Section -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100\">\n <div class=\"d-flex align-items-center\">\n <h4 class=\"navbar-brand m-0 fw-bold\">\n <i class=\"bi bi-people me-2\"></i>Lab Groups\n </h4>\n </div>\n <div class=\"d-flex gap-2\">\n <button type=\"button\" class=\"btn btn-primary btn-sm\" (click)=\"toggleCreateForm()\">\n <i class=\"bi bi-plus me-1\"></i>New Lab Group\n </button>\n </div>\n </div>\n </div>\n </nav>\n\n <div class=\"groups-content p-4\">\n <!-- Create Group Form -->\n @if (showCreateForm()) {\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-plus-circle me-2\"></i>Create New Lab Group\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"createGroupForm\" (ngSubmit)=\"createLabGroup()\" class=\"row g-3\">\n <div class=\"col-md-6\">\n <div class=\"form-floating\">\n <input \n type=\"text\" \n class=\"form-control\" \n id=\"groupName\" \n formControlName=\"name\"\n [class.is-invalid]=\"createGroupForm.get('name')?.invalid && createGroupForm.get('name')?.touched\"\n placeholder=\"Group name\">\n <label for=\"groupName\">Group Name *</label>\n </div>\n @if (createGroupForm.get('name')?.invalid && createGroupForm.get('name')?.touched) {\n <div class=\"invalid-feedback\">\n @if (createGroupForm.get('name')?.errors?.['required']) {\n Group name is required\n }\n @if (createGroupForm.get('name')?.errors?.['minlength']) {\n Group name must be at least 2 characters\n }\n </div>\n }\n </div>\n <div class=\"col-md-6\">\n <div class=\"form-check form-switch mt-3\">\n <input \n class=\"form-check-input\" \n type=\"checkbox\" \n id=\"allowInvites\" \n formControlName=\"allowMemberInvites\">\n <label class=\"form-check-label\" for=\"allowInvites\">\n <i class=\"bi bi-envelope me-1\"></i>Allow members to invite others\n </label>\n </div>\n </div>\n <div class=\"col-12\">\n <div class=\"form-floating\">\n <textarea \n class=\"form-control\" \n id=\"groupDescription\" \n formControlName=\"description\"\n style=\"height: 100px\"\n placeholder=\"Description\"></textarea>\n <label for=\"groupDescription\">Description (optional)</label>\n </div>\n </div>\n <div class=\"col-12\">\n <div class=\"d-flex gap-2\">\n <button \n type=\"submit\" \n class=\"btn btn-primary\"\n [disabled]=\"createGroupForm.invalid || isCreatingGroup()\">\n @if (isCreatingGroup()) {\n <span class=\"spinner-border spinner-border-sm me-2\" role=\"status\"></span>\n }\n Create Group\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"toggleCreateForm()\">\n Cancel\n </button>\n </div>\n </div>\n </form>\n </div>\n </div>\n }\n\n <!-- Search & Filters Section -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-search me-2\"></i>Search & Filters\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"searchForm\" class=\"row g-3\">\n <div class=\"col-md-12\">\n <div class=\"form-floating\">\n <input \n type=\"search\" \n class=\"form-control\" \n id=\"groupSearch\" \n formControlName=\"search\"\n placeholder=\"Search groups...\">\n <label for=\"groupSearch\">\n <i class=\"bi bi-search me-1\"></i>Search Groups\n </label>\n </div>\n </div>\n </form>\n </div>\n </div>\n\n <!-- Groups List -->\n <div class=\"card\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-people me-2\"></i>Lab Groups\n <span class=\"badge bg-primary ms-2\">{{ labGroupsData().count }}</span>\n </h5>\n @if (isLoading()) {\n <div class=\"spinner-border spinner-border-sm text-primary\" role=\"status\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n }\n </div>\n <div class=\"card-body p-0\">\n @if (hasLabGroups()) {\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\"><i class=\"bi bi-tag me-1\"></i>Group Name</th>\n <th scope=\"col\"><i class=\"bi bi-person me-1\"></i>Creator</th>\n <th scope=\"col\"><i class=\"bi bi-people me-1\"></i>Members</th>\n <th scope=\"col\"><i class=\"bi bi-check-circle me-1\"></i>Status</th>\n <th scope=\"col\"><i class=\"bi bi-calendar me-1\"></i>Created</th>\n <th scope=\"col\" style=\"width: 120px;\"><i class=\"bi bi-gear me-1\"></i>Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (group of labGroupsData().results; track group.id) {\n <tr>\n <td>\n <strong>{{ group.name }}</strong>\n @if (group.description) {\n <br><small class=\"text-muted\">{{ group.description }}</small>\n }\n </td>\n <td>{{ group.creatorName || 'Unknown' }}</td>\n <td>\n <span class=\"badge bg-info\">{{ group.memberCount }}</span>\n </td>\n <td>\n @if (group.isActive) {\n <span class=\"badge bg-success\">Active</span>\n } @else {\n <span class=\"badge bg-secondary\">Inactive</span>\n }\n </td>\n <td>\n <small>{{ group.createdAt | date:'short' }}</small>\n </td>\n <td>\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n <button \n type=\"button\"\n class=\"btn btn-outline-info\" \n (click)=\"viewGroupMembers(group)\"\n title=\"View members\">\n <i class=\"bi bi-people\"></i>\n </button>\n @if (group.isMember && !group.isCreator) {\n <button \n type=\"button\"\n class=\"btn btn-outline-warning\" \n (click)=\"leaveGroup(group)\"\n title=\"Leave group\">\n <i class=\"bi bi-box-arrow-right\"></i>\n </button>\n }\n @if (group.canManage) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger\" \n (click)=\"deleteGroup(group)\"\n title=\"Delete group\">\n <i class=\"bi bi-trash\"></i>\n </button>\n }\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <!-- Pagination -->\n @if (showPagination()) {\n <div class=\"d-flex justify-content-center mt-3 p-3\">\n <ngb-pagination\n [collectionSize]=\"labGroupsData().count\"\n [page]=\"currentPage()\"\n [pageSize]=\"pageSize()\"\n [maxSize]=\"5\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onPageChange($event)\"\n class=\"pagination-sm\">\n </ngb-pagination>\n </div>\n }\n } @else {\n <div class=\"text-center py-5\">\n <div class=\"text-muted\">\n <i class=\"bi bi-people display-1 mb-3\"></i>\n <h5>No Lab Groups Found</h5>\n <p>\n @if (hasSearchValue()) {\n Try adjusting your search criteria or create a new lab group.\n } @else {\n Get started by creating your first lab group.\n }\n </p>\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"toggleCreateForm()\">\n <i class=\"bi bi-plus me-1\"></i>Create First Lab Group\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n\n<!-- Group Members Modal -->\n@if (selectedGroupForMembers()) {\n <div class=\"modal-backdrop fade show\"></div>\n <div class=\"modal fade show d-block\" tabindex=\"-1\">\n <div class=\"modal-dialog modal-lg\">\n <div class=\"modal-content\">\n <div class=\"modal-header\">\n <h5 class=\"modal-title\">\n <i class=\"bi bi-people me-2\"></i>{{ currentGroupName() }} - Members\n </h5>\n <button type=\"button\" class=\"btn-close\" (click)=\"closeGroupDetails()\"></button>\n </div>\n <div class=\"modal-body\">\n <!-- Invite Form -->\n @if (canInviteToCurrentGroup()) {\n <div class=\"card mb-4\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-envelope me-2\"></i>Invite Member\n </h6>\n <button \n type=\"button\"\n class=\"btn btn-sm btn-outline-primary\" \n (click)=\"toggleInviteForm()\">\n @if (showInviteForm()) {\n Cancel\n } @else {\n <i class=\"bi bi-plus me-1\"></i>Invite\n }\n </button>\n </div>\n @if (showInviteForm()) {\n <div class=\"card-body\">\n <form [formGroup]=\"inviteForm\" (ngSubmit)=\"inviteMember()\" class=\"row g-3\">\n <div class=\"col-md-8\">\n <div class=\"form-floating\">\n <input \n type=\"email\" \n class=\"form-control\" \n id=\"inviteEmail\" \n formControlName=\"invitedEmail\"\n [class.is-invalid]=\"inviteForm.get('invitedEmail')?.invalid && inviteForm.get('invitedEmail')?.touched\"\n placeholder=\"Email address\">\n <label for=\"inviteEmail\">Email Address *</label>\n </div>\n @if (inviteForm.get('invitedEmail')?.invalid && inviteForm.get('invitedEmail')?.touched) {\n <div class=\"invalid-feedback\">\n @if (inviteForm.get('invitedEmail')?.errors?.['required']) {\n Email is required\n }\n @if (inviteForm.get('invitedEmail')?.errors?.['email']) {\n Please enter a valid email address\n }\n </div>\n }\n </div>\n <div class=\"col-md-4\">\n <button \n type=\"submit\" \n class=\"btn btn-primary w-100 h-100\"\n [disabled]=\"inviteForm.invalid || isInviting()\">\n @if (isInviting()) {\n <span class=\"spinner-border spinner-border-sm me-2\" role=\"status\"></span>\n }\n Send Invite\n </button>\n </div>\n <div class=\"col-12\">\n <div class=\"form-floating\">\n <textarea \n class=\"form-control\" \n id=\"inviteMessage\" \n formControlName=\"message\"\n style=\"height: 80px\"\n placeholder=\"Message\"></textarea>\n <label for=\"inviteMessage\">Message (optional)</label>\n </div>\n </div>\n </form>\n </div>\n }\n </div>\n }\n\n <!-- Current Members -->\n <div class=\"card mb-3\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-people me-2\"></i>Current Members ({{ groupMembersCount() }})\n </h6>\n </div>\n <div class=\"card-body p-0\">\n @if (hasGroupMembers()) {\n <div class=\"list-group list-group-flush\">\n @for (member of groupMembers(); track member.id) {\n <div class=\"list-group-item d-flex justify-content-between align-items-center\">\n <div>\n <strong>{{ member.firstName }} {{ member.lastName }}</strong>\n <br>\n <small class=\"text-muted\">{{ member.email }}</small>\n @if (selectedGroupForMembers() && member.id === selectedGroupForMembers()!.creator) {\n <span class=\"badge bg-warning ms-2\">Creator</span>\n }\n </div>\n @if (canManageCurrentGroup() && selectedGroupForMembers() && member.id !== selectedGroupForMembers()!.creator) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger btn-sm\" \n (click)=\"removeMember(member.id)\"\n title=\"Remove member\">\n <i class=\"bi bi-person-dash\"></i>\n </button>\n }\n </div>\n }\n </div>\n } @else {\n <div class=\"text-center py-3 text-muted\">\n <i class=\"bi bi-people fs-4 mb-2\"></i>\n <div>No members found</div>\n </div>\n }\n </div>\n </div>\n\n <!-- Pending Invitations -->\n @if (hasPendingInvitations()) {\n <div class=\"card\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-clock me-2\"></i>Pending Invitations ({{ pendingInvitationsCount() }})\n </h6>\n </div>\n <div class=\"card-body p-0\">\n <div class=\"list-group list-group-flush\">\n @for (invitation of pendingInvitations(); track invitation.id) {\n <div class=\"list-group-item d-flex justify-content-between align-items-center\">\n <div>\n <strong>{{ invitation.invitedEmail }}</strong>\n <br>\n <small class=\"text-muted\">\n Invited by {{ invitation.inviterName }} on {{ invitation.createdAt | date:'short' }}\n </small>\n @if (invitation.message) {\n <br>\n <small class=\"text-info\">{{ invitation.message }}</small>\n }\n </div>\n @if (canManageCurrentGroup()) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger btn-sm\" \n (click)=\"cancelInvitation(invitation.id)\"\n title=\"Cancel invitation\">\n <i class=\"bi bi-x-circle\"></i>\n </button>\n }\n </div>\n }\n </div>\n </div>\n </div>\n }\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"closeGroupDetails()\">\n Close\n </button>\n </div>\n </div>\n </div>\n </div>\n}", styles: [".lab-groups-container .groups-content{max-width:1200px;margin:0 auto}@media (max-width: 768px){.lab-groups-container .groups-content{padding:1rem!important}.lab-groups-container .btn-group{flex-direction:column;width:100%}.lab-groups-container .btn-group .btn{border-radius:.25rem!important;margin-bottom:.25rem}.lab-groups-container .modal-dialog{margin:.5rem}}\n"] }]
2876
+ args: [{ selector: 'ccc-lab-groups', standalone: true, imports: [CommonModule, ReactiveFormsModule, NgbModule], template: "<div class=\"lab-groups-container\">\n <!-- Header Section -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100\">\n <div class=\"d-flex align-items-center\">\n <h4 class=\"navbar-brand m-0 fw-bold\">\n <i class=\"bi bi-people me-2\"></i>Lab Groups\n </h4>\n </div>\n <div class=\"d-flex gap-2\">\n <button type=\"button\" class=\"btn btn-primary btn-sm\" (click)=\"toggleCreateForm()\">\n <i class=\"bi bi-plus me-1\"></i>New Lab Group\n </button>\n </div>\n </div>\n </div>\n </nav>\n\n <div class=\"groups-content p-4\">\n <!-- Create Group Form -->\n @if (showCreateForm()) {\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-plus-circle me-2\"></i>Create New Lab Group\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"createGroupForm\" (ngSubmit)=\"createLabGroup()\" class=\"row g-3\">\n <div class=\"col-md-6\">\n <div class=\"form-floating\">\n <input \n type=\"text\" \n class=\"form-control\" \n id=\"groupName\" \n formControlName=\"name\"\n [class.is-invalid]=\"createGroupForm.get('name')?.invalid && createGroupForm.get('name')?.touched\"\n placeholder=\"Group name\">\n <label for=\"groupName\">Group Name *</label>\n </div>\n @if (createGroupForm.get('name')?.invalid && createGroupForm.get('name')?.touched) {\n <div class=\"invalid-feedback\">\n @if (createGroupForm.get('name')?.errors?.['required']) {\n Group name is required\n }\n @if (createGroupForm.get('name')?.errors?.['minlength']) {\n Group name must be at least 2 characters\n }\n </div>\n }\n </div>\n <div class=\"col-md-6\">\n <div class=\"form-check form-switch mt-3\">\n <input \n class=\"form-check-input\" \n type=\"checkbox\" \n id=\"allowInvites\" \n formControlName=\"allowMemberInvites\">\n <label class=\"form-check-label\" for=\"allowInvites\">\n <i class=\"bi bi-envelope me-1\"></i>Allow members to invite others\n </label>\n </div>\n </div>\n <div class=\"col-12\">\n <div class=\"form-floating\">\n <textarea \n class=\"form-control\" \n id=\"groupDescription\" \n formControlName=\"description\"\n style=\"height: 100px\"\n placeholder=\"Description\"></textarea>\n <label for=\"groupDescription\">Description (optional)</label>\n </div>\n </div>\n <div class=\"col-12\">\n <div class=\"d-flex gap-2\">\n <button \n type=\"submit\" \n class=\"btn btn-primary\"\n [disabled]=\"createGroupForm.invalid || isCreatingGroup()\">\n @if (isCreatingGroup()) {\n <span class=\"spinner-border spinner-border-sm me-2\" role=\"status\"></span>\n }\n Create Group\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"toggleCreateForm()\">\n Cancel\n </button>\n </div>\n </div>\n </form>\n </div>\n </div>\n }\n\n <!-- Search & Filters Section -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-search me-2\"></i>Search & Filters\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"searchForm\" class=\"row g-3\">\n <div class=\"col-md-12\">\n <div class=\"form-floating\">\n <input \n type=\"search\" \n class=\"form-control\" \n id=\"groupSearch\" \n formControlName=\"search\"\n placeholder=\"Search groups...\">\n <label for=\"groupSearch\">\n <i class=\"bi bi-search me-1\"></i>Search Groups\n </label>\n </div>\n </div>\n </form>\n </div>\n </div>\n\n <!-- Groups List -->\n <div class=\"card\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-people me-2\"></i>Lab Groups\n <span class=\"badge bg-primary ms-2\">{{ labGroupsData().count }}</span>\n </h5>\n @if (isLoading()) {\n <div class=\"spinner-border spinner-border-sm text-primary\" role=\"status\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n }\n </div>\n <div class=\"card-body p-0\">\n @if (hasLabGroups()) {\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\"><i class=\"bi bi-tag me-1\"></i>Group Name</th>\n <th scope=\"col\"><i class=\"bi bi-person me-1\"></i>Creator</th>\n <th scope=\"col\"><i class=\"bi bi-people me-1\"></i>Members</th>\n <th scope=\"col\"><i class=\"bi bi-check-circle me-1\"></i>Status</th>\n <th scope=\"col\"><i class=\"bi bi-calendar me-1\"></i>Created</th>\n <th scope=\"col\" style=\"width: 120px;\"><i class=\"bi bi-gear me-1\"></i>Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (group of labGroupsData().results; track group.id) {\n <tr>\n <td>\n <strong>{{ group.name }}</strong>\n @if (group.description) {\n <br><small class=\"text-muted\">{{ group.description }}</small>\n }\n </td>\n <td>{{ group.creatorName || 'Unknown' }}</td>\n <td>\n <span class=\"badge bg-info\">{{ group.memberCount }}</span>\n </td>\n <td>\n @if (group.isActive) {\n <span class=\"badge bg-success\">Active</span>\n } @else {\n <span class=\"badge bg-secondary\">Inactive</span>\n }\n </td>\n <td>\n <small>{{ group.createdAt | date:'short' }}</small>\n </td>\n <td>\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n <button \n type=\"button\"\n class=\"btn btn-outline-info\" \n (click)=\"viewGroupMembers(group)\"\n title=\"View members\">\n <i class=\"bi bi-people\"></i>\n </button>\n @if (group.isMember && !group.isCreator) {\n <button \n type=\"button\"\n class=\"btn btn-outline-warning\" \n (click)=\"leaveGroup(group)\"\n title=\"Leave group\">\n <i class=\"bi bi-box-arrow-right\"></i>\n </button>\n }\n @if (group.canManage) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger\" \n (click)=\"deleteGroup(group)\"\n title=\"Delete group\">\n <i class=\"bi bi-trash\"></i>\n </button>\n }\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <!-- Pagination -->\n @if (showPagination()) {\n <div class=\"d-flex justify-content-center mt-3 p-3\">\n <ngb-pagination\n [collectionSize]=\"labGroupsData().count\"\n [page]=\"currentPage()\"\n [pageSize]=\"pageSize()\"\n [maxSize]=\"5\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onPageChange($event)\"\n class=\"pagination-sm\">\n </ngb-pagination>\n </div>\n }\n } @else {\n <div class=\"text-center py-5\">\n <div class=\"text-muted\">\n <i class=\"bi bi-people display-1 mb-3\"></i>\n <h5>No Lab Groups Found</h5>\n <p>\n @if (hasSearchValue()) {\n Try adjusting your search criteria or create a new lab group.\n } @else {\n Get started by creating your first lab group.\n }\n </p>\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"toggleCreateForm()\">\n <i class=\"bi bi-plus me-1\"></i>Create First Lab Group\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n\n<!-- Group Members Modal -->\n@if (selectedGroupForMembers()) {\n <div class=\"modal-backdrop fade show\"></div>\n <div class=\"modal fade show d-block\" tabindex=\"-1\">\n <div class=\"modal-dialog modal-lg\">\n <div class=\"modal-content\">\n <div class=\"modal-header\">\n <h5 class=\"modal-title\">\n <i class=\"bi bi-people me-2\"></i>{{ currentGroupName() }} - Members\n </h5>\n <button type=\"button\" class=\"btn-close\" (click)=\"closeGroupDetails()\"></button>\n </div>\n <div class=\"modal-body\">\n <!-- Invite Form -->\n @if (canInviteToCurrentGroup()) {\n <div class=\"card mb-4\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-envelope me-2\"></i>Invite Member\n </h6>\n <button \n type=\"button\"\n class=\"btn btn-sm btn-outline-primary\" \n (click)=\"toggleInviteForm()\">\n @if (showInviteForm()) {\n Cancel\n } @else {\n <i class=\"bi bi-plus me-1\"></i>Invite\n }\n </button>\n </div>\n @if (showInviteForm()) {\n <div class=\"card-body\">\n <form [formGroup]=\"inviteForm\" (ngSubmit)=\"inviteMember()\" class=\"row g-3\">\n <div class=\"col-md-8\">\n <div class=\"form-floating\">\n <input \n type=\"email\" \n class=\"form-control\" \n id=\"inviteEmail\" \n formControlName=\"invitedEmail\"\n [class.is-invalid]=\"inviteForm.get('invitedEmail')?.invalid && inviteForm.get('invitedEmail')?.touched\"\n placeholder=\"Email address\">\n <label for=\"inviteEmail\">Email Address *</label>\n </div>\n @if (inviteForm.get('invitedEmail')?.invalid && inviteForm.get('invitedEmail')?.touched) {\n <div class=\"invalid-feedback\">\n @if (inviteForm.get('invitedEmail')?.errors?.['required']) {\n Email is required\n }\n @if (inviteForm.get('invitedEmail')?.errors?.['email']) {\n Please enter a valid email address\n }\n </div>\n }\n </div>\n <div class=\"col-md-4\">\n <button \n type=\"submit\" \n class=\"btn btn-primary w-100 h-100\"\n [disabled]=\"inviteForm.invalid || isInviting()\">\n @if (isInviting()) {\n <span class=\"spinner-border spinner-border-sm me-2\" role=\"status\"></span>\n }\n Send Invite\n </button>\n </div>\n <div class=\"col-12\">\n <div class=\"form-floating\">\n <textarea \n class=\"form-control\" \n id=\"inviteMessage\" \n formControlName=\"message\"\n style=\"height: 80px\"\n placeholder=\"Message\"></textarea>\n <label for=\"inviteMessage\">Message (optional)</label>\n </div>\n </div>\n </form>\n </div>\n }\n </div>\n }\n\n <!-- Current Members -->\n <div class=\"card mb-3\">\n <div class=\"card-header\">\n <div class=\"d-flex justify-content-between align-items-center\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-people me-2\"></i>Current Members ({{ groupMembersCount() }})\n </h6>\n @if (hasSubGroups()) {\n <div class=\"form-check form-switch\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"directMembersOnly\"\n [checked]=\"directMembersOnly()\"\n (change)=\"toggleDirectMembersOnly()\">\n <label class=\"form-check-label small\" for=\"directMembersOnly\">\n Direct only\n </label>\n </div>\n }\n </div>\n </div>\n <div class=\"card-body p-0\">\n @if (hasGroupMembers()) {\n <div class=\"list-group list-group-flush\">\n @for (member of groupMembers(); track member.id) {\n <div class=\"list-group-item d-flex justify-content-between align-items-center\">\n <div>\n <strong>{{ member.firstName }} {{ member.lastName }}</strong>\n <br>\n <small class=\"text-muted\">{{ member.email }}</small>\n @if (selectedGroupForMembers() && member.id === selectedGroupForMembers()!.creator) {\n <span class=\"badge bg-warning ms-2\">Creator</span>\n }\n </div>\n @if (canManageCurrentGroup() && selectedGroupForMembers() && member.id !== selectedGroupForMembers()!.creator) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger btn-sm\" \n (click)=\"removeMember(member.id)\"\n title=\"Remove member\">\n <i class=\"bi bi-person-dash\"></i>\n </button>\n }\n </div>\n }\n </div>\n } @else {\n <div class=\"text-center py-3 text-muted\">\n <i class=\"bi bi-people fs-4 mb-2\"></i>\n <div>No members found</div>\n </div>\n }\n </div>\n @if (showMemberPagination()) {\n <div class=\"card-footer\">\n <div class=\"d-flex justify-content-center\">\n <ngb-pagination\n [collectionSize]=\"memberTotal()\"\n [page]=\"memberPage()\"\n [pageSize]=\"memberPageSize()\"\n [maxSize]=\"5\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onMemberPageChange($event)\"\n class=\"pagination-sm mb-0\">\n </ngb-pagination>\n </div>\n </div>\n }\n </div>\n\n <!-- Pending Invitations -->\n @if (hasPendingInvitations()) {\n <div class=\"card\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-clock me-2\"></i>Pending Invitations ({{ pendingInvitationsCount() }})\n </h6>\n </div>\n <div class=\"card-body p-0\">\n <div class=\"list-group list-group-flush\">\n @for (invitation of pendingInvitations(); track invitation.id) {\n <div class=\"list-group-item d-flex justify-content-between align-items-center\">\n <div>\n <strong>{{ invitation.invitedEmail }}</strong>\n <br>\n <small class=\"text-muted\">\n Invited by {{ invitation.inviterName }} on {{ invitation.createdAt | date:'short' }}\n </small>\n @if (invitation.message) {\n <br>\n <small class=\"text-info\">{{ invitation.message }}</small>\n }\n </div>\n @if (canManageCurrentGroup()) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger btn-sm\" \n (click)=\"cancelInvitation(invitation.id)\"\n title=\"Cancel invitation\">\n <i class=\"bi bi-x-circle\"></i>\n </button>\n }\n </div>\n }\n </div>\n </div>\n </div>\n }\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"closeGroupDetails()\">\n Close\n </button>\n </div>\n </div>\n </div>\n </div>\n}", styles: [".lab-groups-container .groups-content{max-width:1200px;margin:0 auto}@media (max-width: 768px){.lab-groups-container .groups-content{padding:1rem!important}.lab-groups-container .btn-group{flex-direction:column;width:100%}.lab-groups-container .btn-group .btn{border-radius:.25rem!important;margin-bottom:.25rem}.lab-groups-container .modal-dialog{margin:.5rem}}\n"] }]
2837
2877
  }], ctorParameters: () => [] });
2838
2878
 
2839
2879
  class UserProfileComponent {