Files
dashboard/frontend/src/services/members.ts
Matthias Hochmeister 1e478479be rework vehicle handling
2026-02-28 13:57:41 +01:00

129 lines
3.6 KiB
TypeScript

import { api } from './api';
import {
MemberListItem,
MemberWithProfile,
MemberFilters,
MemberStats,
CreateMemberProfileData,
UpdateMemberProfileData,
} from '../types/member.types';
// ----------------------------------------------------------------
// Response envelope shapes
// ----------------------------------------------------------------
interface ApiListResponse<T> {
success: boolean;
data: T[];
meta: { total: number; page: number };
}
interface ApiItemResponse<T> {
success: boolean;
data: T;
}
// ----------------------------------------------------------------
// Service
// ----------------------------------------------------------------
/**
* Builds a URLSearchParams object from the filter object so query
* strings like ?status[]=aktiv&status[]=passiv are sent correctly.
*/
function buildParams(filters?: MemberFilters): URLSearchParams {
const params = new URLSearchParams();
if (!filters) return params;
if (filters.search) params.append('search', filters.search);
if (filters.page) params.append('page', String(filters.page));
if (filters.pageSize) params.append('pageSize', String(filters.pageSize));
filters.status?.forEach((s) => params.append('status[]', s));
filters.dienstgrad?.forEach((d) => params.append('dienstgrad[]', d));
return params;
}
export const membersService = {
/**
* Fetches a paginated, optionally filtered list of members.
*/
async getMembers(
filters?: MemberFilters
): Promise<{ items: MemberListItem[]; total: number; page: number }> {
const params = buildParams(filters);
const response = await api.get<ApiListResponse<MemberListItem>>(
`/api/members?${params.toString()}`
);
if (!response.data?.data) {
throw new Error('Invalid API response');
}
return {
items: response.data.data,
total: response.data.meta?.total ?? 0,
page: response.data.meta?.page ?? 1,
};
},
/**
* Fetches a single member with their full profile and rank history.
*/
async getMember(userId: string): Promise<MemberWithProfile> {
const response = await api.get<ApiItemResponse<MemberWithProfile>>(
`/api/members/${userId}`
);
if (!response.data?.data) {
throw new Error('Invalid API response');
}
return response.data.data;
},
/**
* Creates a new member profile for an existing auth user.
* Restricted to Kommandant/Admin (enforced server-side).
*/
async createMemberProfile(
userId: string,
data: CreateMemberProfileData
): Promise<MemberWithProfile> {
const response = await api.post<ApiItemResponse<MemberWithProfile>>(
`/api/members/${userId}/profile`,
data
);
if (!response.data?.data) {
throw new Error('Invalid API response');
}
return response.data.data;
},
/**
* Partially updates a member profile.
* Kommandant/Admin: full update.
* Own profile: limited fields only (enforced server-side).
*/
async updateMember(
userId: string,
data: UpdateMemberProfileData
): Promise<MemberWithProfile> {
const response = await api.patch<ApiItemResponse<MemberWithProfile>>(
`/api/members/${userId}`,
data
);
if (!response.data?.data) {
throw new Error('Invalid API response');
}
return response.data.data;
},
/**
* Fetches aggregate counts for the dashboard KPI widget.
*/
async getMemberStats(): Promise<MemberStats> {
const response = await api.get<ApiItemResponse<MemberStats>>('/api/members/stats');
if (!response.data?.data) {
throw new Error('Invalid API response');
}
return response.data.data;
},
};