feat: complete V1 phases 0-7

This commit is contained in:
2026-08-12 14:35:12 +08:00
commit c3dd9be674
311 changed files with 38750 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
import type { ApiErrorResponse, ApiSuccessResponse } from '@lehuo/contracts';
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string | undefined;
export class ApiClientError extends Error {
constructor(
message: string,
readonly code: string,
readonly requestId: string | null,
) {
super(message);
}
}
export async function apiRequest<T>(path: string, init: RequestInit = {}): Promise<T> {
if (!API_BASE_URL) {
throw new ApiClientError('管理后台尚未配置服务地址', 'CONFIGURATION_ERROR', null);
}
let response: Response;
try {
response = await fetch(new URL(path, API_BASE_URL), {
...init,
credentials: 'include',
headers: {
'content-type': 'application/json',
...init.headers,
},
});
} catch {
throw new ApiClientError('网络连接失败,请检查网络后重试', 'NETWORK_ERROR', null);
}
const payload = (await response.json()) as ApiSuccessResponse<T> | ApiErrorResponse;
if (!response.ok || !payload.success) {
const error = payload as ApiErrorResponse;
throw new ApiClientError(
error.error?.message || '请求失败,请稍后重试',
error.error?.code || 'UNKNOWN_ERROR',
error.requestId || response.headers.get('x-request-id'),
);
}
return payload.data;
}
+172
View File
@@ -0,0 +1,172 @@
import type {
AcceptServiceApplicationRequest,
AcceptServiceApplicationResponse,
AssigneeOption,
BeneficiaryDetailResponse,
BeneficiaryTagOption,
ConfirmUploadResponse,
CompleteServiceCaseRequest,
CompleteServiceCaseResponse,
CreateServiceRecordRequest,
CreateUploadIntentRequest,
CreateUploadIntentResponse,
DownloadUrlResponse,
PaginatedBeneficiariesResponse,
PaginatedServiceCasesResponse,
ReplaceCaseAssigneesRequest,
ServiceCaseDetailResponse,
ServiceCategory,
ServiceCaseStatus,
ServiceRecordResponse,
TransitionServiceApplicationRequest,
UpdateBeneficiaryRequest,
UpdateServiceRecordRequest,
} from '@lehuo/contracts';
import { apiRequest } from './api-client';
function queryString(values: Record<string, string | number | boolean | undefined>): string {
const params = new URLSearchParams();
Object.entries(values).forEach(([key, value]) => {
if (value !== undefined && value !== '') params.set(key, String(value));
});
return params.toString();
}
export function transitionServiceApplication(
publicId: string,
input: TransitionServiceApplicationRequest,
): Promise<{ status: string }> {
return apiRequest(
`/api/v1/admin/service-applications/${encodeURIComponent(publicId)}/transition`,
{
method: 'POST',
body: JSON.stringify(input),
},
);
}
export function acceptServiceApplication(
publicId: string,
input: AcceptServiceApplicationRequest,
): Promise<AcceptServiceApplicationResponse> {
return apiRequest(`/api/v1/admin/service-applications/${encodeURIComponent(publicId)}/accept`, {
method: 'POST',
body: JSON.stringify(input),
});
}
export function fetchBeneficiaryTags(): Promise<BeneficiaryTagOption[]> {
return apiRequest('/api/v1/admin/beneficiaries/tags');
}
export function fetchBeneficiaries(query: {
page: number;
pageSize: number;
primaryCategory?: ServiceCategory;
status?: string;
keyCareFlag?: boolean;
}): Promise<PaginatedBeneficiariesResponse> {
return apiRequest(`/api/v1/admin/beneficiaries?${queryString(query)}`);
}
export function fetchBeneficiary(publicId: string): Promise<BeneficiaryDetailResponse> {
return apiRequest(`/api/v1/admin/beneficiaries/${encodeURIComponent(publicId)}`);
}
export function updateBeneficiary(
publicId: string,
input: UpdateBeneficiaryRequest,
): Promise<BeneficiaryDetailResponse> {
return apiRequest(`/api/v1/admin/beneficiaries/${encodeURIComponent(publicId)}`, {
method: 'PUT',
body: JSON.stringify(input),
});
}
export function fetchAssigneeOptions(): Promise<AssigneeOption[]> {
return apiRequest('/api/v1/admin/service-cases/assignee-options');
}
export function fetchServiceCases(query: {
page: number;
pageSize: number;
status?: ServiceCaseStatus;
category?: ServiceCategory;
caseNo?: string;
}): Promise<PaginatedServiceCasesResponse> {
return apiRequest(`/api/v1/admin/service-cases?${queryString(query)}`);
}
export function fetchServiceCase(publicId: string): Promise<ServiceCaseDetailResponse> {
return apiRequest(`/api/v1/admin/service-cases/${encodeURIComponent(publicId)}`);
}
export function startServiceCase(publicId: string): Promise<ServiceCaseDetailResponse> {
return apiRequest(`/api/v1/admin/service-cases/${encodeURIComponent(publicId)}/start`, {
method: 'POST',
body: '{}',
});
}
export function completeServiceCase(
publicId: string,
input: CompleteServiceCaseRequest,
): Promise<CompleteServiceCaseResponse> {
return apiRequest(`/api/v1/admin/service-cases/${encodeURIComponent(publicId)}/complete`, {
method: 'POST',
body: JSON.stringify(input),
});
}
export function replaceCaseAssignees(
publicId: string,
input: ReplaceCaseAssigneesRequest,
): Promise<ServiceCaseDetailResponse> {
return apiRequest(`/api/v1/admin/service-cases/${encodeURIComponent(publicId)}/assignees`, {
method: 'PUT',
body: JSON.stringify(input),
});
}
export function createServiceRecord(
publicId: string,
input: CreateServiceRecordRequest,
): Promise<ServiceRecordResponse> {
return apiRequest(`/api/v1/admin/service-cases/${encodeURIComponent(publicId)}/records`, {
method: 'POST',
body: JSON.stringify(input),
});
}
export function updateServiceRecord(
casePublicId: string,
recordPublicId: string,
input: UpdateServiceRecordRequest,
): Promise<ServiceRecordResponse> {
return apiRequest(
`/api/v1/admin/service-cases/${encodeURIComponent(casePublicId)}/records/${encodeURIComponent(recordPublicId)}`,
{ method: 'PATCH', body: JSON.stringify(input) },
);
}
export function createUploadIntent(
casePublicId: string,
input: CreateUploadIntentRequest,
): Promise<CreateUploadIntentResponse> {
return apiRequest(
`/api/v1/service-cases/${encodeURIComponent(casePublicId)}/files/upload-intents`,
{ method: 'POST', body: JSON.stringify(input) },
);
}
export function confirmUpload(publicId: string): Promise<ConfirmUploadResponse> {
return apiRequest(`/api/v1/files/${encodeURIComponent(publicId)}/confirm`, {
method: 'POST',
body: '{}',
});
}
export function fetchDownloadUrl(publicId: string): Promise<DownloadUrlResponse> {
return apiRequest(`/api/v1/files/${encodeURIComponent(publicId)}/download-url`);
}
@@ -0,0 +1,37 @@
import type {
ContentPostDetailResponse,
CreateContentPostRequest,
PaginatedContentPostsResponse,
PaginatedNotificationDeliveriesResponse,
TransitionContentPostRequest,
} from '@lehuo/contracts';
import { apiRequest } from './api-client';
const query = (v: Record<string, string | number | undefined>) => {
const p = new URLSearchParams();
Object.entries(v).forEach(([k, x]) => x !== undefined && p.set(k, String(x)));
return p.toString();
};
export const fetchContent = (v: {
page: number;
pageSize: number;
status?: string;
type?: string;
}) => apiRequest<PaginatedContentPostsResponse>(`/api/v1/admin/content?${query(v)}`);
export const createContent = (input: CreateContentPostRequest) =>
apiRequest<ContentPostDetailResponse>('/api/v1/admin/content', {
method: 'POST',
body: JSON.stringify(input),
});
export const transitionContent = (id: string, input: TransitionContentPostRequest) =>
apiRequest<ContentPostDetailResponse>(`/api/v1/admin/content/${id}/transition`, {
method: 'POST',
body: JSON.stringify(input),
});
export const fetchDeliveries = (v: { page: number; pageSize: number; status?: string }) =>
apiRequest<PaginatedNotificationDeliveriesResponse>(
`/api/v1/admin/notification-deliveries?${query(v)}`,
);
export const retryDelivery = (id: string) =>
apiRequest<{ queued: true }>(`/api/v1/admin/notification-deliveries/${id}/retry`, {
method: 'POST',
});
+55
View File
@@ -0,0 +1,55 @@
import type {
CompleteFollowUpRequest,
CreateFollowUpRequest,
FollowUpDetailResponse,
FollowUpMutationResponse,
FollowUpStatus,
FollowUpType,
FollowUpWorkbenchSummary,
PaginatedFollowUpsResponse,
} from '@lehuo/contracts';
import { apiRequest } from './api-client';
function queryString(values: Record<string, string | number | boolean | undefined>): string {
const params = new URLSearchParams();
Object.entries(values).forEach(([key, value]) => {
if (value !== undefined && value !== '') params.set(key, String(value));
});
return params.toString();
}
export function fetchFollowUpSummary(): Promise<FollowUpWorkbenchSummary> {
return apiRequest('/api/v1/admin/follow-ups/summary');
}
export function fetchFollowUps(query: {
page: number;
pageSize: number;
status?: FollowUpStatus;
followUpType?: FollowUpType;
overdue?: boolean;
}): Promise<PaginatedFollowUpsResponse> {
return apiRequest(`/api/v1/admin/follow-ups?${queryString(query)}`);
}
export function fetchFollowUp(publicId: string): Promise<FollowUpDetailResponse> {
return apiRequest(`/api/v1/admin/follow-ups/${encodeURIComponent(publicId)}`);
}
export function createFollowUp(input: CreateFollowUpRequest): Promise<FollowUpMutationResponse> {
return apiRequest('/api/v1/admin/follow-ups', {
method: 'POST',
body: JSON.stringify(input),
});
}
export function completeFollowUp(
publicId: string,
input: CompleteFollowUpRequest,
): Promise<FollowUpMutationResponse> {
return apiRequest(`/api/v1/admin/follow-ups/${encodeURIComponent(publicId)}/complete`, {
method: 'POST',
body: JSON.stringify(input),
});
}
+89
View File
@@ -0,0 +1,89 @@
import type {
ActivityDetailResponse,
ActivityRegistrationResponse,
AssigneeOption,
CheckInActivityRegistrationRequest,
CreateActivityRequest,
CreateProjectRequest,
PaginatedActivitiesResponse,
PaginatedActivityRegistrationsResponse,
PaginatedProjectsResponse,
PaginatedVolunteersResponse,
ProjectDetailResponse,
RecordServiceMinutesRequest,
ReviewActivityRegistrationRequest,
ReviewVolunteerRequest,
TransitionActivityRequest,
TransitionProjectRequest,
} from '@lehuo/contracts';
import { apiRequest } from './api-client';
const query = (values: Record<string, string | number | undefined>) => {
const params = new URLSearchParams();
Object.entries(values).forEach(
([key, value]) => value !== undefined && params.set(key, String(value)),
);
return params.toString();
};
export const fetchVolunteers = (values: { page: number; pageSize: number; status?: string }) =>
apiRequest<PaginatedVolunteersResponse>(`/api/v1/admin/volunteers?${query(values)}`);
export const reviewVolunteer = (publicId: string, input: ReviewVolunteerRequest) =>
apiRequest(`/api/v1/admin/volunteers/${publicId}/review`, {
method: 'POST',
body: JSON.stringify(input),
});
export const fetchProjects = (values: { page: number; pageSize: number; status?: string }) =>
apiRequest<PaginatedProjectsResponse>(`/api/v1/admin/projects?${query(values)}`);
export const fetchProjectManagerOptions = () =>
apiRequest<AssigneeOption[]>('/api/v1/admin/project-manager-options');
export const createProject = (input: CreateProjectRequest) =>
apiRequest<ProjectDetailResponse>('/api/v1/admin/projects', {
method: 'POST',
body: JSON.stringify(input),
});
export const transitionProject = (publicId: string, input: TransitionProjectRequest) =>
apiRequest<ProjectDetailResponse>(`/api/v1/admin/projects/${publicId}/transition`, {
method: 'POST',
body: JSON.stringify(input),
});
export const fetchActivities = (values: { page: number; pageSize: number; status?: string }) =>
apiRequest<PaginatedActivitiesResponse>(`/api/v1/admin/activities?${query(values)}`);
export const createActivity = (input: CreateActivityRequest) =>
apiRequest<ActivityDetailResponse>('/api/v1/admin/activities', {
method: 'POST',
body: JSON.stringify(input),
});
export const transitionActivity = (publicId: string, input: TransitionActivityRequest) =>
apiRequest<ActivityDetailResponse>(`/api/v1/admin/activities/${publicId}/transition`, {
method: 'POST',
body: JSON.stringify(input),
});
export const fetchRegistrations = (values: {
page: number;
pageSize: number;
status?: string;
activityPublicId?: string;
}) =>
apiRequest<PaginatedActivityRegistrationsResponse>(
`/api/v1/admin/activity-registrations?${query(values)}`,
);
export const reviewRegistration = (publicId: string, input: ReviewActivityRegistrationRequest) =>
apiRequest<ActivityRegistrationResponse>(
`/api/v1/admin/activity-registrations/${publicId}/review`,
{ method: 'POST', body: JSON.stringify(input) },
);
export const checkInRegistration = (publicId: string, input: CheckInActivityRegistrationRequest) =>
apiRequest<ActivityRegistrationResponse>(
`/api/v1/admin/activity-registrations/${publicId}/check-in`,
{ method: 'POST', body: JSON.stringify(input) },
);
export const recordServiceMinutes = (publicId: string, input: RecordServiceMinutesRequest) =>
apiRequest<ActivityRegistrationResponse>(
`/api/v1/admin/activity-registrations/${publicId}/service-minutes`,
{ method: 'POST', body: JSON.stringify(input) },
);
@@ -0,0 +1,25 @@
import type {
AdminServiceApplicationQuery,
PaginatedServiceApplicationsResponse,
ServiceApplicationDetailResponse,
} from '@lehuo/contracts';
import { apiRequest } from './api-client';
export function fetchAdminServiceApplications(
query: AdminServiceApplicationQuery,
): Promise<PaginatedServiceApplicationsResponse> {
const params = new URLSearchParams();
if (query.page) params.set('page', String(query.page));
if (query.pageSize) params.set('pageSize', String(query.pageSize));
if (query.applicationNo) params.set('applicationNo', query.applicationNo);
if (query.serviceCategory) params.set('serviceCategory', query.serviceCategory);
if (query.status) params.set('status', query.status);
return apiRequest(`/api/v1/admin/service-applications?${params.toString()}`);
}
export function fetchAdminServiceApplication(
publicId: string,
): Promise<ServiceApplicationDetailResponse> {
return apiRequest(`/api/v1/admin/service-applications/${encodeURIComponent(publicId)}`);
}
+8
View File
@@ -0,0 +1,8 @@
import type { StatisticsOverview } from '@lehuo/contracts';
import { apiRequest } from './api-client';
export const fetchStatistics = (startDate?: string, endDate?: string) => {
const p = new URLSearchParams();
if (startDate) p.set('startDate', startDate);
if (endDate) p.set('endDate', endDate);
return apiRequest<StatisticsOverview>(`/api/v1/admin/statistics/overview?${p.toString()}`);
};