diff --git a/eeg_frontend/src/app/app.routes.ts b/eeg_frontend/src/app/app.routes.ts
index 6a966ec..662c156 100644
--- a/eeg_frontend/src/app/app.routes.ts
+++ b/eeg_frontend/src/app/app.routes.ts
@@ -20,6 +20,8 @@ import {AdminTariffComponent} from './pages/admin-tariff/admin-tariff';
import {UserTariffComponent} from './pages/user-tariff/user-tariff';
import {ConsumptionDashboardComponent} from './pages/consumption-dashboard/consumption-dashboard';
import {UploadMeteringDataComponent} from './pages/upload-metering-data/upload-metering-data';
+import {ProducerInvitesComponent} from './pages/producer-invites/producer-invites';
+import {ConsumerInvitesComponent} from './pages/consumer-invites/consumer-invites';
export const routes: Routes = [
{ path: '', component: LandingPage },
@@ -84,6 +86,16 @@ export const routes: Routes = [
component: ConsumptionDashboardComponent,
canActivate: [roleGuard(['MEMBER'])]
},
+ {
+ path: 'producer-invites',
+ component: ProducerInvitesComponent,
+ canActivate: [roleGuard(['MEMBER'])]
+ },
+ {
+ path: 'consumer-invites',
+ component: ConsumerInvitesComponent,
+ canActivate: [roleGuard(['MEMBER'])]
+ },
{
path: 'profile',
component: ProfileComponent,
diff --git a/eeg_frontend/src/app/layout/dashboard-layout/dashboard-nav-items.ts b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-nav-items.ts
index c31a108..d1982e7 100644
--- a/eeg_frontend/src/app/layout/dashboard-layout/dashboard-nav-items.ts
+++ b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-nav-items.ts
@@ -16,5 +16,7 @@ export const NAV_ITEMS: NavItem[] = [
{label: 'Energiegemeinschaften', path: '/dashboard/energy-communities', roles: ['ADMIN']},
{label: 'Tarife verwalten', path: '/dashboard/tariffs', roles: ['ADMIN']},
{label: 'Messdaten-Upload', path: '/dashboard/upload-metering-data', roles: ['ADMIN']},
- {label: 'Meine Tarife', path: '/dashboard/my-tariffs', roles: ['MEMBER']}
+ {label: 'Meine Tarife', path: '/dashboard/my-tariffs', roles: ['MEMBER']},
+ {label: 'Meine Einladungen', path: '/dashboard/producer-invites', roles: ['MEMBER']},
+ {label: 'Offene Einladungen', path: '/dashboard/consumer-invites', roles: ['MEMBER']}
];
diff --git a/eeg_frontend/src/app/pages/consumer-invites/consumer-invites.ts b/eeg_frontend/src/app/pages/consumer-invites/consumer-invites.ts
new file mode 100644
index 0000000..a870d4c
--- /dev/null
+++ b/eeg_frontend/src/app/pages/consumer-invites/consumer-invites.ts
@@ -0,0 +1,147 @@
+import {Component, OnInit, inject} from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { MatCardModule } from '@angular/material/card';
+import { MatButtonModule } from '@angular/material/button';
+import { MatIconModule } from '@angular/material/icon';
+import { MatTableModule } from '@angular/material/table';
+import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
+import { HttpClient } from '@angular/common/http';
+import { environment } from '../../../environments/environment';
+
+interface TariffInvite {
+ id: string;
+ producerUserId: string;
+ consumerUserId: string;
+ energyCommunityId: string;
+ code: string;
+ note: string;
+ status: string;
+ expiresAt: string;
+ acceptedAt: string;
+ createdAt: string;
+}
+
+@Component({
+ selector: 'app-consumer-invites',
+ standalone: true,
+ imports: [
+ CommonModule,
+ MatCardModule,
+ MatButtonModule,
+ MatIconModule,
+ MatTableModule,
+ MatSnackBarModule,
+ MatProgressSpinnerModule
+ ],
+ template: `
+
+
Offene Einladungen
+
+
+
+ Ausstehende Einladungen
+ Sie wurden eingeladen, einen Spezial-Tarif zu erhalten
+
+
+ @if (isLoading) {
+
+
+
+ } @else if (invites.length === 0) {
+ Keine offenen Einladungen vorhanden.
+ } @else {
+
+ @for (invite of invites; track invite.id) {
+
+
+
+
Einladung von Producer
+
Code: {{ invite.code }}
+ @if (invite.note) {
+
"{{ invite.note }}"
+ }
+
Gültig bis: {{ invite.expiresAt | date:'dd.MM.yyyy HH:mm' }}
+
+
+
+
+
+
+
+ }
+
+ }
+
+
+
+ `
+})
+export class ConsumerInvitesComponent implements OnInit {
+ private http = inject(HttpClient);
+ private snackBar = inject(MatSnackBar);
+
+ private apiUrl = `${environment.apiUrl}/api`;
+
+ invites: TariffInvite[] = [];
+ isLoading = false;
+ acceptingId: string | null = null;
+
+ ngOnInit() {
+ this.loadInvites();
+ }
+
+ loadInvites() {
+ this.isLoading = true;
+
+ this.http.get(`${this.apiUrl}/tariffs/invites/pending`)
+ .subscribe({
+ next: (invites) => {
+ this.invites = invites;
+ this.isLoading = false;
+ },
+ error: () => {
+ this.isLoading = false;
+ this.snackBar.open('Fehler beim Laden der Einladungen', 'Schließen', { duration: 3000 });
+ }
+ });
+ }
+
+ acceptInvite(invite: TariffInvite) {
+ this.acceptingId = invite.id;
+
+ this.http.post(`${this.apiUrl}/tariffs/invites/${invite.id}/accept`, {})
+ .subscribe({
+ next: () => {
+ this.snackBar.open('Einladung angenommen! Der Producer kann jetzt einen Spezial-Tarif für Sie erstellen.', 'Schließen', { duration: 5000 });
+ this.loadInvites();
+ },
+ error: (err) => {
+ this.acceptingId = null;
+ const message = err.error?.message || 'Fehler beim Annehmen der Einladung';
+ this.snackBar.open(message, 'Schließen', { duration: 5000 });
+ }
+ });
+ }
+
+ rejectInvite(invite: TariffInvite) {
+ // For now, we just remove it from the local list
+ // In a real implementation, we might want to add a reject endpoint
+ this.snackBar.open('Einladung abgelehnt', 'Schließen', { duration: 3000 });
+ this.invites = this.invites.filter(i => i.id !== invite.id);
+ }
+}
diff --git a/eeg_frontend/src/app/pages/producer-invites/producer-invites.ts b/eeg_frontend/src/app/pages/producer-invites/producer-invites.ts
new file mode 100644
index 0000000..6b8126e
--- /dev/null
+++ b/eeg_frontend/src/app/pages/producer-invites/producer-invites.ts
@@ -0,0 +1,266 @@
+import {Component, OnInit, inject} from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { MatCardModule } from '@angular/material/card';
+import { MatButtonModule } from '@angular/material/button';
+import { MatInputModule } from '@angular/material/input';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatSelectModule } from '@angular/material/select';
+import { MatIconModule } from '@angular/material/icon';
+import { MatTableModule } from '@angular/material/table';
+import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
+import { DashboardService, AdminStats } from '../../services/dashboard';
+import { HttpClient } from '@angular/common/http';
+import { environment } from '../../../environments/environment';
+import { AuthService } from '../../services/auth';
+
+interface CommunityMember {
+ userId: string;
+ firstName: string;
+ lastName: string;
+ email: string;
+}
+
+interface TariffInvite {
+ id: string;
+ producerUserId: string;
+ consumerUserId: string;
+ energyCommunityId: string;
+ code: string;
+ note: string;
+ status: string;
+ expiresAt: string;
+ acceptedAt: string;
+ createdAt: string;
+}
+
+interface EnergyCommunity {
+ id: string;
+ name: string;
+}
+
+@Component({
+ selector: 'app-producer-invites',
+ standalone: true,
+ imports: [
+ CommonModule,
+ FormsModule,
+ MatCardModule,
+ MatButtonModule,
+ MatInputModule,
+ MatFormFieldModule,
+ MatSelectModule,
+ MatIconModule,
+ MatTableModule,
+ MatSnackBarModule,
+ MatProgressSpinnerModule
+ ],
+ template: `
+
+
Meine Einladungen
+
+
+
+
+ Neue Einladung erstellen
+ Laden Sie einen Consumer ein, einen Spezial-Tarif zu erhalten
+
+
+ @if (communities.length === 0) {
+ Sie sind noch keiner Energiegemeinschaft beigetreten.
+ } @else {
+
+
+ Energiegemeinschaft
+
+ @for (community of communities; track community.id) {
+ {{ community.name }}
+ }
+
+
+
+ @if (selectedCommunityId) {
+
+ Consumer auswählen
+
+ @for (member of members; track member.userId) {
+ {{ member.firstName }} {{ member.lastName }}
+ }
+
+
+
+
+ Notiz (optional)
+
+
+
+
+ }
+
+ }
+
+
+
+
+
+
+ Gesendete Einladungen
+
+
+ @if (invites.length === 0) {
+ Noch keine Einladungen gesendet.
+ } @else {
+
+
+ | Consumer |
+ {{ invite.consumerUserId }} |
+
+
+
+ Code |
+
+ {{ invite.code }}
+ |
+
+
+
+ Status |
+
+ {{ getStatusLabel(invite.status) }}
+ |
+
+
+
+ Gültig bis |
+ {{ invite.expiresAt | date:'dd.MM.yyyy HH:mm' }} |
+
+
+
+
+
+ }
+
+
+
+ `
+})
+export class ProducerInvitesComponent implements OnInit {
+ private http = inject(HttpClient);
+ private snackBar = inject(MatSnackBar);
+ private authService = inject(AuthService);
+
+ private apiUrl = `${environment.apiUrl}/api`;
+
+ communities: EnergyCommunity[] = [];
+ members: CommunityMember[] = [];
+ invites: TariffInvite[] = [];
+
+ selectedCommunityId: string = '';
+ selectedConsumerId: string = '';
+ inviteNote: string = '';
+
+ isLoading = false;
+
+ displayedColumns = ['consumer', 'code', 'status', 'expiresAt'];
+
+ ngOnInit() {
+ this.loadCommunities();
+ }
+
+ loadCommunities() {
+ this.http.get(`${this.apiUrl}/community/admin/communities`)
+ .subscribe({
+ next: (communities) => this.communities = communities,
+ error: () => this.snackBar.open('Fehler beim Laden der Energiegemeinschaften', 'Schließen', { duration: 3000 })
+ });
+ }
+
+ loadMembers() {
+ if (!this.selectedCommunityId) return;
+
+ this.http.get(`${this.apiUrl}/community/admin/communities/${this.selectedCommunityId}/members`)
+ .subscribe({
+ next: (members) => {
+ const currentUserId = this.authService.getCurrentUserId();
+ this.members = members.filter(m => m.userId !== currentUserId);
+ },
+ error: () => this.snackBar.open('Fehler beim Laden der Mitglieder', 'Schließen', { duration: 3000 })
+ });
+
+ this.loadInvites();
+ }
+
+ loadInvites() {
+ if (!this.selectedCommunityId) return;
+
+ this.http.get(`${this.apiUrl}/tariffs/invites/community/${this.selectedCommunityId}`)
+ .subscribe({
+ next: (invites) => this.invites = invites,
+ error: () => this.snackBar.open('Fehler beim Laden der Einladungen', 'Schließen', { duration: 3000 })
+ });
+ }
+
+ createInvite() {
+ if (!this.selectedConsumerId || !this.selectedCommunityId) return;
+
+ this.isLoading = true;
+
+ const request = {
+ consumerUserId: this.selectedConsumerId,
+ energyCommunityId: this.selectedCommunityId,
+ note: this.inviteNote || null
+ };
+
+ this.http.post(`${this.apiUrl}/tariffs/invites`, request)
+ .subscribe({
+ next: (invite) => {
+ this.isLoading = false;
+ this.snackBar.open('Einladung erfolgreich gesendet!', 'Schließen', { duration: 3000 });
+ this.inviteNote = '';
+ this.selectedConsumerId = '';
+ this.loadInvites();
+ },
+ error: (err) => {
+ this.isLoading = false;
+ const message = err.error?.message || 'Fehler beim Erstellen der Einladung';
+ this.snackBar.open(message, 'Schließen', { duration: 5000 });
+ }
+ });
+ }
+
+ getStatusClass(status: string): string {
+ switch (status) {
+ case 'PENDING': return 'text-yellow-600 bg-yellow-100 px-2 py-1 rounded';
+ case 'ACCEPTED': return 'text-green-600 bg-green-100 px-2 py-1 rounded';
+ case 'EXPIRED': return 'text-red-600 bg-red-100 px-2 py-1 rounded';
+ default: return 'text-slate-600 bg-slate-100 px-2 py-1 rounded';
+ }
+ }
+
+ getStatusLabel(status: string): string {
+ switch (status) {
+ case 'PENDING': return 'Ausstehend';
+ case 'ACCEPTED': return 'Angenommen';
+ case 'EXPIRED': return 'Abgelaufen';
+ default: return status;
+ }
+ }
+}
+
+interface TariffInviteRequest {
+ consumerUserId: string;
+ energyCommunityId: string;
+ note: string | null;
+}