feat: complete V1 phases 0-7
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# @lehuo/contracts
|
||||
|
||||
API DTO、枚举和跨端共享类型的单一事实来源。业务枚举与接口契约必须在此维护,禁止前后端分别复制。
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@lehuo/contracts",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"test:unit": "vitest run",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
export const API_ERROR_CODES = {
|
||||
AUTH_REQUIRED: 'AUTH_REQUIRED',
|
||||
AUTH_SESSION_EXPIRED: 'AUTH_SESSION_EXPIRED',
|
||||
FORBIDDEN: 'FORBIDDEN',
|
||||
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
||||
INVALID_CREDENTIALS: 'INVALID_CREDENTIALS',
|
||||
ACCOUNT_LOCKED: 'ACCOUNT_LOCKED',
|
||||
ACCOUNT_DISABLED: 'ACCOUNT_DISABLED',
|
||||
WECHAT_LOGIN_FAILED: 'WECHAT_LOGIN_FAILED',
|
||||
RESOURCE_NOT_FOUND: 'RESOURCE_NOT_FOUND',
|
||||
APPLICATION_NOT_FOUND: 'APPLICATION_NOT_FOUND',
|
||||
BENEFICIARY_NOT_FOUND: 'BENEFICIARY_NOT_FOUND',
|
||||
CASE_NOT_FOUND: 'CASE_NOT_FOUND',
|
||||
SERVICE_RECORD_NOT_FOUND: 'SERVICE_RECORD_NOT_FOUND',
|
||||
FILE_NOT_FOUND: 'FILE_NOT_FOUND',
|
||||
FOLLOW_UP_NOT_FOUND: 'FOLLOW_UP_NOT_FOUND',
|
||||
VOLUNTEER_NOT_FOUND: 'VOLUNTEER_NOT_FOUND',
|
||||
VOLUNTEER_NOT_APPROVED: 'VOLUNTEER_NOT_APPROVED',
|
||||
PROJECT_NOT_FOUND: 'PROJECT_NOT_FOUND',
|
||||
ACTIVITY_NOT_FOUND: 'ACTIVITY_NOT_FOUND',
|
||||
REGISTRATION_NOT_FOUND: 'REGISTRATION_NOT_FOUND',
|
||||
ACTIVITY_FULL: 'ACTIVITY_FULL',
|
||||
REGISTRATION_CLOSED: 'REGISTRATION_CLOSED',
|
||||
RESOURCE_CONFLICT: 'RESOURCE_CONFLICT',
|
||||
UPLOAD_NOT_READY: 'UPLOAD_NOT_READY',
|
||||
STORAGE_UNAVAILABLE: 'STORAGE_UNAVAILABLE',
|
||||
DUPLICATE_REQUEST: 'DUPLICATE_REQUEST',
|
||||
INVALID_STATE_TRANSITION: 'INVALID_STATE_TRANSITION',
|
||||
RATE_LIMITED: 'RATE_LIMITED',
|
||||
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
||||
} as const;
|
||||
|
||||
export type ApiErrorCode = (typeof API_ERROR_CODES)[keyof typeof API_ERROR_CODES];
|
||||
|
||||
export interface ApiSuccessResponse<T> {
|
||||
success: true;
|
||||
data: T;
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
export interface ApiErrorResponse {
|
||||
success: false;
|
||||
error: {
|
||||
code: ApiErrorCode;
|
||||
message: string;
|
||||
details?: Record<string, string[]>;
|
||||
};
|
||||
requestId: string;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { API_ERROR_CODES, PERMISSION_CODES, ROLE_CODES } from './index.js';
|
||||
|
||||
describe('authentication contracts', () => {
|
||||
it('keeps approved role and permission codes centralized', () => {
|
||||
expect(ROLE_CODES).toContain('VOLUNTEER');
|
||||
expect(ROLE_CODES).toContain('SUPER_ADMIN');
|
||||
expect(PERMISSION_CODES.ADMIN_ACCESS).toBe('admin:access');
|
||||
});
|
||||
|
||||
it('uses stable public error codes', () => {
|
||||
expect(API_ERROR_CODES.AUTH_REQUIRED).toBe('AUTH_REQUIRED');
|
||||
expect(API_ERROR_CODES.INVALID_CREDENTIALS).toBe('INVALID_CREDENTIALS');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
export const USER_STATUSES = ['ACTIVE', 'SUSPENDED', 'DISABLED'] as const;
|
||||
export type UserStatus = (typeof USER_STATUSES)[number];
|
||||
|
||||
export const USER_TYPES = ['PUBLIC', 'STAFF'] as const;
|
||||
export type UserType = (typeof USER_TYPES)[number];
|
||||
|
||||
export const ROLE_CODES = [
|
||||
'VOLUNTEER',
|
||||
'STAFF',
|
||||
'PROJECT_MANAGER',
|
||||
'ADMIN',
|
||||
'SUPER_ADMIN',
|
||||
] as const;
|
||||
export type RoleCode = (typeof ROLE_CODES)[number];
|
||||
|
||||
export const PERMISSION_CODES = {
|
||||
ADMIN_ACCESS: 'admin:access',
|
||||
USERS_READ: 'users:read',
|
||||
USERS_MANAGE: 'users:manage',
|
||||
ROLES_MANAGE: 'roles:manage',
|
||||
AUDIT_READ: 'audit:read',
|
||||
SYSTEM_MANAGE: 'system:manage',
|
||||
SENSITIVE_READ: 'sensitive:read',
|
||||
SENSITIVE_EXPORT: 'sensitive:export',
|
||||
RESOURCES_ACCESS_ALL: 'resources:access_all',
|
||||
APPLICATIONS_READ: 'applications:read',
|
||||
APPLICATIONS_MANAGE: 'applications:manage',
|
||||
BENEFICIARIES_READ: 'beneficiaries:read',
|
||||
BENEFICIARIES_MANAGE: 'beneficiaries:manage',
|
||||
CASES_READ: 'cases:read',
|
||||
CASES_MANAGE: 'cases:manage',
|
||||
CASES_ASSIGN: 'cases:assign',
|
||||
ASSIGNED_CASES_READ: 'assigned_cases:read',
|
||||
SERVICE_RECORDS_WRITE: 'service_records:write',
|
||||
FILES_WRITE: 'files:write',
|
||||
FOLLOW_UPS_READ: 'follow_ups:read',
|
||||
FOLLOW_UPS_MANAGE: 'follow_ups:manage',
|
||||
FOLLOW_UPS_COMPLETE: 'follow_ups:complete',
|
||||
VOLUNTEERS_READ: 'volunteers:read',
|
||||
VOLUNTEERS_REVIEW: 'volunteers:review',
|
||||
PROJECTS_READ: 'projects:read',
|
||||
PROJECTS_MANAGE: 'projects:manage',
|
||||
PROJECTS_PUBLISH: 'projects:publish',
|
||||
ACTIVITIES_READ: 'activities:read',
|
||||
ACTIVITIES_MANAGE: 'activities:manage',
|
||||
ACTIVITIES_PUBLISH: 'activities:publish',
|
||||
REGISTRATIONS_MANAGE: 'registrations:manage',
|
||||
CONTENT_READ: 'content:read',
|
||||
CONTENT_MANAGE: 'content:manage',
|
||||
CONTENT_REVIEW: 'content:review',
|
||||
NOTIFICATIONS_MANAGE: 'notifications:manage',
|
||||
STATISTICS_READ: 'statistics:read',
|
||||
EXPORTS_CREATE: 'exports:create',
|
||||
} as const;
|
||||
export type PermissionCode = (typeof PERMISSION_CODES)[keyof typeof PERMISSION_CODES];
|
||||
|
||||
export interface AuthUser {
|
||||
publicId: string;
|
||||
status: UserStatus;
|
||||
userType: UserType;
|
||||
displayName: string | null;
|
||||
roles: RoleCode[];
|
||||
permissions: PermissionCode[];
|
||||
}
|
||||
|
||||
export interface AdminLoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface AdminLoginResponse {
|
||||
user: AuthUser;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
export interface WechatLoginRequest {
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface WechatLoginResponse {
|
||||
accessToken: string;
|
||||
tokenType: 'Bearer';
|
||||
expiresAt: string;
|
||||
user: AuthUser;
|
||||
}
|
||||
|
||||
export interface CurrentUserResponse {
|
||||
user: AuthUser;
|
||||
}
|
||||
|
||||
export interface LogoutResponse {
|
||||
loggedOut: true;
|
||||
}
|
||||
|
||||
export interface UpdateUserRolesRequest {
|
||||
roles: RoleCode[];
|
||||
}
|
||||
|
||||
export interface UpdateUserRolesResponse {
|
||||
userPublicId: string;
|
||||
roles: RoleCode[];
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
import type { ServiceApplicationStatus, ServiceCategory } from './service-applications.js';
|
||||
|
||||
export const BENEFICIARY_GENDERS = ['FEMALE', 'MALE', 'OTHER', 'UNDISCLOSED'] as const;
|
||||
export type BeneficiaryGender = (typeof BENEFICIARY_GENDERS)[number];
|
||||
|
||||
export const BENEFICIARY_STATUSES = ['ACTIVE', 'INACTIVE', 'ARCHIVED'] as const;
|
||||
export type BeneficiaryStatus = (typeof BENEFICIARY_STATUSES)[number];
|
||||
|
||||
export const SERVICE_CASE_PRIORITIES = ['LOW', 'NORMAL', 'HIGH', 'URGENT'] as const;
|
||||
export type ServiceCasePriority = (typeof SERVICE_CASE_PRIORITIES)[number];
|
||||
|
||||
export const SERVICE_CASE_STATUSES = [
|
||||
'ASSIGNED',
|
||||
'IN_SERVICE',
|
||||
'SERVICE_COMPLETED',
|
||||
'SUSPENDED',
|
||||
'CANCELLED',
|
||||
] as const;
|
||||
export type ServiceCaseStatus = (typeof SERVICE_CASE_STATUSES)[number];
|
||||
|
||||
export const CASE_ASSIGNEE_TYPES = ['STAFF', 'VOLUNTEER'] as const;
|
||||
export type CaseAssigneeType = (typeof CASE_ASSIGNEE_TYPES)[number];
|
||||
|
||||
export const SERVICE_RECORD_TYPES = [
|
||||
'HOME_VISIT',
|
||||
'PHONE_CONTACT',
|
||||
'ESCORT',
|
||||
'MATERIAL_SUPPORT',
|
||||
'ACTIVITY',
|
||||
'OTHER',
|
||||
] as const;
|
||||
export type ServiceRecordType = (typeof SERVICE_RECORD_TYPES)[number];
|
||||
|
||||
export const BENEFICIARY_CONFIRM_STATUSES = [
|
||||
'NOT_REQUESTED',
|
||||
'CONFIRMED',
|
||||
'PARTIALLY_CONFIRMED',
|
||||
'DISPUTED',
|
||||
] as const;
|
||||
export type BeneficiaryConfirmStatus = (typeof BENEFICIARY_CONFIRM_STATUSES)[number];
|
||||
|
||||
export const SERVICE_CASE_STATUS_LABELS: Record<ServiceCaseStatus, string> = {
|
||||
ASSIGNED: '已指派',
|
||||
IN_SERVICE: '服务中',
|
||||
SERVICE_COMPLETED: '服务已完成',
|
||||
SUSPENDED: '服务暂缓',
|
||||
CANCELLED: '已取消',
|
||||
};
|
||||
|
||||
export const SERVICE_CASE_PRIORITY_LABELS: Record<ServiceCasePriority, string> = {
|
||||
LOW: '低',
|
||||
NORMAL: '一般',
|
||||
HIGH: '较高',
|
||||
URGENT: '紧急',
|
||||
};
|
||||
|
||||
export const SERVICE_RECORD_TYPE_LABELS: Record<ServiceRecordType, string> = {
|
||||
HOME_VISIT: '上门探访',
|
||||
PHONE_CONTACT: '电话关怀',
|
||||
ESCORT: '陪同协助',
|
||||
MATERIAL_SUPPORT: '物资支持',
|
||||
ACTIVITY: '公益活动',
|
||||
OTHER: '其他服务',
|
||||
};
|
||||
|
||||
export interface BeneficiaryContactInput {
|
||||
name: string;
|
||||
relation: string;
|
||||
mobile: string;
|
||||
isGuardian: boolean;
|
||||
isEmergencyContact: boolean;
|
||||
}
|
||||
|
||||
export interface NewBeneficiaryInput {
|
||||
displayName: string;
|
||||
gender?: BeneficiaryGender;
|
||||
birthYear?: number;
|
||||
mobile?: string;
|
||||
regionCode: string;
|
||||
addressDetail?: string;
|
||||
keyCareFlag: boolean;
|
||||
isMinor: boolean;
|
||||
notes?: string;
|
||||
contacts: BeneficiaryContactInput[];
|
||||
tagCodes: string[];
|
||||
guardianConsentConfirmed?: true;
|
||||
}
|
||||
|
||||
export interface AcceptServiceApplicationRequest {
|
||||
clientRequestId: string;
|
||||
beneficiary:
|
||||
| { mode: 'CREATE'; profile: NewBeneficiaryInput }
|
||||
| {
|
||||
mode: 'LINK_EXISTING';
|
||||
beneficiaryPublicId: string;
|
||||
manualMatchConfirmed: true;
|
||||
};
|
||||
ownerStaffPublicId: string;
|
||||
collaboratorUserPublicIds: string[];
|
||||
priority: ServiceCasePriority;
|
||||
plannedStartAt?: string;
|
||||
plannedEndAt?: string;
|
||||
serviceLocation?: string;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface TransitionServiceApplicationRequest {
|
||||
targetStatus: Extract<ServiceApplicationStatus, 'PENDING_CONTACT' | 'ASSESSING'>;
|
||||
reasonCode: string;
|
||||
}
|
||||
|
||||
export interface BeneficiaryContactDetail {
|
||||
publicId: string;
|
||||
name: string;
|
||||
relation: string;
|
||||
mobile: string;
|
||||
mobileMasked: string;
|
||||
isGuardian: boolean;
|
||||
isEmergencyContact: boolean;
|
||||
hasConsentRecord: boolean;
|
||||
}
|
||||
|
||||
export interface BeneficiaryTagOption {
|
||||
code: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
export interface BeneficiaryListItem {
|
||||
publicId: string;
|
||||
displayName: string;
|
||||
gender: BeneficiaryGender | null;
|
||||
birthYear: number | null;
|
||||
mobileMasked: string | null;
|
||||
regionCode: string;
|
||||
primaryCategory: ServiceCategory;
|
||||
keyCareFlag: boolean;
|
||||
isMinor: boolean;
|
||||
status: BeneficiaryStatus;
|
||||
tags: BeneficiaryTagOption[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface BeneficiaryDetail extends BeneficiaryListItem {
|
||||
mobile: string | null;
|
||||
addressDetail: string | null;
|
||||
notes: string | null;
|
||||
contacts: BeneficiaryContactDetail[];
|
||||
serviceCaseCount: number;
|
||||
}
|
||||
|
||||
export interface PaginatedBeneficiariesResponse {
|
||||
items: BeneficiaryListItem[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface BeneficiaryDetailResponse {
|
||||
beneficiary: BeneficiaryDetail;
|
||||
}
|
||||
|
||||
export interface UpdateBeneficiaryRequest extends NewBeneficiaryInput {
|
||||
status: BeneficiaryStatus;
|
||||
}
|
||||
|
||||
export interface AssigneeOption {
|
||||
userPublicId: string;
|
||||
displayName: string;
|
||||
type: CaseAssigneeType;
|
||||
department: string | null;
|
||||
}
|
||||
|
||||
export interface CaseAssigneeItem extends AssigneeOption {
|
||||
role: 'OWNER' | 'COLLABORATOR';
|
||||
assignedAt: string;
|
||||
}
|
||||
|
||||
export interface ServiceCaseHistoryItem {
|
||||
status: ServiceCaseStatus;
|
||||
statusLabel: string;
|
||||
reasonLabel: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ServiceRecordAttachment {
|
||||
publicId: string;
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface ServiceRecordItem {
|
||||
publicId: string;
|
||||
serviceStartAt: string;
|
||||
serviceEndAt: string | null;
|
||||
serviceType: ServiceRecordType;
|
||||
serviceTypeLabel: string;
|
||||
content: string;
|
||||
result: string;
|
||||
followUpSuggestion: string | null;
|
||||
beneficiaryConfirmStatus: BeneficiaryConfirmStatus;
|
||||
createdByDisplayName: string;
|
||||
revision: number;
|
||||
attachments: ServiceRecordAttachment[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ServiceCaseListItem {
|
||||
publicId: string;
|
||||
caseNo: string;
|
||||
applicationPublicId: string;
|
||||
applicationNo: string;
|
||||
beneficiaryPublicId: string;
|
||||
beneficiaryDisplayName: string;
|
||||
category: ServiceCategory;
|
||||
priority: ServiceCasePriority;
|
||||
priorityLabel: string;
|
||||
status: ServiceCaseStatus;
|
||||
statusLabel: string;
|
||||
ownerDisplayName: string;
|
||||
plannedStartAt: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ServiceCaseDetail extends ServiceCaseListItem {
|
||||
regionCode: string;
|
||||
beneficiaryMobile: string | null;
|
||||
beneficiaryMobileMasked: string | null;
|
||||
serviceLocation: string | null;
|
||||
summary: string;
|
||||
resultSummary: string | null;
|
||||
followUpPlan: string | null;
|
||||
completedAt: string | null;
|
||||
assignees: CaseAssigneeItem[];
|
||||
history: ServiceCaseHistoryItem[];
|
||||
records: ServiceRecordItem[];
|
||||
}
|
||||
|
||||
export interface PaginatedServiceCasesResponse {
|
||||
items: ServiceCaseListItem[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ServiceCaseDetailResponse {
|
||||
serviceCase: ServiceCaseDetail;
|
||||
}
|
||||
|
||||
export interface AcceptServiceApplicationResponse {
|
||||
serviceCase: ServiceCaseDetail;
|
||||
idempotentReplay: boolean;
|
||||
}
|
||||
|
||||
export interface ReplaceCaseAssigneesRequest {
|
||||
collaboratorUserPublicIds: string[];
|
||||
}
|
||||
|
||||
export interface CreateServiceRecordRequest {
|
||||
clientRequestId: string;
|
||||
serviceStartAt: string;
|
||||
serviceEndAt?: string;
|
||||
serviceType: ServiceRecordType;
|
||||
content: string;
|
||||
result: string;
|
||||
followUpSuggestion?: string;
|
||||
beneficiaryConfirmStatus: BeneficiaryConfirmStatus;
|
||||
attachmentPublicIds: string[];
|
||||
}
|
||||
|
||||
export interface UpdateServiceRecordRequest extends Omit<
|
||||
CreateServiceRecordRequest,
|
||||
'clientRequestId'
|
||||
> {
|
||||
expectedRevision: number;
|
||||
}
|
||||
|
||||
export interface ServiceRecordResponse {
|
||||
record: ServiceRecordItem;
|
||||
idempotentReplay?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateUploadIntentRequest {
|
||||
clientRequestId: string;
|
||||
filename: string;
|
||||
mimeType: 'image/jpeg' | 'image/png' | 'image/webp';
|
||||
size: number;
|
||||
sha256?: string;
|
||||
}
|
||||
|
||||
export interface CreateUploadIntentResponse {
|
||||
file: ServiceRecordAttachment;
|
||||
uploadUrl: string;
|
||||
uploadHeaders: Record<string, string>;
|
||||
expiresAt: string;
|
||||
idempotentReplay: boolean;
|
||||
}
|
||||
|
||||
export interface ConfirmUploadResponse {
|
||||
file: ServiceRecordAttachment;
|
||||
}
|
||||
|
||||
export interface DownloadUrlResponse {
|
||||
downloadUrl: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
export const CONTENT_POST_TYPES = [
|
||||
'ORGANIZATION_INTRO',
|
||||
'PUBLIC_WELFARE_NEWS',
|
||||
'SERVICE_STORY',
|
||||
'NOTICE',
|
||||
'FAQ',
|
||||
] as const;
|
||||
export type ContentPostType = (typeof CONTENT_POST_TYPES)[number];
|
||||
export const CONTENT_POST_TYPE_LABELS: Record<ContentPostType, string> = {
|
||||
ORGANIZATION_INTRO: '机构介绍',
|
||||
PUBLIC_WELFARE_NEWS: '公益动态',
|
||||
SERVICE_STORY: '服务故事',
|
||||
NOTICE: '通知公告',
|
||||
FAQ: '常见问题',
|
||||
};
|
||||
export const CONTENT_POST_STATUSES = ['DRAFT', 'PENDING_REVIEW', 'PUBLISHED', 'ARCHIVED'] as const;
|
||||
export type ContentPostStatus = (typeof CONTENT_POST_STATUSES)[number];
|
||||
export const CONTENT_POST_STATUS_LABELS: Record<ContentPostStatus, string> = {
|
||||
DRAFT: '草稿',
|
||||
PENDING_REVIEW: '待审核',
|
||||
PUBLISHED: '已发布',
|
||||
ARCHIVED: '已归档',
|
||||
};
|
||||
export interface ContentPostListItem {
|
||||
publicId: string;
|
||||
type: ContentPostType;
|
||||
title: string;
|
||||
summary: string;
|
||||
status: ContentPostStatus;
|
||||
revision: number;
|
||||
publishedAt: string | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
export interface ContentStatusHistoryItem {
|
||||
fromStatus: ContentPostStatus | null;
|
||||
toStatus: ContentPostStatus;
|
||||
reasonCode: string;
|
||||
note: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface ContentPostDetail extends ContentPostListItem {
|
||||
content: string;
|
||||
coverFilePublicId: string | null;
|
||||
privacyReviewedAt: string | null;
|
||||
reviewedAt: string | null;
|
||||
history: ContentStatusHistoryItem[];
|
||||
}
|
||||
export interface PaginatedContentPostsResponse {
|
||||
items: ContentPostListItem[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
export interface ContentPostDetailResponse {
|
||||
post: ContentPostDetail;
|
||||
idempotentReplay?: boolean;
|
||||
}
|
||||
export interface CreateContentPostRequest {
|
||||
clientRequestId: string;
|
||||
type: ContentPostType;
|
||||
title: string;
|
||||
summary: string;
|
||||
content: string;
|
||||
coverFilePublicId?: string;
|
||||
}
|
||||
export interface UpdateContentPostRequest extends Omit<
|
||||
CreateContentPostRequest,
|
||||
'clientRequestId'
|
||||
> {
|
||||
expectedRevision: number;
|
||||
}
|
||||
export interface TransitionContentPostRequest {
|
||||
clientRequestId: string;
|
||||
targetStatus: ContentPostStatus;
|
||||
note: string;
|
||||
privacyReviewed?: true;
|
||||
}
|
||||
|
||||
export const NOTIFICATION_TYPES = [
|
||||
'APPLICATION_SUBMITTED',
|
||||
'APPLICATION_ACCEPTED',
|
||||
'SERVICE_ARRANGED',
|
||||
'SERVICE_COMPLETED',
|
||||
'FOLLOW_UP_PENDING',
|
||||
'ACTIVITY_REGISTRATION_RESULT',
|
||||
] as const;
|
||||
export type NotificationType = (typeof NOTIFICATION_TYPES)[number];
|
||||
export const NOTIFICATION_TYPE_LABELS: Record<NotificationType, string> = {
|
||||
APPLICATION_SUBMITTED: '申请已提交',
|
||||
APPLICATION_ACCEPTED: '申请已受理',
|
||||
SERVICE_ARRANGED: '服务已安排',
|
||||
SERVICE_COMPLETED: '服务已完成',
|
||||
FOLLOW_UP_PENDING: '待回访',
|
||||
ACTIVITY_REGISTRATION_RESULT: '活动报名结果',
|
||||
};
|
||||
export interface NotificationItem {
|
||||
publicId: string;
|
||||
type: NotificationType;
|
||||
title: string;
|
||||
body: string;
|
||||
resourceType: string | null;
|
||||
resourcePublicId: string | null;
|
||||
readAt: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface PaginatedNotificationsResponse {
|
||||
items: NotificationItem[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
unreadCount: number;
|
||||
}
|
||||
export interface NotificationPreferenceItem {
|
||||
type: NotificationType;
|
||||
inAppEnabled: boolean;
|
||||
wechatEnabled: boolean;
|
||||
authorizedAt: string | null;
|
||||
}
|
||||
export interface NotificationPreferencesResponse {
|
||||
items: NotificationPreferenceItem[];
|
||||
wechatAdapterConfigured: boolean;
|
||||
}
|
||||
export interface UpdateNotificationPreferencesRequest {
|
||||
items: Array<{ type: NotificationType; inAppEnabled: boolean; wechatEnabled: boolean }>;
|
||||
}
|
||||
export interface NotificationDeliveryItem {
|
||||
publicId: string;
|
||||
notificationPublicId: string;
|
||||
type: NotificationType;
|
||||
channel: 'WECHAT_SUBSCRIPTION';
|
||||
status: 'PENDING' | 'PROCESSING' | 'SENT' | 'FAILED' | 'DEAD';
|
||||
attemptCount: number;
|
||||
nextAttemptAt: string;
|
||||
lastAttemptAt: string | null;
|
||||
errorCode: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface PaginatedNotificationDeliveriesResponse {
|
||||
items: NotificationDeliveryItem[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
FOLLOW_UP_RESULT_CODES,
|
||||
FOLLOW_UP_RESULT_LABELS,
|
||||
FOLLOW_UP_STATUS_LABELS,
|
||||
FOLLOW_UP_TYPES,
|
||||
FOLLOW_UP_TYPE_LABELS,
|
||||
} from './follow-ups.js';
|
||||
|
||||
describe('follow-up contracts', () => {
|
||||
it('keeps every persisted enum value mapped to a user-facing label', () => {
|
||||
expect(Object.keys(FOLLOW_UP_TYPE_LABELS).sort()).toEqual([...FOLLOW_UP_TYPES].sort());
|
||||
expect(Object.keys(FOLLOW_UP_RESULT_LABELS).sort()).toEqual([...FOLLOW_UP_RESULT_CODES].sort());
|
||||
expect(FOLLOW_UP_STATUS_LABELS.PENDING).toBe('待回访');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
import type { ServiceCategory } from './service-applications.js';
|
||||
|
||||
export const FOLLOW_UP_TYPES = ['SERVICE_COMPLETION', 'KEY_CARE', 'MANUAL'] as const;
|
||||
export type FollowUpType = (typeof FOLLOW_UP_TYPES)[number];
|
||||
|
||||
export const FOLLOW_UP_STATUSES = ['PENDING', 'COMPLETED', 'CANCELLED'] as const;
|
||||
export type FollowUpStatus = (typeof FOLLOW_UP_STATUSES)[number];
|
||||
|
||||
export const FOLLOW_UP_RESULT_CODES = [
|
||||
'SATISFIED',
|
||||
'GENERALLY_SATISFIED',
|
||||
'UNSATISFIED',
|
||||
'NEED_MORE_HELP',
|
||||
'UNREACHABLE',
|
||||
'OTHER',
|
||||
] as const;
|
||||
export type FollowUpResultCode = (typeof FOLLOW_UP_RESULT_CODES)[number];
|
||||
|
||||
export const FOLLOW_UP_TYPE_LABELS: Record<FollowUpType, string> = {
|
||||
SERVICE_COMPLETION: '服务完成回访',
|
||||
KEY_CARE: '重点关爱回访',
|
||||
MANUAL: '人工回访',
|
||||
};
|
||||
|
||||
export const FOLLOW_UP_STATUS_LABELS: Record<FollowUpStatus, string> = {
|
||||
PENDING: '待回访',
|
||||
COMPLETED: '已完成',
|
||||
CANCELLED: '已取消',
|
||||
};
|
||||
|
||||
export const FOLLOW_UP_RESULT_LABELS: Record<FollowUpResultCode, string> = {
|
||||
SATISFIED: '满意',
|
||||
GENERALLY_SATISFIED: '基本满意',
|
||||
UNSATISFIED: '不满意',
|
||||
NEED_MORE_HELP: '需要继续帮扶',
|
||||
UNREACHABLE: '未能联系',
|
||||
OTHER: '其他',
|
||||
};
|
||||
|
||||
export interface CompleteServiceCaseRequest {
|
||||
clientRequestId: string;
|
||||
resultSummary: string;
|
||||
followUpPlan: string;
|
||||
followUpDueAt: string;
|
||||
}
|
||||
|
||||
export interface CompleteServiceCaseResponse {
|
||||
serviceCasePublicId: string;
|
||||
serviceCaseStatus: 'SERVICE_COMPLETED';
|
||||
applicationStatus: 'FOLLOW_UP_PENDING';
|
||||
followUp: FollowUpListItem;
|
||||
idempotentReplay: boolean;
|
||||
}
|
||||
|
||||
export interface CreateFollowUpRequest {
|
||||
clientRequestId: string;
|
||||
beneficiaryPublicId: string;
|
||||
ownerStaffPublicId: string;
|
||||
serviceCasePublicId?: string;
|
||||
followUpType: Extract<FollowUpType, 'KEY_CARE' | 'MANUAL'>;
|
||||
dueAt: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface CompleteFollowUpRequest {
|
||||
clientRequestId: string;
|
||||
resultCode: FollowUpResultCode;
|
||||
resultNote: string;
|
||||
continuationConsentConfirmed?: true;
|
||||
continuationDescription?: string;
|
||||
}
|
||||
|
||||
export interface FollowUpListItem {
|
||||
publicId: string;
|
||||
followUpNo: string;
|
||||
beneficiaryPublicId: string;
|
||||
beneficiaryDisplayName: string;
|
||||
beneficiaryKeyCareFlag: boolean;
|
||||
category: ServiceCategory;
|
||||
serviceCasePublicId: string | null;
|
||||
serviceCaseNo: string | null;
|
||||
ownerStaffPublicId: string;
|
||||
ownerDisplayName: string;
|
||||
dueAt: string;
|
||||
followUpType: FollowUpType;
|
||||
followUpTypeLabel: string;
|
||||
status: FollowUpStatus;
|
||||
statusLabel: string;
|
||||
resultCode: FollowUpResultCode | null;
|
||||
resultLabel: string | null;
|
||||
createdAt: string;
|
||||
completedAt: string | null;
|
||||
}
|
||||
|
||||
export interface FollowUpDetail extends FollowUpListItem {
|
||||
reason: string;
|
||||
resultNote: string | null;
|
||||
continuedApplicationPublicId: string | null;
|
||||
continuedApplicationNo: string | null;
|
||||
}
|
||||
|
||||
export interface FollowUpDetailResponse {
|
||||
followUp: FollowUpDetail;
|
||||
}
|
||||
|
||||
export interface FollowUpMutationResponse extends FollowUpDetailResponse {
|
||||
idempotentReplay: boolean;
|
||||
}
|
||||
|
||||
export interface PaginatedFollowUpsResponse {
|
||||
items: FollowUpListItem[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface FollowUpWorkbenchSummary {
|
||||
dueToday: number;
|
||||
overdue: number;
|
||||
dueThisWeek: number;
|
||||
keyCarePending: number;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { HEALTH_STATUS } from './health.js';
|
||||
|
||||
describe('health contract', () => {
|
||||
it('uses a stable healthy status', () => {
|
||||
expect(HEALTH_STATUS).toBe('ok');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
export const HEALTH_STATUS = 'ok' as const;
|
||||
|
||||
export interface HealthResponse {
|
||||
status: typeof HEALTH_STATUS;
|
||||
service: 'api';
|
||||
timestamp: string;
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
export { API_ERROR_CODES } from './api.js';
|
||||
export type { ApiErrorCode, ApiErrorResponse, ApiSuccessResponse } from './api.js';
|
||||
export { PERMISSION_CODES, ROLE_CODES, USER_STATUSES, USER_TYPES } from './auth.js';
|
||||
export type {
|
||||
AdminLoginRequest,
|
||||
AdminLoginResponse,
|
||||
AuthUser,
|
||||
CurrentUserResponse,
|
||||
LogoutResponse,
|
||||
PermissionCode,
|
||||
RoleCode,
|
||||
UserStatus,
|
||||
UserType,
|
||||
UpdateUserRolesRequest,
|
||||
UpdateUserRolesResponse,
|
||||
WechatLoginRequest,
|
||||
WechatLoginResponse,
|
||||
} from './auth.js';
|
||||
export { HEALTH_STATUS } from './health.js';
|
||||
export type { HealthResponse } from './health.js';
|
||||
export {
|
||||
APPLICATION_APPLY_MODE_LABELS,
|
||||
APPLICATION_APPLY_MODES,
|
||||
SERVICE_APPLICATION_STATUS_LABELS,
|
||||
SERVICE_APPLICATION_STATUSES,
|
||||
SERVICE_CATEGORIES,
|
||||
SERVICE_CATEGORY_LABELS,
|
||||
} from './service-applications.js';
|
||||
export type { StatisticsOverview } from './statistics.js';
|
||||
export {
|
||||
CONTENT_POST_STATUSES,
|
||||
CONTENT_POST_STATUS_LABELS,
|
||||
CONTENT_POST_TYPES,
|
||||
CONTENT_POST_TYPE_LABELS,
|
||||
NOTIFICATION_TYPES,
|
||||
NOTIFICATION_TYPE_LABELS,
|
||||
} from './content-notifications.js';
|
||||
export type {
|
||||
ContentPostType,
|
||||
ContentPostStatus,
|
||||
ContentPostListItem,
|
||||
ContentPostDetail,
|
||||
ContentStatusHistoryItem,
|
||||
PaginatedContentPostsResponse,
|
||||
ContentPostDetailResponse,
|
||||
CreateContentPostRequest,
|
||||
UpdateContentPostRequest,
|
||||
TransitionContentPostRequest,
|
||||
NotificationType,
|
||||
NotificationItem,
|
||||
PaginatedNotificationsResponse,
|
||||
NotificationPreferenceItem,
|
||||
NotificationPreferencesResponse,
|
||||
UpdateNotificationPreferencesRequest,
|
||||
NotificationDeliveryItem,
|
||||
PaginatedNotificationDeliveriesResponse,
|
||||
} from './content-notifications.js';
|
||||
export {
|
||||
FOLLOW_UP_RESULT_CODES,
|
||||
FOLLOW_UP_RESULT_LABELS,
|
||||
FOLLOW_UP_STATUSES,
|
||||
FOLLOW_UP_STATUS_LABELS,
|
||||
FOLLOW_UP_TYPES,
|
||||
FOLLOW_UP_TYPE_LABELS,
|
||||
} from './follow-ups.js';
|
||||
export {
|
||||
ACTIVITY_STATUSES,
|
||||
ACTIVITY_STATUS_LABELS,
|
||||
CHECK_IN_STATUSES,
|
||||
CHECK_IN_STATUS_LABELS,
|
||||
PROJECT_STATUSES,
|
||||
PROJECT_STATUS_LABELS,
|
||||
REGISTRATION_STATUSES,
|
||||
REGISTRATION_STATUS_LABELS,
|
||||
VOLUNTEER_SKILL_CODES,
|
||||
VOLUNTEER_SKILL_LABELS,
|
||||
VOLUNTEER_STATUSES,
|
||||
VOLUNTEER_STATUS_LABELS,
|
||||
} from './participation.js';
|
||||
export type {
|
||||
ActivityCheckInStatus,
|
||||
ActivityDetail,
|
||||
ActivityDetailResponse,
|
||||
ActivityListItem,
|
||||
ActivityRegistrationItem,
|
||||
ActivityRegistrationResponse,
|
||||
ActivityRegistrationStatus,
|
||||
ActivityStatus,
|
||||
AdminVolunteerDetailResponse,
|
||||
AdminVolunteerListItem,
|
||||
ApplyVolunteerRequest,
|
||||
CheckInActivityRegistrationRequest,
|
||||
CreateActivityRequest,
|
||||
CreateMediaUploadIntentRequest,
|
||||
CreateProjectRequest,
|
||||
PaginatedActivitiesResponse,
|
||||
PaginatedActivityRegistrationsResponse,
|
||||
PaginatedProjectsResponse,
|
||||
PaginatedVolunteersResponse,
|
||||
ProjectDetail,
|
||||
ProjectDetailResponse,
|
||||
ProjectListItem,
|
||||
ProjectStatus,
|
||||
RecordServiceMinutesRequest,
|
||||
RegisterActivityRequest,
|
||||
ReplaceProjectCasesRequest,
|
||||
RestoreVolunteerRequest,
|
||||
ReviewActivityRegistrationRequest,
|
||||
ReviewVolunteerRequest,
|
||||
TransitionActivityRequest,
|
||||
TransitionProjectRequest,
|
||||
UpdateActivityRequest,
|
||||
UpdateProjectRequest,
|
||||
VolunteerProfile,
|
||||
VolunteerProfileResponse,
|
||||
VolunteerSkillCode,
|
||||
VolunteerStatus,
|
||||
} from './participation.js';
|
||||
export type {
|
||||
CompleteFollowUpRequest,
|
||||
CompleteServiceCaseRequest,
|
||||
CompleteServiceCaseResponse,
|
||||
CreateFollowUpRequest,
|
||||
FollowUpDetail,
|
||||
FollowUpDetailResponse,
|
||||
FollowUpListItem,
|
||||
FollowUpMutationResponse,
|
||||
FollowUpResultCode,
|
||||
FollowUpStatus,
|
||||
FollowUpType,
|
||||
FollowUpWorkbenchSummary,
|
||||
PaginatedFollowUpsResponse,
|
||||
} from './follow-ups.js';
|
||||
export {
|
||||
BENEFICIARY_CONFIRM_STATUSES,
|
||||
BENEFICIARY_GENDERS,
|
||||
BENEFICIARY_STATUSES,
|
||||
CASE_ASSIGNEE_TYPES,
|
||||
SERVICE_CASE_PRIORITIES,
|
||||
SERVICE_CASE_PRIORITY_LABELS,
|
||||
SERVICE_CASE_STATUSES,
|
||||
SERVICE_CASE_STATUS_LABELS,
|
||||
SERVICE_RECORD_TYPES,
|
||||
SERVICE_RECORD_TYPE_LABELS,
|
||||
} from './case-management.js';
|
||||
export type {
|
||||
AcceptServiceApplicationRequest,
|
||||
AcceptServiceApplicationResponse,
|
||||
AssigneeOption,
|
||||
BeneficiaryConfirmStatus,
|
||||
BeneficiaryContactDetail,
|
||||
BeneficiaryContactInput,
|
||||
BeneficiaryDetail,
|
||||
BeneficiaryDetailResponse,
|
||||
BeneficiaryGender,
|
||||
BeneficiaryListItem,
|
||||
BeneficiaryStatus,
|
||||
BeneficiaryTagOption,
|
||||
CaseAssigneeItem,
|
||||
CaseAssigneeType,
|
||||
ConfirmUploadResponse,
|
||||
CreateServiceRecordRequest,
|
||||
CreateUploadIntentRequest,
|
||||
CreateUploadIntentResponse,
|
||||
DownloadUrlResponse,
|
||||
NewBeneficiaryInput,
|
||||
PaginatedBeneficiariesResponse,
|
||||
PaginatedServiceCasesResponse,
|
||||
ReplaceCaseAssigneesRequest,
|
||||
ServiceCaseDetail,
|
||||
ServiceCaseDetailResponse,
|
||||
ServiceCaseHistoryItem,
|
||||
ServiceCaseListItem,
|
||||
ServiceCasePriority,
|
||||
ServiceCaseStatus,
|
||||
ServiceRecordAttachment,
|
||||
ServiceRecordItem,
|
||||
ServiceRecordResponse,
|
||||
ServiceRecordType,
|
||||
TransitionServiceApplicationRequest,
|
||||
UpdateBeneficiaryRequest,
|
||||
UpdateServiceRecordRequest,
|
||||
} from './case-management.js';
|
||||
export type {
|
||||
AdminServiceApplicationQuery,
|
||||
ApplicationApplyMode,
|
||||
CreateServiceApplicationRequest,
|
||||
CreateServiceApplicationResponse,
|
||||
PaginatedServiceApplicationsResponse,
|
||||
ServiceApplicationDetail,
|
||||
ServiceApplicationDetailResponse,
|
||||
ServiceApplicationHistoryItem,
|
||||
ServiceApplicationListItem,
|
||||
ServiceApplicationStatus,
|
||||
ServiceCategoriesResponse,
|
||||
ServiceCategory,
|
||||
ServiceCategoryOption,
|
||||
} from './service-applications.js';
|
||||
@@ -0,0 +1,363 @@
|
||||
import type { ServiceCategory } from './service-applications.js';
|
||||
|
||||
export const VOLUNTEER_STATUSES = [
|
||||
'APPLIED',
|
||||
'APPROVED',
|
||||
'REJECTED',
|
||||
'SUSPENDED',
|
||||
'INACTIVE',
|
||||
] as const;
|
||||
export type VolunteerStatus = (typeof VOLUNTEER_STATUSES)[number];
|
||||
|
||||
export const VOLUNTEER_STATUS_LABELS: Record<VolunteerStatus, string> = {
|
||||
APPLIED: '待审核',
|
||||
APPROVED: '已认证',
|
||||
REJECTED: '未通过',
|
||||
SUSPENDED: '已暂停',
|
||||
INACTIVE: '已停用',
|
||||
};
|
||||
|
||||
export const VOLUNTEER_SKILL_CODES = [
|
||||
'ELDER_CARE',
|
||||
'CHILD_CARE',
|
||||
'DISABILITY_SUPPORT',
|
||||
'EVENT_SUPPORT',
|
||||
'FIRST_AID',
|
||||
'PHOTOGRAPHY',
|
||||
] as const;
|
||||
export type VolunteerSkillCode = (typeof VOLUNTEER_SKILL_CODES)[number];
|
||||
|
||||
export const VOLUNTEER_SKILL_LABELS: Record<VolunteerSkillCode, string> = {
|
||||
ELDER_CARE: '老年关怀',
|
||||
CHILD_CARE: '儿童陪伴',
|
||||
DISABILITY_SUPPORT: '残障支持',
|
||||
EVENT_SUPPORT: '活动协助',
|
||||
FIRST_AID: '急救技能',
|
||||
PHOTOGRAPHY: '摄影记录',
|
||||
};
|
||||
|
||||
export const PROJECT_STATUSES = ['DRAFT', 'PUBLISHED', 'COMPLETED'] as const;
|
||||
export type ProjectStatus = (typeof PROJECT_STATUSES)[number];
|
||||
export const PROJECT_STATUS_LABELS: Record<ProjectStatus, string> = {
|
||||
DRAFT: '草稿',
|
||||
PUBLISHED: '已发布',
|
||||
COMPLETED: '已完成',
|
||||
};
|
||||
|
||||
export const ACTIVITY_STATUSES = [
|
||||
'DRAFT',
|
||||
'PUBLISHED',
|
||||
'REGISTRATION_CLOSED',
|
||||
'IN_PROGRESS',
|
||||
'COMPLETED',
|
||||
'CANCELLED',
|
||||
] as const;
|
||||
export type ActivityStatus = (typeof ACTIVITY_STATUSES)[number];
|
||||
export const ACTIVITY_STATUS_LABELS: Record<ActivityStatus, string> = {
|
||||
DRAFT: '草稿',
|
||||
PUBLISHED: '报名中',
|
||||
REGISTRATION_CLOSED: '报名已截止',
|
||||
IN_PROGRESS: '进行中',
|
||||
COMPLETED: '已完成',
|
||||
CANCELLED: '已取消',
|
||||
};
|
||||
|
||||
export const REGISTRATION_STATUSES = ['PENDING', 'APPROVED', 'REJECTED', 'CANCELLED'] as const;
|
||||
export type ActivityRegistrationStatus = (typeof REGISTRATION_STATUSES)[number];
|
||||
export const REGISTRATION_STATUS_LABELS: Record<ActivityRegistrationStatus, string> = {
|
||||
PENDING: '待审核',
|
||||
APPROVED: '报名成功',
|
||||
REJECTED: '未通过',
|
||||
CANCELLED: '已取消',
|
||||
};
|
||||
|
||||
export const CHECK_IN_STATUSES = ['NOT_CHECKED_IN', 'CHECKED_IN', 'ABSENT'] as const;
|
||||
export type ActivityCheckInStatus = (typeof CHECK_IN_STATUSES)[number];
|
||||
export const CHECK_IN_STATUS_LABELS: Record<ActivityCheckInStatus, string> = {
|
||||
NOT_CHECKED_IN: '未签到',
|
||||
CHECKED_IN: '已签到',
|
||||
ABSENT: '未到场',
|
||||
};
|
||||
|
||||
export interface ApplyVolunteerRequest {
|
||||
clientRequestId: string;
|
||||
expectedRevision?: number;
|
||||
realName: string;
|
||||
mobile: string;
|
||||
regionCode: string;
|
||||
availabilityNote: string;
|
||||
skillCodes: VolunteerSkillCode[];
|
||||
interestCategories: ServiceCategory[];
|
||||
privacyConsent: true;
|
||||
}
|
||||
|
||||
export interface VolunteerStatusHistoryItem {
|
||||
status: VolunteerStatus;
|
||||
statusLabel: string;
|
||||
reasonLabel: string;
|
||||
note: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface VolunteerProfile {
|
||||
publicId: string;
|
||||
realName: string;
|
||||
mobile: string;
|
||||
mobileMasked: string;
|
||||
regionCode: string;
|
||||
availabilityNote: string;
|
||||
skillCodes: VolunteerSkillCode[];
|
||||
skillLabels: string[];
|
||||
interestCategories: ServiceCategory[];
|
||||
status: VolunteerStatus;
|
||||
statusLabel: string;
|
||||
revision: number;
|
||||
totalServiceMinutes: number;
|
||||
completedActivityCount: number;
|
||||
history: VolunteerStatusHistoryItem[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface VolunteerProfileResponse {
|
||||
volunteer: VolunteerProfile | null;
|
||||
idempotentReplay?: boolean;
|
||||
}
|
||||
|
||||
export interface AdminVolunteerListItem {
|
||||
publicId: string;
|
||||
realName: string;
|
||||
mobileMasked: string;
|
||||
regionCode: string;
|
||||
skillLabels: string[];
|
||||
interestCategories: ServiceCategory[];
|
||||
status: VolunteerStatus;
|
||||
statusLabel: string;
|
||||
totalServiceMinutes: number;
|
||||
completedActivityCount: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface PaginatedVolunteersResponse {
|
||||
items: AdminVolunteerListItem[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface AdminVolunteerDetailResponse {
|
||||
volunteer: VolunteerProfile;
|
||||
}
|
||||
|
||||
export interface ReviewVolunteerRequest {
|
||||
clientRequestId: string;
|
||||
targetStatus: Extract<VolunteerStatus, 'APPROVED' | 'REJECTED' | 'SUSPENDED'>;
|
||||
note: string;
|
||||
}
|
||||
|
||||
export interface RestoreVolunteerRequest {
|
||||
clientRequestId: string;
|
||||
targetStatus: 'APPROVED';
|
||||
note: string;
|
||||
}
|
||||
|
||||
export interface CreateProjectRequest {
|
||||
clientRequestId: string;
|
||||
name: string;
|
||||
category: ServiceCategory;
|
||||
summary: string;
|
||||
content: string;
|
||||
coverFilePublicId?: string;
|
||||
managerUserPublicId: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
targetPeople?: number;
|
||||
isPublic: boolean;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
export interface UpdateProjectRequest extends Omit<CreateProjectRequest, 'clientRequestId'> {
|
||||
expectedRevision: number;
|
||||
}
|
||||
|
||||
export interface ProjectStatusHistoryItem {
|
||||
status: ProjectStatus;
|
||||
statusLabel: string;
|
||||
reasonLabel: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ProjectListItem {
|
||||
publicId: string;
|
||||
projectNo: string;
|
||||
name: string;
|
||||
category: ServiceCategory;
|
||||
summary: string;
|
||||
coverImageUrl: string | null;
|
||||
managerUserPublicId: string;
|
||||
managerDisplayName: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
targetPeople: number | null;
|
||||
status: ProjectStatus;
|
||||
statusLabel: string;
|
||||
isPublic: boolean;
|
||||
sortOrder: number;
|
||||
revision: number;
|
||||
activityCount: number;
|
||||
serviceCaseCount: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ProjectDetail extends ProjectListItem {
|
||||
content: string;
|
||||
coverFilePublicId: string | null;
|
||||
history: ProjectStatusHistoryItem[];
|
||||
}
|
||||
|
||||
export interface ProjectDetailResponse {
|
||||
project: ProjectDetail;
|
||||
idempotentReplay?: boolean;
|
||||
}
|
||||
|
||||
export interface PaginatedProjectsResponse {
|
||||
items: ProjectListItem[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface TransitionProjectRequest {
|
||||
clientRequestId: string;
|
||||
targetStatus: Extract<ProjectStatus, 'PUBLISHED' | 'COMPLETED'>;
|
||||
}
|
||||
|
||||
export interface ReplaceProjectCasesRequest {
|
||||
serviceCasePublicIds: string[];
|
||||
}
|
||||
|
||||
export interface CreateActivityRequest {
|
||||
clientRequestId: string;
|
||||
projectPublicId?: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
content: string;
|
||||
coverFilePublicId?: string;
|
||||
startAt: string;
|
||||
endAt: string;
|
||||
registrationDeadline: string;
|
||||
locationText: string;
|
||||
capacity?: number;
|
||||
}
|
||||
|
||||
export interface UpdateActivityRequest extends Omit<CreateActivityRequest, 'clientRequestId'> {
|
||||
expectedRevision: number;
|
||||
}
|
||||
|
||||
export interface ActivityStatusHistoryItem {
|
||||
status: ActivityStatus;
|
||||
statusLabel: string;
|
||||
reasonLabel: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ActivityListItem {
|
||||
publicId: string;
|
||||
activityNo: string;
|
||||
projectPublicId: string | null;
|
||||
projectName: string | null;
|
||||
title: string;
|
||||
summary: string;
|
||||
coverImageUrl: string | null;
|
||||
startAt: string;
|
||||
endAt: string;
|
||||
registrationDeadline: string;
|
||||
locationText: string;
|
||||
capacity: number | null;
|
||||
activeRegistrationCount: number;
|
||||
remainingCapacity: number | null;
|
||||
status: ActivityStatus;
|
||||
statusLabel: string;
|
||||
canRegister: boolean;
|
||||
revision: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ActivityDetail extends ActivityListItem {
|
||||
content: string;
|
||||
coverFilePublicId: string | null;
|
||||
history: ActivityStatusHistoryItem[];
|
||||
}
|
||||
|
||||
export interface ActivityDetailResponse {
|
||||
activity: ActivityDetail;
|
||||
currentUserRegistration: ActivityRegistrationItem | null;
|
||||
idempotentReplay?: boolean;
|
||||
}
|
||||
|
||||
export interface PaginatedActivitiesResponse {
|
||||
items: ActivityListItem[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface TransitionActivityRequest {
|
||||
clientRequestId: string;
|
||||
targetStatus: Exclude<ActivityStatus, 'DRAFT'>;
|
||||
}
|
||||
|
||||
export interface RegisterActivityRequest {
|
||||
clientRequestId: string;
|
||||
}
|
||||
|
||||
export interface ActivityRegistrationItem {
|
||||
publicId: string;
|
||||
activityPublicId: string;
|
||||
activityTitle: string;
|
||||
volunteerPublicId: string;
|
||||
volunteerName: string;
|
||||
mobileMasked: string;
|
||||
status: ActivityRegistrationStatus;
|
||||
statusLabel: string;
|
||||
checkInStatus: ActivityCheckInStatus;
|
||||
checkInStatusLabel: string;
|
||||
serviceMinutes: number | null;
|
||||
registeredAt: string;
|
||||
}
|
||||
|
||||
export interface ActivityRegistrationResponse {
|
||||
registration: ActivityRegistrationItem;
|
||||
idempotentReplay: boolean;
|
||||
}
|
||||
|
||||
export interface PaginatedActivityRegistrationsResponse {
|
||||
items: ActivityRegistrationItem[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ReviewActivityRegistrationRequest {
|
||||
clientRequestId: string;
|
||||
targetStatus: Extract<ActivityRegistrationStatus, 'APPROVED' | 'REJECTED'>;
|
||||
note: string;
|
||||
}
|
||||
|
||||
export interface CheckInActivityRegistrationRequest {
|
||||
clientRequestId: string;
|
||||
targetStatus: Extract<ActivityCheckInStatus, 'CHECKED_IN' | 'ABSENT'>;
|
||||
}
|
||||
|
||||
export interface RecordServiceMinutesRequest {
|
||||
clientRequestId: string;
|
||||
serviceMinutes: number;
|
||||
}
|
||||
|
||||
export interface CreateMediaUploadIntentRequest {
|
||||
clientRequestId: string;
|
||||
businessType: 'PROJECT_COVER' | 'ACTIVITY_COVER';
|
||||
filename: string;
|
||||
mimeType: 'image/jpeg' | 'image/png' | 'image/webp';
|
||||
size: number;
|
||||
sha256?: string;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
APPLICATION_APPLY_MODE_LABELS,
|
||||
SERVICE_APPLICATION_STATUS_LABELS,
|
||||
SERVICE_CATEGORIES,
|
||||
SERVICE_CATEGORY_LABELS,
|
||||
} from './index.js';
|
||||
|
||||
describe('service application contracts', () => {
|
||||
it('keeps the six approved V1 service categories stable', () => {
|
||||
expect(SERVICE_CATEGORIES).toHaveLength(6);
|
||||
expect(SERVICE_CATEGORY_LABELS.OLD_AGE).toBe('关爱老人');
|
||||
expect(SERVICE_CATEGORY_LABELS.CHILD).toBe('儿童关爱');
|
||||
});
|
||||
|
||||
it('provides user-facing relationship and status labels', () => {
|
||||
expect(APPLICATION_APPLY_MODE_LABELS.FAMILY).toBe('帮家人申请');
|
||||
expect(SERVICE_APPLICATION_STATUS_LABELS.PENDING_CONTACT).toBe('工作人员正在联系');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
export const SERVICE_CATEGORIES = [
|
||||
'OLD_AGE',
|
||||
'WOMEN',
|
||||
'CHILD',
|
||||
'DISABILITY',
|
||||
'LIFE_DIFFICULTY',
|
||||
'OTHER',
|
||||
] as const;
|
||||
export type ServiceCategory = (typeof SERVICE_CATEGORIES)[number];
|
||||
|
||||
export const APPLICATION_APPLY_MODES = [
|
||||
'SELF',
|
||||
'FAMILY',
|
||||
'COMMUNITY',
|
||||
'VOLUNTEER',
|
||||
'OTHER',
|
||||
] as const;
|
||||
export type ApplicationApplyMode = (typeof APPLICATION_APPLY_MODES)[number];
|
||||
|
||||
export const SERVICE_APPLICATION_STATUSES = [
|
||||
'SUBMITTED',
|
||||
'PENDING_CONTACT',
|
||||
'ASSESSING',
|
||||
'REJECTED',
|
||||
'OUT_OF_SCOPE',
|
||||
'REFERRED',
|
||||
'ACCEPTED',
|
||||
'SERVICE_PLANNED',
|
||||
'IN_SERVICE',
|
||||
'SERVICE_COMPLETED',
|
||||
'FOLLOW_UP_PENDING',
|
||||
'CLOSED',
|
||||
'CANCELLED',
|
||||
'SUSPENDED',
|
||||
] as const;
|
||||
export type ServiceApplicationStatus = (typeof SERVICE_APPLICATION_STATUSES)[number];
|
||||
|
||||
export const SERVICE_CATEGORY_LABELS: Record<ServiceCategory, string> = {
|
||||
OLD_AGE: '关爱老人',
|
||||
WOMEN: '困境妇女',
|
||||
CHILD: '儿童关爱',
|
||||
DISABILITY: '残障帮扶',
|
||||
LIFE_DIFFICULTY: '生活困难',
|
||||
OTHER: '其他服务',
|
||||
};
|
||||
|
||||
export const APPLICATION_APPLY_MODE_LABELS: Record<ApplicationApplyMode, string> = {
|
||||
SELF: '为自己申请',
|
||||
FAMILY: '帮家人申请',
|
||||
COMMUNITY: '社区或机构代办',
|
||||
VOLUNTEER: '志愿者代办',
|
||||
OTHER: '其他代办',
|
||||
};
|
||||
|
||||
export const SERVICE_APPLICATION_STATUS_LABELS: Record<ServiceApplicationStatus, string> = {
|
||||
SUBMITTED: '已提交',
|
||||
PENDING_CONTACT: '工作人员正在联系',
|
||||
ASSESSING: '需求评估中',
|
||||
REJECTED: '暂未通过',
|
||||
OUT_OF_SCOPE: '超出当前服务范围',
|
||||
REFERRED: '已协助转介',
|
||||
ACCEPTED: '已受理',
|
||||
SERVICE_PLANNED: '服务安排中',
|
||||
IN_SERVICE: '服务进行中',
|
||||
SERVICE_COMPLETED: '服务已完成',
|
||||
FOLLOW_UP_PENDING: '等待回访',
|
||||
CLOSED: '服务已结束',
|
||||
CANCELLED: '申请已取消',
|
||||
SUSPENDED: '服务暂缓',
|
||||
};
|
||||
|
||||
export interface ServiceCategoryOption {
|
||||
code: ServiceCategory;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface ServiceCategoriesResponse {
|
||||
items: ServiceCategoryOption[];
|
||||
}
|
||||
|
||||
export interface CreateServiceApplicationRequest {
|
||||
clientRequestId: string;
|
||||
serviceCategory: ServiceCategory;
|
||||
applyMode: ApplicationApplyMode;
|
||||
beneficiaryName: string;
|
||||
contactMobile: string;
|
||||
regionCode: string;
|
||||
description: string;
|
||||
preferredContactTime?: string;
|
||||
needsHomeVisit?: boolean;
|
||||
mobilityDifficulty?: boolean;
|
||||
privacyConsent: true;
|
||||
}
|
||||
|
||||
export interface ServiceApplicationHistoryItem {
|
||||
status: ServiceApplicationStatus;
|
||||
statusLabel: string;
|
||||
reasonLabel: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ServiceApplicationListItem {
|
||||
publicId: string;
|
||||
applicationNo: string;
|
||||
serviceCategory: ServiceCategory;
|
||||
serviceCategoryLabel: string;
|
||||
applyMode: ApplicationApplyMode;
|
||||
applyModeLabel: string;
|
||||
beneficiaryName: string;
|
||||
contactMobileMasked: string;
|
||||
regionCode: string;
|
||||
status: ServiceApplicationStatus;
|
||||
statusLabel: string;
|
||||
submittedAt: string;
|
||||
}
|
||||
|
||||
export interface ServiceApplicationDetail extends ServiceApplicationListItem {
|
||||
contactMobile: string;
|
||||
description: string;
|
||||
preferredContactTime: string | null;
|
||||
needsHomeVisit: boolean | null;
|
||||
mobilityDifficulty: boolean | null;
|
||||
history: ServiceApplicationHistoryItem[];
|
||||
}
|
||||
|
||||
export interface CreateServiceApplicationResponse {
|
||||
application: ServiceApplicationDetail;
|
||||
idempotentReplay: boolean;
|
||||
}
|
||||
|
||||
export interface PaginatedServiceApplicationsResponse {
|
||||
items: ServiceApplicationListItem[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ServiceApplicationDetailResponse {
|
||||
application: ServiceApplicationDetail;
|
||||
}
|
||||
|
||||
export interface AdminServiceApplicationQuery {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
applicationNo?: string;
|
||||
serviceCategory?: ServiceCategory;
|
||||
status?: ServiceApplicationStatus;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { ServiceCategory } from './service-applications.js';
|
||||
export interface StatisticsOverview {
|
||||
period: { startDate: string; endDate: string };
|
||||
applicationCount: number;
|
||||
acceptedCount: number;
|
||||
completedServiceCount: number;
|
||||
servicePersonTimes: number;
|
||||
uniqueBeneficiaryCount: number;
|
||||
volunteerCount: number;
|
||||
activityCount: number;
|
||||
volunteerMinutes: number;
|
||||
byCategory: Array<{ category: ServiceCategory; count: number }>;
|
||||
monthly: Array<{ month: string; applications: number; completedServices: number }>;
|
||||
generatedAt: string;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"sourceMap": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": ["src/**/*.spec.ts"]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"rootDir": "src",
|
||||
"types": ["vitest/globals"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user