diff --git a/public/i18n/en_US.json b/public/i18n/en_US.json index 810f4ce6..72a2845c 100644 --- a/public/i18n/en_US.json +++ b/public/i18n/en_US.json @@ -93,6 +93,8 @@ "Add Meter Readings to": "Add Meter Readings to", "Add Shared Properties": "Add Shared Properties", "Add Shared Tax Lots": "Add Shared Tax Lots", + "Add User": "Add User", + "Add User to Organization": "Add User to Organization", "Add a Document": "Add a Document", "Add a label": "Add a label", "Add another file": "Add another file", @@ -336,11 +338,13 @@ "Create Group": "Create Group", "Create Meter": "Create Meter", "Create New Sub-Organization": "Create New Sub-Organization", + "Create Organization": "Create Organization", "Create Profile": "Create Profile", "Create Project": "Create Project", "Create Service for System": "Create Service for System", "Create System": "Create System", "Create UBID": "Create UBID", + "Create User": "Create User", "Create Your Account": "Create Your Account", "Create a Custom Report to get started!": "Create a Custom Report to get started!", "Create a New Data Set.": "Create a New Data Set.", @@ -971,6 +975,7 @@ "No matching instances": "No matching instances", "No program created": "No program created", "No program selected": "No program selected", + "No users found in this organization": "No users found in this organization", "No warnings\/errors": "No warnings\/errors", "None": "None", "Not Compliant": "Not Compliant", @@ -1001,6 +1006,7 @@ "Or Create New (With User as Head):": "Or Create New (With User as Head):", "Organization": "Organization", "Organization Name": "Organization Name", + "Organization Name required": "Organization Name required", "Organization Name:": "Organization Name:", "Organization Owner(s)": "Organization Owner(s)", "Organization Settings": "Organization Settings", @@ -1166,6 +1172,7 @@ "Remove Service for System": "Remove Service for System", "Remove System": "Remove System", "Remove User": "Remove User", + "Remove User from Organizatio": "Remove User from Organizatio", "Remove buildings from project": "Remove buildings from project", "Remove inventory and organizations": "Remove inventory and organizations", "Removing buildings from project": "Removing buildings from project", @@ -1198,6 +1205,7 @@ "Review matches": "Review matches", "Right Axis": "Right Axis", "Right Half": "Right Half", + "Role": "Role", "Role:": "Role:", "Row 1": "Row 1", "Row 2": "Row 2", @@ -1591,8 +1599,10 @@ "User Last Name:": "User Last Name:", "User's email address. Used for auth as well.": "User's email address. Used for auth as well.", "Username": "Username", + "Users": "Users", "Using your current browser will prevent you from accessing features on our website. Use the links below to download a new browser or upgrade your existing browser.": "Using your current browser will prevent you from accessing features on our website. Use the links below to download a new browser or upgrade your existing browser.", "Valid": "Valid", + "Valid email required": "Valid email required", "Version": "Version", "View Compliance Tracking": "View Compliance Tracking", "View Project": "View Project", diff --git a/public/i18n/es.json b/public/i18n/es.json index 76da38a3..c1facf65 100644 --- a/public/i18n/es.json +++ b/public/i18n/es.json @@ -93,6 +93,8 @@ "Add Meter Readings to": "Agregar lecturas del medidor a", "Add Shared Properties": "Añadir propiedades compartidas", "Add Shared Tax Lots": "Añadir lotes fiscales compartidos", + "Add User": "Agregar usuario", + "Add User to Organization": "Agregar usuario a la organización", "Add a Document": "Añadir un documento", "Add a label": "Añadir una etiqueta", "Add another file": "Añadir otro archivo", @@ -336,11 +338,13 @@ "Create Group": "Crear grupo", "Create Meter": "Crear medidor", "Create New Sub-Organization": "Crear nueva suborganización", + "Create Organization": "Crear organización", "Create Profile": "Crear perfil", "Create Project": "Crear proyecto", "Create Service for System": "Crear servicio para el sistema", "Create System": "Crear sistema", "Create UBID": "Crear UBID", + "Create User": "Crear usuario", "Create Your Account": "Cree su cuenta", "Create a Custom Report to get started!": "Cree un informe personalizado para empezar", "Create a New Data Set.": "Crear un nuevo conjunto de datos.", @@ -971,6 +975,7 @@ "No matching instances": "No hay instancias coincidentes", "No program created": "No se ha creado ningún programa", "No program selected": "Ningún programa seleccionado", + "No users found in this organization": "No se encontraron usuarios en esta organización.", "No warnings\/errors": "Sin advertencias\/errores", "None": "Ninguno", "Not Compliant": "No conforme", @@ -1001,6 +1006,7 @@ "Or Create New (With User as Head):": "O Crear Nuevo (Con Usuario como Cabecera):", "Organization": "Organización", "Organization Name": "Nombre de la organización", + "Organization Name required": "Se requiere el nombre de la organización.", "Organization Name:": "Nombre de la organización:", "Organization Owner(s)": "Organización Propietario(s)", "Organization Settings": "Configuración de la organización", @@ -1166,6 +1172,7 @@ "Remove Service for System": "Quitar servicio del sistema", "Remove System": "Quitar sistema", "Remove User": "Eliminar usuario", + "Remove User from Organizatio": "Eliminar usuario de la organización", "Remove buildings from project": "Retirar edificios del proyecto", "Remove inventory and organizations": "Eliminar inventario y organizaciones", "Removing buildings from project": "Retirada de edificios del proyecto", @@ -1198,6 +1205,7 @@ "Review matches": "Partidos de revisión", "Right Axis": "Eje derecho", "Right Half": "Mitad derecha", + "Role": "Role", "Role:": "Papel:", "Row 1": "Fila 1", "Row 2": "Fila 2", @@ -1591,8 +1599,10 @@ "User Last Name:": "Apellido del usuario:", "User's email address. Used for auth as well.": "Dirección de correo electrónico del usuario. También se utiliza para la autenticación.", "Username": "Nombre de usuario", + "Users": "Usuarios", "Using your current browser will prevent you from accessing features on our website. Use the links below to download a new browser or upgrade your existing browser.": "El uso de su navegador actual le impedirá acceder a las funciones de nuestro sitio web. Utilice los enlaces siguientes para descargar un nuevo navegador o actualizar el que ya tiene.", "Valid": "Válido", + "Valid email required": "Se requiere un correo electrónico válido.", "Version": "Versión", "View Compliance Tracking": "Ver el seguimiento del cumplimiento", "View Project": "Ver proyecto", diff --git a/public/i18n/fr_CA.json b/public/i18n/fr_CA.json index 4485debc..204cd610 100644 --- a/public/i18n/fr_CA.json +++ b/public/i18n/fr_CA.json @@ -93,6 +93,8 @@ "Add Meter Readings to": "Ajouter des relevés de compteur à", "Add Shared Properties": "Ajouter des propriétés partagées", "Add Shared Tax Lots": "Ajouter des lots d'impôt partagés", + "Add User": "Ajouter un utilisateur", + "Add User to Organization": "Ajouter l'utilisateur à l'organisation", "Add a Document": "Ajouter un document", "Add a label": "Ajouter une étiquette", "Add another file": "Ajouter un autre fichier", @@ -336,11 +338,13 @@ "Create Group": "Créer un groupe", "Create Meter": "Créer un compteur", "Create New Sub-Organization": "Créer nouvelle sous-organisation", + "Create Organization": "Créer une organisation", "Create Profile": "Créer un profil", "Create Project": "Créer un projet", "Create Service for System": "Créer un service pour le système", "Create System": "Créer un système", "Create UBID": "Créer un UBID", + "Create User": "Créer un utilisateur", "Create Your Account": "Créez votre compte", "Create a Custom Report to get started!": "Créez un rapport personnalisé pour commencer !", "Create a New Data Set.": "Créer un nouveau jeu de données.", @@ -971,6 +975,7 @@ "No matching instances": "Aucune instance correspondante", "No program created": "Aucun programme créé", "No program selected": "Aucun programme sélectionné", + "No users found in this organization": "Aucun utilisateur trouvé dans cette organisation", "No warnings\/errors": "Aucun avertissement\/erreur", "None": "Aucun", "Not Compliant": "Non conforme", @@ -1001,6 +1006,7 @@ "Or Create New (With User as Head):": "Ou Créer Nouveau (avec l'utilisateur en tant que chef):", "Organization": "Organisation", "Organization Name": "Nom de l'organisation", + "Organization Name required": "Nom de l'organisation requis", "Organization Name:": "Nom de l'organisation:", "Organization Owner(s)": "Propriétaire(s) de l'organisation", "Organization Settings": "Paramètres de l'organisation", @@ -1166,6 +1172,7 @@ "Remove Service for System": "Supprimer le service pour le système", "Remove System": "Supprimer le système", "Remove User": "Supprimer l'utilisateur", + "Remove User from Organizatio": "Supprimer l'utilisateur de l'organisation", "Remove buildings from project": "Supprimer les bâtiments du projet", "Remove inventory and organizations": "Supprimer l'inventaire et les organisations", "Removing buildings from project": "Retrait des bâtiments du projet", @@ -1198,6 +1205,7 @@ "Review matches": "Examiner les correspondences", "Right Axis": "Axe droit", "Right Half": "Moitié droite", + "Role": "Rôle", "Role:": "Rôle:", "Row 1": "Rangée 1", "Row 2": "Rangée 2", @@ -1591,8 +1599,10 @@ "User Last Name:": "Nom de famille de l'utilisateur:", "User's email address. Used for auth as well.": "Adresse e-mail de l'utilisateur. Utilisé également pour auth.", "Username": "Nom d'utilisateur", + "Users": "Utilisateurs", "Using your current browser will prevent you from accessing features on our website. Use the links below to download a new browser or upgrade your existing browser.": "L'utilisation de votre navigateur actuel vous empêchera d'accéder aux fonctionnalités de notre site Web. Utilisez les liens ci-dessous pour télécharger un nouveau navigateur ou mettre à niveau votre navigateur existant.", "Valid": "Valide", + "Valid email required": "Adresse e-mail valide requise", "Version": "Version", "View Compliance Tracking": "Afficher le suivi de la conformité", "View Project": "Voir le projet", diff --git a/src/@seed/api/column/column.service.ts b/src/@seed/api/column/column.service.ts index a647a424..2be0bb3d 100644 --- a/src/@seed/api/column/column.service.ts +++ b/src/@seed/api/column/column.service.ts @@ -6,7 +6,7 @@ import { catchError, map, ReplaySubject } from 'rxjs' import type { ProgressResponse } from '@seed/api' import { ErrorService } from '@seed/services/error/error.service' import { UserService } from '../user' -import type { Column, ColumnsResponse } from './column.types' +import type { Column, ColumnsResponse, RenameColumnResponse } from './column.types' @Injectable({ providedIn: 'root' }) export class ColumnService { @@ -86,4 +86,13 @@ export class ColumnService { }), ) } + + renameColumn(orgId: number, columnId: number, newColumnName: string, overwrite: boolean): Observable { + const url = `/api/v3/columns/${columnId}/rename/` + return this._httpClient.post(url, { organization_id: orgId, new_column_name: newColumnName, overwrite }).pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error renaming column') + }), + ) + } } diff --git a/src/@seed/api/column/column.types.ts b/src/@seed/api/column/column.types.ts index 448a5368..b943b69b 100644 --- a/src/@seed/api/column/column.types.ts +++ b/src/@seed/api/column/column.types.ts @@ -42,6 +42,11 @@ export type ColumnsResponse = { columns: Column[]; } +export type RenameColumnResponse = { + success: boolean; + message: string; +} + export type GenericColumn = { [key: string]: unknown; display_name: string; diff --git a/src/@seed/api/organization/organization.service.ts b/src/@seed/api/organization/organization.service.ts index 9bea1e4e..c40d3712 100644 --- a/src/@seed/api/organization/organization.service.ts +++ b/src/@seed/api/organization/organization.service.ts @@ -15,9 +15,13 @@ import type { AccessLevelsByDepth, AccessLevelTree, AccessLevelTreeResponse, + AddUserToOrgResponse, + AdminOrganization, + AdminOrganizationsResponse, BriefOrganization, CanDeleteInstanceResponse, CreateAccessLevelInstanceRequest, + CreateOrganizationResponse, EditAccessLevelInstanceRequest, EditAccessLevelInstanceResponse, FilterByViewsResponse, @@ -312,13 +316,60 @@ export class OrganizationService { switchMap((org: Organization) => this._inventoryService.getView(org.org_id, viewId, type).pipe(map((view) => ({ org, view })))), map(({ org, view }: { org: Organization; view: ViewResponse }) => { const displayFieldKey = type === 'taxlots' ? org.taxlot_display_field : org.property_display_field - const displayField = view.state[displayFieldKey] as string + // Check top-level state first, then extra_data (for extra data columns like "Nick Name") + const displayField = (view.state[displayFieldKey] ?? view.state.extra_data?.[displayFieldKey]) as string const defaultName = type === 'taxlots' ? `Tax Lot ${view.taxlot.id}` : `Property ${view.property.id}` return displayField || defaultName }), ) } + getAllOrganizations(): Observable { + const url = '/api/v3/organizations/' + return this._httpClient.get(url).pipe( + map(({ organizations }) => organizations.toSorted((a, b) => naturalSort(a.name, b.name))), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error fetching all organizations') + }), + ) + } + + createOrganization(userId: number, organizationName: string): Observable { + const url = '/api/v3/organizations/' + return this._httpClient.post(url, { user_id: userId, organization_name: organizationName }).pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error creating organization') + }), + ) + } + + deleteOrganization(orgId: number): Observable { + const url = `/api/v3/organizations/${orgId}/` + return this._httpClient.delete(url).pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error deleting organization') + }), + ) + } + + deleteOrganizationInventory(orgId: number): Observable { + const url = `/api/v3/organizations/${orgId}/inventory/` + return this._httpClient.delete(url).pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error deleting organization inventory') + }), + ) + } + + addUserToOrganization(orgId: number, userId: number): Observable { + const url = `/api/v3/organizations/${orgId}/users/${userId}/add/` + return this._httpClient.put(url, {}).pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error adding user to organization') + }), + ) + } + private _sortAccessLevelInstances(tree: AccessLevelInstance[]): AccessLevelInstance[] { return tree .map((instance) => ({ diff --git a/src/@seed/api/organization/organization.types.ts b/src/@seed/api/organization/organization.types.ts index e45a638c..50c2a62c 100644 --- a/src/@seed/api/organization/organization.types.ts +++ b/src/@seed/api/organization/organization.types.ts @@ -238,3 +238,26 @@ export type FilterByViewsResponse = { access_level_instance_ids: number[]; status: string; } + +export type AdminOrganization = { + id: number; + name: string; + created: string; + number_of_users: number; + user_count?: number; + property_count?: number; + taxlot_count?: number; +} + +export type AdminOrganizationsResponse = { + organizations: AdminOrganization[]; +} + +export type CreateOrganizationResponse = { + status: string; + organization: AdminOrganization; +} + +export type AddUserToOrgResponse = { + status: string; +} diff --git a/src/@seed/api/user/user.service.ts b/src/@seed/api/user/user.service.ts index 44d9f314..2a85a8ca 100644 --- a/src/@seed/api/user/user.service.ts +++ b/src/@seed/api/user/user.service.ts @@ -13,6 +13,7 @@ import type { SetDefaultOrganizationResponse, UserAuth, UserAuthResponse, + UserBrief, UserUpdateRequest, } from '@seed/api' import { ErrorService } from '@seed/services' @@ -166,6 +167,18 @@ export class UserService { ) } + /** + * Get all users (superuser endpoint) + */ + getAllUsers(): Observable { + return this._httpClient.get<{ users: UserBrief[] }>('/api/v3/users/').pipe( + map((response) => response.users), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error fetching all users') + }), + ) + } + // applies defaults to an org users settings checkUserSettings(userSettings: OrganizationUserSettings) { userSettings ??= {} diff --git a/src/@seed/api/user/user.types.ts b/src/@seed/api/user/user.types.ts index 8517b0ca..c1cf5c19 100644 --- a/src/@seed/api/user/user.types.ts +++ b/src/@seed/api/user/user.types.ts @@ -23,6 +23,11 @@ export type CurrentUser = { settings: OrganizationUserSettings; } +export type UserBrief = { + user_id: number; + email: string; +} + export type UserUpdateRequest = { first_name: string; last_name: string; diff --git a/src/@seed/components/page/page.component.html b/src/@seed/components/page/page.component.html index 3142e1b7..85eb2e49 100644 --- a/src/@seed/components/page/page.component.html +++ b/src/@seed/components/page/page.component.html @@ -28,7 +28,7 @@

@if (config.titleIcon) { - + } {{ t(config.title) }} @if (config.subTitle) { diff --git a/src/app/modules/datasets/dataset/dataset.component.html b/src/app/modules/datasets/dataset/dataset.component.html index 70b1fb8e..a2eb97f2 100644 --- a/src/app/modules/datasets/dataset/dataset.component.html +++ b/src/app/modules/datasets/dataset/dataset.component.html @@ -4,6 +4,12 @@ subTitle: datasetName$ | async, titleIcon: 'fa-solid:sitemap', breadcrumbs: ['Dataset', 'Detail'], + action: addDataFile, + actionIcon: 'fa-solid:plus', + actionText: 'Data File', + action2: addMeterData, + action2Icon: 'fa-solid:plus', + action2Text: 'Meter Data', }" >
diff --git a/src/app/modules/datasets/dataset/dataset.component.ts b/src/app/modules/datasets/dataset/dataset.component.ts index 9b45d8d4..533af46e 100644 --- a/src/app/modules/datasets/dataset/dataset.component.ts +++ b/src/app/modules/datasets/dataset/dataset.component.ts @@ -12,6 +12,8 @@ import { CycleService, DatasetService, UserService } from '@seed/api' import { DeleteModalComponent, PageComponent } from '@seed/components' import { ConfigService } from '@seed/services' import { naturalSort } from '@seed/utils' +import { DataUploadModalComponent } from '../data-upload/data-upload-modal.component' +import { MeterDataUploadModalComponent } from '../data-upload/meter-upload-modal.component' @Component({ selector: 'seed-dataset', @@ -157,6 +159,36 @@ export class DatasetComponent implements OnDestroy, OnInit { .subscribe() } + addDataFile = () => { + if (!this.dataset || !this.cycles.length) return + this._dialog + .open(DataUploadModalComponent, { + width: '40rem', + data: { orgId: this.orgId, dataset: this.dataset, cycles: this.cycles }, + }) + .afterClosed() + .pipe( + filter(Boolean), + switchMap(() => this.getDataset()), + ) + .subscribe() + } + + addMeterData = () => { + if (!this.dataset || !this.cycles.length) return + this._dialog + .open(MeterDataUploadModalComponent, { + width: '60rem', + data: { orgId: this.orgId, datasetId: this.dataset.id, cycleId: this.cycles[0].id, file: null }, + }) + .afterClosed() + .pipe( + filter(Boolean), + switchMap(() => this.getDataset()), + ) + .subscribe() + } + downloadDocument(file: string, filename: string) { const a = document.createElement('a') // NOTE: downloads failing after a recent change. Requires further investigation diff --git a/src/app/modules/inventory-detail/detail/detail.component.html b/src/app/modules/inventory-detail/detail/detail.component.html index 329a2e6a..9b4524ae 100644 --- a/src/app/modules/inventory-detail/detail/detail.component.html +++ b/src/app/modules/inventory-detail/detail/detail.component.html @@ -1,7 +1,7 @@
Missing Site EUI
-
{{ dashboard['Views Missing Site EUI'] | number }}
+
+ {{ dashboard['Views Missing Site EUI'] | number }} +
Missing Gross Floor Area
-
{{ dashboard['Views Missing Gross Floor Area'] | number }}
+
+ {{ dashboard['Views Missing Gross Floor Area'] | number }} +

diff --git a/src/app/modules/inventory-list/list/grid/grid-controls.component.html b/src/app/modules/inventory-list/list/grid/grid-controls.component.html index 341c23e2..ce527d09 100644 --- a/src/app/modules/inventory-list/list/grid/grid-controls.component.html +++ b/src/app/modules/inventory-list/list/grid/grid-controls.component.html @@ -50,17 +50,17 @@ } @if (pagination) { {{ pagination.start }} - {{ pagination.end }} of {{ pagination.total }} -
+
-
+
Page {{ pagination.page }} of {{ pagination.num_pages }} -
+
-
+
} diff --git a/src/app/modules/organizations/columns/geocoding/geocoding.component.html b/src/app/modules/organizations/columns/geocoding/geocoding.component.html index f24ffc9a..b42b17bc 100644 --- a/src/app/modules/organizations/columns/geocoding/geocoding.component.html +++ b/src/app/modules/organizations/columns/geocoding/geocoding.component.html @@ -18,7 +18,7 @@
diff --git a/src/app/modules/organizations/columns/import-settings/import-settings.component.html b/src/app/modules/organizations/columns/import-settings/import-settings.component.html index 466d8abf..eb3412da 100644 --- a/src/app/modules/organizations/columns/import-settings/import-settings.component.html +++ b/src/app/modules/organizations/columns/import-settings/import-settings.component.html @@ -1,4 +1,4 @@ -
+
Exclude Columns From Uniqueness
@@ -12,7 +12,7 @@
{{ c.display_name }}
@@ -49,7 +49,7 @@
{{ c.display_name }}
@@ -87,7 +87,7 @@
{{ c.display_name }}
diff --git a/src/app/modules/organizations/columns/list/list.component.html b/src/app/modules/organizations/columns/list/list.component.html index 5d588a42..bb50ca11 100644 --- a/src/app/modules/organizations/columns/list/list.component.html +++ b/src/app/modules/organizations/columns/list/list.component.html @@ -36,14 +36,14 @@ Actions @if (c.is_extra_data) { } diff --git a/src/app/modules/organizations/columns/list/list.component.ts b/src/app/modules/organizations/columns/list/list.component.ts index e3bd84cd..d6ace998 100644 --- a/src/app/modules/organizations/columns/list/list.component.ts +++ b/src/app/modules/organizations/columns/list/list.component.ts @@ -1,12 +1,13 @@ import { Component, inject, type OnDestroy, ViewEncapsulation } from '@angular/core' import { MatDialog } from '@angular/material/dialog' import { MatTableDataSource } from '@angular/material/table' -import { Subject, takeUntil, tap } from 'rxjs' +import { filter, Subject, switchMap, takeUntil, tap } from 'rxjs' import type { Column, Organization } from '@seed/api' import { ColumnService, OrganizationService } from '@seed/api' import { SharedImports } from '@seed/directives' import { DeleteModalComponent } from './modal/delete-modal.component' import { FormModalComponent } from './modal/form-modal.component' +import { RenameModalComponent } from './modal/rename-modal.component' @Component({ selector: 'seed-organizations-columns-list-properties', @@ -51,7 +52,25 @@ export class ListComponent implements OnDestroy { } rename(column: Column) { - console.log('Rename called for column: ', column) + const allColumnNames = this.columnTableDataSource.data.map((c) => c.column_name) + const dialogRef = this._dialog.open(RenameModalComponent, { + width: '40rem', + data: { column, allColumnNames }, + }) + + dialogRef + .afterClosed() + .pipe( + takeUntil(this._unsubscribeAll$), + filter(Boolean), + switchMap(() => { + if (column.table_name === 'PropertyState') { + return this._columnService.getPropertyColumns(column.organization_id) + } + return this._columnService.getTaxLotColumns(column.organization_id) + }), + ) + .subscribe() } buildColumnList() { diff --git a/src/app/modules/organizations/columns/list/modal/rename-modal.component.html b/src/app/modules/organizations/columns/list/modal/rename-modal.component.html new file mode 100644 index 00000000..5bffcb02 --- /dev/null +++ b/src/app/modules/organizations/columns/list/modal/rename-modal.component.html @@ -0,0 +1,68 @@ +
+ +
Rename Column
+
+ + +@if (step === 1) { + + + Current Name + + + + + New Name + + + + @if (nameExists) { + A column with the name "{{ newName }}" already exists. Data will be merged if you proceed. + } + + @if (newName === data.column.column_name) { + The new name must be different from the current name. + } + + +
    +
  • This operation is irreversible.
  • +
  • The new column will replace the original column's data.
  • +
  • Default units will be applied to the renamed column.
  • +
  • This operation may take a long time for large datasets.
  • +
+
+ + @if (nameExists) { + Overwrite existing column data with this column's data? + } + + I acknowledge the above warnings + + @if (inProgress) { + + } +
+ +
+ + +
+} @else { + + @if (result?.success) { + {{ result.message }} + } @else { + {{ result?.message || 'An error occurred' }} + } + + + New Name + + + + +
+ +
+} diff --git a/src/app/modules/organizations/columns/list/modal/rename-modal.component.ts b/src/app/modules/organizations/columns/list/modal/rename-modal.component.ts index e69de29b..f0ef0a10 100644 --- a/src/app/modules/organizations/columns/list/modal/rename-modal.component.ts +++ b/src/app/modules/organizations/columns/list/modal/rename-modal.component.ts @@ -0,0 +1,66 @@ +import { Component, inject } from '@angular/core' +import { FormsModule } from '@angular/forms' +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog' +import type { Column, RenameColumnResponse } from '@seed/api' +import { ColumnService } from '@seed/api' +import { AlertComponent } from '@seed/components' +import { MaterialImports } from '@seed/materials' +import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' + +@Component({ + selector: 'seed-columns-rename-modal', + templateUrl: './rename-modal.component.html', + imports: [AlertComponent, FormsModule, MaterialImports], +}) +export class RenameModalComponent { + private _dialogRef = inject(MatDialogRef) + private _columnService = inject(ColumnService) + private _snackBar = inject(SnackBarService) + + data = inject(MAT_DIALOG_DATA) as { column: Column; allColumnNames: string[] } + + step = 1 + newName = '' + nameExists = false + userAcknowledgement = false + overwritePreference = false + inProgress = false + result: RenameColumnResponse | null = null + + checkNameExists() { + this.nameExists = this.data.allColumnNames.includes(this.newName) + if (!this.nameExists) { + this.overwritePreference = false + } + } + + isValid(): boolean { + if (!this.newName || this.newName === this.data.column.column_name) return false + if (this.nameExists) return this.userAcknowledgement && this.overwritePreference + return this.userAcknowledgement + } + + onSubmit() { + this.inProgress = true + this._columnService + .renameColumn(this.data.column.organization_id, this.data.column.id, this.newName, this.overwritePreference) + .subscribe({ + next: (response) => { + this.result = response + this.step = 2 + this.inProgress = false + if (response.success) { + this._snackBar.success('Column renamed successfully') + } + }, + error: () => { + this.inProgress = false + this._snackBar.alert('Failed to rename column') + }, + }) + } + + close(refresh = false) { + this._dialogRef.close(refresh) + } +} diff --git a/src/app/modules/organizations/columns/mappings/action-buttons.component.html b/src/app/modules/organizations/columns/mappings/action-buttons.component.html index ab8e071f..64018b79 100644 --- a/src/app/modules/organizations/columns/mappings/action-buttons.component.html +++ b/src/app/modules/organizations/columns/mappings/action-buttons.component.html @@ -1,6 +1,6 @@ diff --git a/src/app/modules/organizations/columns/mappings/mappings.component.html b/src/app/modules/organizations/columns/mappings/mappings.component.html index e43d05b3..24229135 100644 --- a/src/app/modules/organizations/columns/mappings/mappings.component.html +++ b/src/app/modules/organizations/columns/mappings/mappings.component.html @@ -23,13 +23,13 @@
} diff --git a/src/app/modules/organizations/columns/matching-criteria/criteria-list.component.html b/src/app/modules/organizations/columns/matching-criteria/criteria-list.component.html index 9b2f7563..a3fec29a 100644 --- a/src/app/modules/organizations/columns/matching-criteria/criteria-list.component.html +++ b/src/app/modules/organizations/columns/matching-criteria/criteria-list.component.html @@ -8,7 +8,7 @@
@if (canRemove(column)) { } @else { Locked diff --git a/src/app/modules/organizations/columns/matching-criteria/matching-criteria.component.html b/src/app/modules/organizations/columns/matching-criteria/matching-criteria.component.html index 294c9b98..bff957b5 100644 --- a/src/app/modules/organizations/columns/matching-criteria/matching-criteria.component.html +++ b/src/app/modules/organizations/columns/matching-criteria/matching-criteria.component.html @@ -2,6 +2,9 @@
Current Criteria + @if (isLocked) { + (Locked — org has access levels and inventory data) + }
@@ -19,8 +22,8 @@ - -@if (rowDataPending.length) { + +@if (rowDataPending.length || pendingRemovals.length) {
@@ -30,16 +33,34 @@
-
- -
+ @if (rowDataPending.length) { +
Adding
+
+ +
+ } + + @if (pendingRemovals.length) { +
Removing
+
+ @for (col of pendingRemovals; track col.id) { +
+ remove_circle + {{ col.display_name }} + +
+ } +
+ } } diff --git a/src/app/modules/organizations/columns/matching-criteria/matching-criteria.component.ts b/src/app/modules/organizations/columns/matching-criteria/matching-criteria.component.ts index af00b0e4..8c74a87a 100644 --- a/src/app/modules/organizations/columns/matching-criteria/matching-criteria.component.ts +++ b/src/app/modules/organizations/columns/matching-criteria/matching-criteria.component.ts @@ -38,6 +38,13 @@ export class MatchingCriteriaComponent implements OnDestroy { originalMatchingColumns: Column[] rowDataCurrent: Record[] = [] rowDataPending: Record[] = [] + // Columns pending removal from current matching criteria + pendingRemovals: Column[] = [] + + // Whether existing matching criteria columns are locked (cannot be removed) + get isLocked(): boolean { + return this.organization?.access_level_names?.length > 1 && this.organization?.inventory_count > 0 + } ngOnDestroy(): void { this._unsubscribeAll$.next() @@ -53,6 +60,9 @@ export class MatchingCriteriaComponent implements OnDestroy { this.columns = columns.filter((c) => c.table_name === tableName).sort((a, b) => naturalSort(a.display_name, b.display_name)) this.matchingColumns = this.columns.filter((c) => c.is_matching_criteria) this.availableColumns = this.columns.filter((c) => !c.is_matching_criteria && !c.derived_column && !c.is_extra_data) + this.pendingRemovals = [] + this.rowDataPending = [] + this.addForm.patchValue({ columnToAdd: null }) this.initGrid() } @@ -65,7 +75,9 @@ export class MatchingCriteriaComponent implements OnDestroy { this.columnDefsCurrent = [ { field: 'id', hide: true }, { field: 'display_name', headerName: 'Column Name' }, - { field: 'status', headerName: 'Status', cellClass: 'text-secondary' }, + ...(this.isLocked + ? [{ field: 'status', headerName: 'Status', cellClass: 'text-secondary' }] + : [{ field: 'actions', headerName: 'Actions', cellRenderer: this.actionRenderer }]), ] this.columnDefsPending = [ { field: 'id', hide: true }, @@ -83,14 +95,14 @@ export class MatchingCriteriaComponent implements OnDestroy { } setRowData() { - this.rowDataCurrent = [] - this.rowDataPending = [] - - this.rowDataCurrent = this.matchingColumns.map((column) => ({ - id: column.id, - display_name: column.display_name, - status: 'Locked', - })) + this.rowDataCurrent = this.matchingColumns + .filter((c) => !this.pendingRemovals.some((r) => r.id === c.id)) + .map((column) => ({ + id: column.id, + display_name: column.display_name, + ...(this.isLocked ? { status: 'Locked' } : {}), + })) + this.rowDataPending = this.rowDataPending.filter((r) => !this.matchingColumns.some((c) => c.id === r.id)) } get gridHeightCurrent() { @@ -103,6 +115,9 @@ export class MatchingCriteriaComponent implements OnDestroy { onCurrentReady(params: GridReadyEvent) { this.gridApiCurrent = params.api this.gridApiCurrent.sizeColumnsToFit() + if (!this.isLocked) { + this.gridApiCurrent.addEventListener('cellClicked', this.onRemoveCurrentColumn.bind(this) as (event: CellClickedEvent) => void) + } } onPendingReady(params: GridReadyEvent) { @@ -123,18 +138,44 @@ export class MatchingCriteriaComponent implements OnDestroy { this.rowDataPending = this.rowDataPending.filter((c) => c.id !== column.id) } + onRemoveCurrentColumn(event: CellClickedEvent) { + if (event.colDef.field !== 'actions') return + + const target = event.event.target as HTMLElement + const action = target.getAttribute('data-action') + if (!action) return + + const { id } = event.data as { id: number } + const column = this.matchingColumns.find((c) => c.id === id) + if (column) { + this.pendingRemovals = [...this.pendingRemovals, column] + this.rowDataCurrent = this.rowDataCurrent.filter((c) => c.id !== column.id) + } + } + addColumn() { const col = this.columns.find((c) => c.id === this.addForm.get('columnToAdd').value) this.rowDataPending = [...this.rowDataPending, { id: col.id, display_name: col.display_name }] } + undoRemoval(column: Column) { + this.pendingRemovals = this.pendingRemovals.filter((c) => c.id !== column.id) + this.rowDataCurrent = [ + ...this.rowDataCurrent, + { id: column.id, display_name: column.display_name, ...(this.isLocked ? { status: 'Locked' } : {}) }, + ] + } + save = () => { - const columnIds = new Set(this.rowDataPending.map((c) => c.id)) - const columns = this.columns.filter((c) => columnIds.has(c.id)) + const addColumnIds = new Set(this.rowDataPending.map((c) => c.id)) + const addColumns = this.columns.filter((c) => addColumnIds.has(c.id)) + const allChanges = [...addColumns, ...this.pendingRemovals] + + if (allChanges.length === 0) return const dialogRef = this._dialog.open(ConfirmModalComponent, { width: '40rem', - data: { cycle: null, orgId: this.organization.id, columns }, + data: { cycle: null, orgId: this.organization.id, columns: allChanges }, }) dialogRef @@ -143,6 +184,7 @@ export class MatchingCriteriaComponent implements OnDestroy { takeUntil(this._unsubscribeAll$), filter(Boolean), tap(() => { + this.pendingRemovals = [] if (this.currentType === 'properties') { this._columnService.getPropertyColumns(this.organization.id).subscribe((columns) => { this.populateMatchingColumns(columns) diff --git a/src/app/modules/organizations/cycles/cycles.component.html b/src/app/modules/organizations/cycles/cycles.component.html index e81fd859..e34487d1 100644 --- a/src/app/modules/organizations/cycles/cycles.component.html +++ b/src/app/modules/organizations/cycles/cycles.component.html @@ -34,10 +34,10 @@ Actions diff --git a/src/app/modules/organizations/data-quality/goal/goal-table.component.html b/src/app/modules/organizations/data-quality/goal/goal-table.component.html index 9bd144eb..a97f464b 100644 --- a/src/app/modules/organizations/data-quality/goal/goal-table.component.html +++ b/src/app/modules/organizations/data-quality/goal/goal-table.component.html @@ -48,10 +48,10 @@ Actions diff --git a/src/app/modules/organizations/data-quality/inventory/inventory-table.component.html b/src/app/modules/organizations/data-quality/inventory/inventory-table.component.html index 49d009b3..0cb369cb 100644 --- a/src/app/modules/organizations/data-quality/inventory/inventory-table.component.html +++ b/src/app/modules/organizations/data-quality/inventory/inventory-table.component.html @@ -42,10 +42,10 @@ Actions diff --git a/src/app/modules/organizations/derived-columns/derived-columns.component.html b/src/app/modules/organizations/derived-columns/derived-columns.component.html index c2fbdc55..b6850199 100644 --- a/src/app/modules/organizations/derived-columns/derived-columns.component.html +++ b/src/app/modules/organizations/derived-columns/derived-columns.component.html @@ -26,10 +26,10 @@ Actions diff --git a/src/app/modules/organizations/derived-columns/modal/form-modal.component.html b/src/app/modules/organizations/derived-columns/modal/form-modal.component.html index ae1fa29c..0e6751bc 100644 --- a/src/app/modules/organizations/derived-columns/modal/form-modal.component.html +++ b/src/app/modules/organizations/derived-columns/modal/form-modal.component.html @@ -46,7 +46,7 @@ @if (parameters.controls.length > 1) { }
diff --git a/src/app/modules/organizations/email-templates/email-templates.component.html b/src/app/modules/organizations/email-templates/email-templates.component.html index d7d6697b..028679bf 100644 --- a/src/app/modules/organizations/email-templates/email-templates.component.html +++ b/src/app/modules/organizations/email-templates/email-templates.component.html @@ -54,10 +54,10 @@

Custom Emails

diff --git a/src/app/modules/organizations/members/members.component.html b/src/app/modules/organizations/members/members.component.html index 75b8de3a..c8765744 100644 --- a/src/app/modules/organizations/members/members.component.html +++ b/src/app/modules/organizations/members/members.component.html @@ -31,10 +31,10 @@ Actions diff --git a/src/app/modules/organizations/settings/salesforce/salesforce.component.html b/src/app/modules/organizations/settings/salesforce/salesforce.component.html index 2caa72ed..559b5182 100644 --- a/src/app/modules/organizations/settings/salesforce/salesforce.component.html +++ b/src/app/modules/organizations/settings/salesforce/salesforce.component.html @@ -500,10 +500,10 @@ Actions diff --git a/src/app/modules/profile/admin/admin.component.html b/src/app/modules/profile/admin/admin.component.html index 8e2e05d8..8c630d53 100644 --- a/src/app/modules/profile/admin/admin.component.html +++ b/src/app/modules/profile/admin/admin.component.html @@ -1,7 +1,252 @@
-
-

- Admin +
+

+ {{ t('Admin') }}

+ + + + + {{ t('Organizations') }} + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ t('ID') }}{{ org.id }}{{ t('Name') }}{{ org.name }}{{ t('Created') }}{{ org.created | date: 'shortDate' }}{{ t('Users') }}{{ org.number_of_users }}{{ t('Properties') }}{{ org.property_count ?? '-' }}{{ t('Tax Lots') }}{{ org.taxlot_count ?? '-' }}{{ t('Actions') }} +
+ @if (deletingInventory.has(org.id)) { + + } @else { + + } + +
+
+
+
+ + + + + {{ t('Create Organization') }} + + +
+ + {{ t('Organization Name') }} + + @if (createOrgForm.controls.organizationName.hasError('required')) { + {{ t('Organization Name required') }} + } + + + {{ t('Owner') }} + + @for (user of allUsers; track user.user_id) { + {{ user.email }} + } + + +
+ +
+
+
+ + + + + {{ t('Create User') }} + + +
+
+ + {{ t('First Name') }} + + @if (createUserForm.controls.firstName.hasError('required')) { + {{ t('First Name required') }} + } + + + {{ t('Last Name') }} + + @if (createUserForm.controls.lastName.hasError('required')) { + {{ t('Last Name required') }} + } + +
+ + {{ t('Email') }} + + @if (createUserForm.controls.email.hasError('required') || createUserForm.controls.email.hasError('email')) { + {{ t('Valid email required') }} + } + +
+ + {{ t('Organization') }} + + @for (org of organizations; track org.id) { + {{ org.name }} + } + + + + {{ t('Role') }} + + @for (role of roles; track role.value) { + {{ role.label }} + } + + +
+
+ + {{ t('Access Level') }} + + @for (name of createUserAccessLevelNames; track name) { + {{ name }} + } + + + + {{ t('Access Level Instance') }} + + @for (ali of createUserAccessLevelInstances; track ali.id) { + {{ ali.name }} + } + + +
+
+ +
+
+
+ + + + + {{ t('Add User to Organization') }} + + +
+
+ + {{ t('Organization') }} + + @for (org of organizations; track org.id) { + {{ org.name }} + } + + + + {{ t('User') }} + + @for (user of allUsers; track user.user_id) { + {{ user.email }} + } + + +
+
+ +
+
+
+ + + + + {{ t('Remove User from Organization') }} + + +
+ + {{ t('Organization') }} + + @for (org of organizations; track org.id) { + {{ org.name }} + } + + + + @if (removeOrgUsers.length > 0) { +
+ @for (user of removeOrgUsers; track user.user_id) { +
+
+ {{ user.first_name }} {{ user.last_name }} + {{ user.email }} + ({{ user.role }}) +
+ +
+ } +
+ } + + @if (selectedRemoveOrgId && removeOrgUsers.length === 0) { +

{{ t('No users found in this organization') }}

+ } +
+

diff --git a/src/app/modules/profile/admin/admin.component.ts b/src/app/modules/profile/admin/admin.component.ts index af853ac6..bc86352e 100644 --- a/src/app/modules/profile/admin/admin.component.ts +++ b/src/app/modules/profile/admin/admin.component.ts @@ -1,10 +1,283 @@ -import { Component } from '@angular/core' +import { DatePipe } from '@angular/common' +import type { OnDestroy, OnInit } from '@angular/core' +import { Component, inject } from '@angular/core' +import { FormControl, FormGroup, type FormGroupDirective, ReactiveFormsModule, Validators } from '@angular/forms' +import { MatDialog } from '@angular/material/dialog' +import { MatTableDataSource } from '@angular/material/table' +import { Subject, switchMap, takeUntil } from 'rxjs' +import type { AccessLevelsByDepth, AdminOrganization, CurrentUser, OrganizationUser, UserBrief, UserRole } from '@seed/api' +import { OrganizationService, ProgressService, UserService } from '@seed/api' +import { DeleteModalComponent } from '@seed/components' import { SharedImports } from '@seed/directives' import { MaterialImports } from '@seed/materials' +import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' @Component({ selector: 'seed-admin', templateUrl: './admin.component.html', - imports: [MaterialImports, SharedImports], + imports: [DatePipe, MaterialImports, ReactiveFormsModule, SharedImports], }) -export class AdminComponent {} +export class AdminComponent implements OnInit, OnDestroy { + private _organizationService = inject(OrganizationService) + private _userService = inject(UserService) + private _progressService = inject(ProgressService) + private _snackBar = inject(SnackBarService) + private _dialog = inject(MatDialog) + private readonly _unsubscribeAll$ = new Subject() + + currentUser: CurrentUser + organizations: AdminOrganization[] = [] + allUsers: UserBrief[] = [] + orgUsers: OrganizationUser[] = [] + + // Organization table + orgDataSource = new MatTableDataSource([]) + orgColumns = ['id', 'name', 'created', 'number_of_users', 'property_count', 'taxlot_count', 'actions'] + + // Progress tracking for inventory deletion + deletingInventory = new Map() + + // Create Organization form + createOrgForm = new FormGroup({ + organizationName: new FormControl('', Validators.required), + userId: new FormControl(null, Validators.required), + }) + + // Create User form + createUserForm = new FormGroup({ + firstName: new FormControl('', Validators.required), + lastName: new FormControl('', Validators.required), + email: new FormControl('', [Validators.required, Validators.email]), + organizationId: new FormControl(null, Validators.required), + role: new FormControl('member', Validators.required), + accessLevel: new FormControl(null, Validators.required), + accessLevelInstanceId: new FormControl(null, Validators.required), + }) + + // Access level state for create user form + createUserAccessLevelNames: string[] = [] + createUserAccessLevelInstancesByDepth: AccessLevelsByDepth = {} + createUserAccessLevelInstances: { id: number; name: string }[] = [] + + // Add User to Org form + addUserOrgForm = new FormGroup({ + organizationId: new FormControl(null, Validators.required), + userId: new FormControl(null, Validators.required), + }) + + // Remove User from Org + selectedRemoveOrgId: number | null = null + removeOrgUsers: OrganizationUser[] = [] + + roles: { value: UserRole; label: string }[] = [ + { value: 'owner', label: 'Owner' }, + { value: 'member', label: 'Member' }, + { value: 'viewer', label: 'Viewer' }, + ] + + ngOnInit(): void { + this._userService.currentUser$.pipe(takeUntil(this._unsubscribeAll$)).subscribe((user) => { + this.currentUser = user + }) + + this.loadOrganizations() + this.loadUsers() + } + + ngOnDestroy(): void { + this._unsubscribeAll$.next() + this._unsubscribeAll$.complete() + } + + loadOrganizations(): void { + this._organizationService.getAllOrganizations().subscribe((orgs) => { + this.organizations = orgs + this.orgDataSource.data = orgs + }) + } + + loadUsers(): void { + this._userService.getAllUsers().subscribe((users) => { + this.allUsers = users + }) + } + + // --- Organization Management --- + + removeInventory(org: AdminOrganization): void { + this.deletingInventory.set(org.id, 0) + this._organizationService + .deleteOrganizationInventory(org.id) + .pipe( + switchMap((response) => { + return this._progressService.checkProgressLoop$(response.progress_key) + }), + ) + .subscribe({ + next: (progress) => { + this.deletingInventory.set(org.id, progress.progress) + }, + error: () => { + this.deletingInventory.delete(org.id) + this._snackBar.alert(`Failed to remove inventory for ${org.name}`) + }, + complete: () => { + this.deletingInventory.delete(org.id) + this._snackBar.success(`Inventory removed for ${org.name}`) + this.loadOrganizations() + }, + }) + } + + deleteOrganization(org: AdminOrganization): void { + const dialogRef = this._dialog.open(DeleteModalComponent, { + width: '400px', + data: { instance: org.name, model: 'Organization' }, + }) + + dialogRef.afterClosed().subscribe((confirmed) => { + if (confirmed) { + this._organizationService.deleteOrganization(org.id).subscribe({ + complete: () => { + this._snackBar.success(`Organization "${org.name}" deleted`) + this.loadOrganizations() + this._organizationService.getBrief().subscribe() + }, + }) + } + }) + } + + // --- Create Organization --- + + onCreateOrg(formDirective: FormGroupDirective): void { + if (this.createOrgForm.valid) { + const { organizationName, userId } = this.createOrgForm.value + this._organizationService.createOrganization(userId, organizationName).subscribe({ + complete: () => { + this._snackBar.success(`Organization "${organizationName}" created`) + formDirective.resetForm() + this.loadOrganizations() + // Refresh the org dropdown in the top-right nav + this._organizationService.getBrief().subscribe() + }, + }) + } + } + + // --- Create User --- + + onCreateUserOrgChange(): void { + const orgId = this.createUserForm.get('organizationId').value + if (!orgId) return + this._organizationService.getAccessLevelTree(orgId).subscribe(({ accessLevelNames, accessLevelTree }) => { + this.createUserAccessLevelNames = accessLevelNames + this.createUserAccessLevelInstancesByDepth = this._calculateAccessLevelsByDepth(accessLevelTree) + // Default to last access level name and first instance + const defaultLevel = accessLevelNames.at(-1) + this.createUserForm.get('accessLevel').setValue(defaultLevel) + this.onCreateUserAccessLevelChange() + }) + } + + onCreateUserAccessLevelChange(): void { + const accessLevel = this.createUserForm.get('accessLevel').value + const depth = this.createUserAccessLevelNames.findIndex((name) => name === accessLevel) + this.createUserAccessLevelInstances = this.createUserAccessLevelInstancesByDepth[depth] ?? [] + if (this.createUserAccessLevelInstances.length) { + this.createUserForm.get('accessLevelInstanceId').setValue(this.createUserAccessLevelInstances[0].id) + } + } + + onCreateUser(formDirective: FormGroupDirective): void { + if (this.createUserForm.valid) { + const { firstName, lastName, email, organizationId, role, accessLevelInstanceId } = this.createUserForm.value + this._userService + .createUser(organizationId, { + first_name: firstName, + last_name: lastName, + email, + org_name: '', + role, + access_level_instance_id: accessLevelInstanceId, + }) + .subscribe({ + complete: () => { + this._snackBar.success(`User "${email}" created`) + formDirective.resetForm({ role: 'member' }) + this.createUserAccessLevelNames = [] + this.createUserAccessLevelInstances = [] + this.loadUsers() + }, + }) + } + } + + // --- Add User to Org --- + + onAddUserToOrg(formDirective: FormGroupDirective): void { + if (this.addUserOrgForm.valid) { + const { organizationId, userId } = this.addUserOrgForm.value + this._organizationService.addUserToOrganization(organizationId, userId).subscribe({ + complete: () => { + const user = this.allUsers.find((u) => u.user_id === userId) + this._snackBar.success(`User "${user?.email}" added to organization`) + formDirective.resetForm() + this.loadOrganizations() + }, + }) + } + } + + // --- Remove User from Org --- + + onRemoveOrgChange(): void { + if (this.selectedRemoveOrgId) { + this._organizationService.getOrganizationUsers(this.selectedRemoveOrgId).subscribe((users) => { + this.removeOrgUsers = users + }) + } else { + this.removeOrgUsers = [] + } + } + + removeUserFromOrg(user: OrganizationUser): void { + const orgId = this.selectedRemoveOrgId + const dialogRef = this._dialog.open(DeleteModalComponent, { + width: '400px', + data: { instance: user.email, model: 'User' }, + }) + + dialogRef.afterClosed().subscribe((confirmed) => { + if (confirmed) { + this._organizationService.deleteOrganizationUser(user.user_id, orgId).subscribe({ + complete: () => { + this._snackBar.success(`User "${user.email}" removed from organization`) + this.onRemoveOrgChange() + this.loadOrganizations() + }, + }) + } + }) + } + + trackByOrgId(_index: number, org: AdminOrganization): number { + return org.id + } + + private _calculateAccessLevelsByDepth( + tree: { id: number; name: string; children?: { id: number; name: string; children?: unknown[] }[] }[], + depth = 0, + result: AccessLevelsByDepth = {}, + ): AccessLevelsByDepth { + if (!tree) return result + result[depth] ??= [] + for (const { children, id, name } of tree) { + result[depth].push({ id, name }) + if (children?.length) { + this._calculateAccessLevelsByDepth(children as typeof tree, depth + 1, result) + } + } + return result + } +} diff --git a/src/app/modules/profile/developer/developer.component.html b/src/app/modules/profile/developer/developer.component.html index 2198dcbb..a61e3add 100644 --- a/src/app/modules/profile/developer/developer.component.html +++ b/src/app/modules/profile/developer/developer.component.html @@ -1,7 +1,7 @@

- Developer + Developer

diff --git a/src/app/modules/profile/info/info.component.html b/src/app/modules/profile/info/info.component.html index 79aa0584..d003bef4 100644 --- a/src/app/modules/profile/info/info.component.html +++ b/src/app/modules/profile/info/info.component.html @@ -1,7 +1,7 @@

- Profile Information + Profile Information

diff --git a/src/app/modules/profile/security/security.component.html b/src/app/modules/profile/security/security.component.html index 5c3c2526..2b672a4b 100644 --- a/src/app/modules/profile/security/security.component.html +++ b/src/app/modules/profile/security/security.component.html @@ -1,7 +1,7 @@

- Security + Security

diff --git a/src/styles/styles.scss b/src/styles/styles.scss index dda267ff..ecfd12c8 100644 --- a/src/styles/styles.scss +++ b/src/styles/styles.scss @@ -195,6 +195,23 @@ } } + .mat-mdc-table { + background: transparent; + color: theme('colors.gray.200'); + } + + .mat-mdc-header-cell { + color: theme('colors.gray.300'); + } + + .mat-mdc-cell { + color: theme('colors.gray.200'); + } + + .mat-mdc-paginator { + color: theme('colors.gray.300'); + } + .nw-editor { background-color: theme('colors.gray.800'); }