diff --git a/src/app/admin/organizations/organizations.component.html b/src/app/admin/organizations/organizations.component.html
new file mode 100644
index 0000000000..394124a1cd
--- /dev/null
+++ b/src/app/admin/organizations/organizations.component.html
@@ -0,0 +1,76 @@
+
+
+
+
Manage Organizations
+
+
+
+
+
+
+
+ | Name |
+ {{org.name}} |
+
+
+
+
+ Description |
+ {{org.description}} |
+
+
+
+
+ Email |
+ {{org.email}} |
+
+
+
+
+ Status |
+
+
+ {{org.is_enabled ? 'check_circle' : 'cancel'}}
+
+ |
+
+
+
+
+ Actions |
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/admin/organizations/organizations.component.scss b/src/app/admin/organizations/organizations.component.scss
new file mode 100644
index 0000000000..11b38d0706
--- /dev/null
+++ b/src/app/admin/organizations/organizations.component.scss
@@ -0,0 +1,64 @@
+.container {
+ padding: 20px;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+table {
+ width: 100%;
+}
+
+.mat-column-actions {
+ width: 120px;
+ text-align: center;
+}
+
+.mat-column-is_enabled {
+ width: 100px;
+ text-align: center;
+}
+
+.mat-column-description {
+ max-width: 300px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.mat-column-email {
+ max-width: 200px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.mat-mdc-row:hover {
+ background-color: rgba(0, 0, 0, 0.04);
+}
+
+.mat-mdc-header-row {
+ background-color: #f5f5f5;
+}
+
+.mat-mdc-header-cell {
+ font-weight: 600;
+ color: rgba(0, 0, 0, 0.87);
+}
+
+.mat-mdc-cell {
+ padding: 0 16px;
+}
+
+.mat-mdc-header-cell {
+ padding: 0 16px;
+}
+
+.mat-mdc-menu-item {
+ .mat-icon {
+ margin-right: 8px;
+ }
+}
+
+.text-red-500 {
+ color: #ef4444;
+}
\ No newline at end of file
diff --git a/src/app/admin/organizations/organizations.component.ts b/src/app/admin/organizations/organizations.component.ts
new file mode 100644
index 0000000000..12602e5def
--- /dev/null
+++ b/src/app/admin/organizations/organizations.component.ts
@@ -0,0 +1,92 @@
+import { Component, OnInit, ViewChild, AfterViewInit } from '@angular/core';
+import { MatTable } from '@angular/material/table';
+import { MatPaginator } from '@angular/material/paginator';
+import { MatSort } from '@angular/material/sort';
+import { MatDialog } from '@angular/material/dialog';
+import { Organization } from 'src/app/api/models/organization';
+import { OrganizationService } from 'src/app/api/services/organization.service';
+import { AlertService } from 'src/app/common/services/alert.service';
+
+@Component({
+ selector: 'f-organizations',
+ templateUrl: './organizations.component.html',
+ styleUrls: ['./organizations.component.scss']
+})
+export class OrganizationsComponent implements OnInit, AfterViewInit {
+ displayedColumns: string[] = ['name', 'description', 'email', 'is_enabled', 'actions'];
+ organizations: Organization[] = [];
+ loading: boolean = true;
+
+ @ViewChild(MatTable) table: MatTable;
+ @ViewChild(MatPaginator) paginator: MatPaginator;
+ @ViewChild(MatSort) sort: MatSort;
+
+ constructor(
+ private organizationService: OrganizationService,
+ private alertService: AlertService,
+ private dialog: MatDialog
+ ) {}
+
+ ngOnInit(): void {
+ this.loadOrganizations();
+ }
+
+ ngAfterViewInit() {
+ // Add sorting and pagination if needed
+ }
+
+ loadOrganizations(): void {
+ this.loading = true;
+ this.organizationService.query().subscribe({
+ next: (orgs) => {
+ this.organizations = orgs;
+ this.loading = false;
+ if (this.table) {
+ this.table.renderRows();
+ }
+ },
+ error: (err) => {
+ this.alertService.error('Failed to load organizations: ' + err);
+ this.loading = false;
+ }
+ });
+ }
+
+ createOrganization(): void {
+ // TODO: Implement create organization dialog
+ }
+
+ editOrganization(organization: Organization): void {
+ // TODO: Implement edit organization dialog
+ }
+
+ deleteOrganization(organization: Organization): void {
+ // TODO: Implement delete organization confirmation dialog
+ this.organizationService.deleteOrganization(organization.id).subscribe({
+ next: () => {
+ this.alertService.success('Organization deleted successfully');
+ this.loadOrganizations();
+ },
+ error: (err) => {
+ this.alertService.error('Failed to delete organization: ' + err);
+ }
+ });
+ }
+
+ addMember(organization: Organization): void {
+ // TODO: Implement add member dialog
+ }
+
+ viewMembers(organization: Organization): void {
+ // TODO: Implement view members dialog
+ this.organizationService.getMembers(organization.id).subscribe({
+ next: (members) => {
+ // TODO: Show members in dialog
+ console.log('Members:', members);
+ },
+ error: (err) => {
+ this.alertService.error('Failed to load members: ' + err);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/api.config.ts b/src/app/api/api.config.ts
new file mode 100644
index 0000000000..9e9fefd08c
--- /dev/null
+++ b/src/app/api/api.config.ts
@@ -0,0 +1 @@
+export const API_URL = '/api';
\ No newline at end of file
diff --git a/src/app/api/models/organization.ts b/src/app/api/models/organization.ts
new file mode 100644
index 0000000000..0aa57fb7e5
--- /dev/null
+++ b/src/app/api/models/organization.ts
@@ -0,0 +1,27 @@
+import { EntityService } from '../services/entity.service';
+import { User } from './user/user';
+
+export class Organization {
+ id: number;
+ name: string;
+ description: string;
+ email: string;
+ is_enabled: boolean;
+ users: User[];
+
+ constructor(params: any) {
+ this.id = params.id;
+ this.name = params.name;
+ this.description = params.description;
+ this.email = params.email;
+ this.is_enabled = params.is_enabled;
+ this.users = params.users || [];
+ }
+
+ matches(filter: string): boolean {
+ return (
+ this.name.toLowerCase().includes(filter.toLowerCase()) ||
+ (this.description && this.description.toLowerCase().includes(filter.toLowerCase()))
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/services/entity.service.ts b/src/app/api/services/entity.service.ts
new file mode 100644
index 0000000000..de0cc34728
--- /dev/null
+++ b/src/app/api/services/entity.service.ts
@@ -0,0 +1,28 @@
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { Observable } from 'rxjs';
+
+@Injectable()
+export abstract class EntityService {
+ protected abstract readonly endpointFormat: string;
+
+ constructor(protected httpClient: HttpClient, protected apiUrl: string) {}
+
+ protected get(endpoint: string, params?: any): Observable {
+ return this.httpClient.get(`${this.apiUrl}/${endpoint}`, { params });
+ }
+
+ protected post(endpoint: string, data: any): Observable {
+ return this.httpClient.post(`${this.apiUrl}/${endpoint}`, data);
+ }
+
+ protected put(endpoint: string, data: any): Observable {
+ return this.httpClient.put(`${this.apiUrl}/${endpoint}`, data);
+ }
+
+ protected delete(endpoint: string): Observable {
+ return this.httpClient.delete(`${this.apiUrl}/${endpoint}`);
+ }
+
+ protected abstract createInstanceFrom(json: any): T;
+}
\ No newline at end of file
diff --git a/src/app/api/services/organization.service.ts b/src/app/api/services/organization.service.ts
new file mode 100644
index 0000000000..007f3a1664
--- /dev/null
+++ b/src/app/api/services/organization.service.ts
@@ -0,0 +1,80 @@
+import { Injectable } from '@angular/core';
+import { EntityService } from './entity.service';
+import { Organization } from '../models/organization';
+import { HttpClient } from '@angular/common/http';
+import { API_URL } from '../api.config';
+import { User } from '../models/user/user';
+import { Observable } from 'rxjs';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class OrganizationService extends EntityService {
+ protected readonly endpointFormat = 'organizations/:id:';
+
+ constructor(httpClient: HttpClient) {
+ super(httpClient, API_URL);
+ }
+
+ protected createInstanceFrom(json: any): Organization {
+ return new Organization(json);
+ }
+
+ // Get all organizations
+ query(params?: any): Observable {
+ return this.httpClient.get(`${this.apiUrl}/${this.endpointFormat.replace(':id:', '')}`, { params });
+ }
+
+ // Get a single organization
+ getOrganization(id: number): Observable {
+ return this.httpClient.get(`${this.apiUrl}/${this.endpointFormat.replace(':id:', id.toString())}`);
+ }
+
+ // Create a new organization
+ create(organization: Organization): Observable {
+ return this.httpClient.post(`${this.apiUrl}/${this.endpointFormat.replace(':id:', '')}`, { organization });
+ }
+
+ // Update an organization
+ update(organization: Organization): Observable {
+ return this.httpClient.put(
+ `${this.apiUrl}/${this.endpointFormat.replace(':id:', organization.id.toString())}`,
+ { organization }
+ );
+ }
+
+ // Delete an organization
+ deleteOrganization(id: number): Observable {
+ return this.httpClient.delete(`${this.apiUrl}/${this.endpointFormat.replace(':id:', id.toString())}`);
+ }
+
+ // Add a member to an organization
+ addMember(organizationId: number, userId: number): Observable {
+ return this.httpClient.post(
+ `${this.apiUrl}/${this.endpointFormat.replace(':id:', organizationId.toString())}/members`,
+ { user_id: userId }
+ );
+ }
+
+ // Remove a member from an organization
+ removeMember(organizationId: number, userId: number): Observable {
+ return this.httpClient.delete(
+ `${this.apiUrl}/${this.endpointFormat.replace(':id:', organizationId.toString())}/members/${userId}`
+ );
+ }
+
+ // Get organization members
+ getMembers(organizationId: number): Observable {
+ return this.httpClient.get(
+ `${this.apiUrl}/${this.endpointFormat.replace(':id:', organizationId.toString())}/members`
+ );
+ }
+
+ // Search users to add to organization
+ searchUsers(organizationId: number, query: string): Observable {
+ return this.httpClient.get(
+ `${this.apiUrl}/${this.endpointFormat.replace(':id:', organizationId.toString())}/search_users`,
+ { params: { query } }
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/app/common/header/header.component.html b/src/app/common/header/header.component.html
index bac6b77c07..182a59cb18 100644
--- a/src/app/common/header/header.component.html
+++ b/src/app/common/header/header.component.html
@@ -57,6 +57,12 @@
school
Manage Units
+
+
+
@if (currentUser.role === 'Admin') {