feat: 完成 R13/R14/R15/R16 小团队管理核心能力

这个提交包含在:
2026-03-21 20:31:34 +08:00
父节点 0826693a8f
当前提交 1e52f1c225
共修改 47 个文件,包含 4298 行新增和 306 行删除
+160
查看文件
@@ -24,6 +24,79 @@ export type EmployeeListResponse = {
items: EmployeeSummary[];
};
export type EmployeeLifecycleEvent = {
id: string;
employeeId: string;
eventType: "onboard" | "regularize" | "transfer" | "offboard";
effectiveDate: string | null;
payloadJson: Record<string, unknown> | null;
createdAt: string | null;
createdBy: string | null;
};
export type EmployeeChecklistTask = {
id: string;
employeeId: string;
scene: "onboarding" | "offboarding" | "transfer";
taskType:
| "account_setup"
| "asset_issue"
| "policy_ack"
| "asset_return"
| "access_revoke"
| "expense_clearance"
| "handover_note";
title: string;
status: "pending" | "completed" | "skipped";
dueDate: string | null;
completedAt: string | null;
completedBy: string | null;
payloadJson: Record<string, unknown> | null;
createdAt: string | null;
};
export type EmployeeProfile = {
employee: {
id: string;
employeeNo: string;
name: string;
phone: string;
email: string;
status: EmployeeStatus;
employmentStatus: "onboarding" | "probation" | "active" | "offboarding" | "inactive";
departmentId: string;
title: string;
directManagerId: string | null;
hireDate: string | null;
probationEndDate: string | null;
lastWorkingDate: string | null;
workLocation: string | null;
emergencyContactName: string | null;
emergencyContactPhone: string | null;
notes: string | null;
archivedAt: string | null;
roleCodes: string[];
department: { id: string; name: string } | null;
directManager: { id: string; employeeNo: string; name: string } | null;
};
lifecycle: {
events: EmployeeLifecycleEvent[];
checklistTasks: EmployeeChecklistTask[];
blockers: {
openAssetCount: number;
pendingWorkflowTaskCount: number;
canCompleteOffboarding: boolean;
};
};
assignedAssets: Array<{
id: string;
assetNo: string;
name: string;
status: string;
location: string | null;
}>;
};
export async function fetchEmployees(params: {
page: number;
pageSize: number;
@@ -39,3 +112,90 @@ export async function fetchEmployees(params: {
return apiRequestData<EmployeeListResponse>(`/api/employees?${search.toString()}`);
}
export async function fetchEmployeeProfile(employeeId: string) {
return apiRequestData<EmployeeProfile>(`/api/employees/${employeeId}/profile`);
}
export async function patchEmployeeProfile(
employeeId: string,
payload: Partial<{
departmentId: string;
directManagerId: string | null;
title: string;
probationEndDate: string | null;
lastWorkingDate: string | null;
workLocation: string | null;
emergencyContactName: string | null;
emergencyContactPhone: string | null;
notes: string | null;
}>,
) {
return apiRequestData<EmployeeProfile>(`/api/employees/${employeeId}/profile`, {
method: "PATCH",
body: JSON.stringify(payload),
});
}
export async function startEmployeeOnboarding(
employeeId: string,
payload: { effectiveDate?: string; probationEndDate?: string | null; notes?: string | null } = {},
) {
return apiRequestData<EmployeeProfile>(`/api/employees/${employeeId}/onboarding/start`, {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function regularizeEmployee(
employeeId: string,
payload: { effectiveDate?: string; notes?: string | null } = {},
) {
return apiRequestData<EmployeeProfile>(`/api/employees/${employeeId}/regularize`, {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function transferEmployee(
employeeId: string,
payload: {
effectiveDate?: string;
departmentId?: string;
directManagerId?: string | null;
title?: string;
notes?: string | null;
} = {},
) {
return apiRequestData<EmployeeProfile>(`/api/employees/${employeeId}/transfer`, {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function startEmployeeOffboarding(
employeeId: string,
payload: { effectiveDate?: string; lastWorkingDate?: string | null; notes?: string | null } = {},
) {
return apiRequestData<EmployeeProfile>(`/api/employees/${employeeId}/offboarding/start`, {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function fetchEmployeeLifecycle(employeeId: string) {
return apiRequestData<EmployeeProfile["lifecycle"]>(`/api/employees/${employeeId}/lifecycle`);
}
export async function completeEmployeeChecklistTask(employeeId: string, taskId: string) {
return apiRequestData<EmployeeProfile["lifecycle"]>(`/api/employees/${employeeId}/checklist-tasks/${taskId}/complete`, {
method: "POST",
body: JSON.stringify({}),
});
}
export async function skipEmployeeChecklistTask(employeeId: string, taskId: string) {
return apiRequestData<EmployeeProfile["lifecycle"]>(`/api/employees/${employeeId}/checklist-tasks/${taskId}/skip`, {
method: "POST",
body: JSON.stringify({}),
});
}
+1 -1
查看文件
@@ -1,6 +1,6 @@
import { apiRequestData } from "./http";
export type FeatureFlagKey = "erp_sync" | "invoice_verification" | "leave_holiday_calendar" | "budget_precheck_v1" | "payment_batch_v1" | "portal_home_v1" | "global_search_v1" | "announcements_v1" | "workflow_definition_admin_v1" | "cutover_console_v1";
export type FeatureFlagKey = "erp_sync" | "invoice_verification" | "leave_holiday_calendar" | "budget_precheck_v1" | "payment_batch_v1" | "portal_home_v1" | "global_search_v1" | "announcements_v1" | "workflow_definition_admin_v1" | "cutover_console_v1" | "presence_board_v1" | "weekly_report_v1" | "employee_lifecycle_v1" | "todo_board_v1";
export type FeatureFlagOverrideMode = "inherit" | "force_on" | "force_off";
export type FeatureFlagRolloutType = "global" | "department_allowlist";
+21
查看文件
@@ -1,6 +1,11 @@
import { apiRequestData } from "./http";
export type PortalHomePayload = {
workspace: {
id: string | null;
persisted: boolean;
profile: "small_team_internal";
};
greeting: { name: string };
cards: Array<{ key: string; title: string; value: string | number; href: string; severity?: "normal" | "warning" | "high" }>;
pendingTasks: { count: number; href: string };
@@ -8,6 +13,22 @@ export type PortalHomePayload = {
riskSummary: { openCount: number; href: string } | null;
announcements: Array<{ id: string; title: string; summary: string; href: string | null }>;
quickLinks: Array<{ key: string; title: string; href: string }>;
todayPresence: { date: string; status: string | null; label: string; note: string | null; href: string } | null;
teamPresence:
| ({ date: string; href: string } & {
office: number;
remote: number;
out: number;
travel: number;
leave: number;
unspecified: number;
total: number;
})
| null;
weeklyReport: { weekStart: string; status: "draft" | "submitted"; needsHelp: boolean; href: string } | null;
weeklySummary: { weekStart: string; submittedCount: number; missingCount: number; needsHelpCount: number; href: string } | null;
todoSummary: { openCount: number; highPriorityCount: number; href: string } | null;
peopleLifecycle: { onboardingCount: number; offboardingCount: number; assetReturnPendingCount: number; href: string } | null;
};
export async function fetchPortalHome() {
+67
查看文件
@@ -0,0 +1,67 @@
import { apiRequestData } from "./http";
export type PresenceStatus = "office" | "remote" | "out" | "travel" | "leave";
export type PresenceTimeRange = "full_day" | "morning" | "afternoon";
export type PresenceSource = "manual" | "leave_sync";
export type PresenceItem = {
id: string;
employeeId: string;
date: string;
status: PresenceStatus;
timeRange: PresenceTimeRange;
note: string | null;
source: PresenceSource;
updatedAt: string | null;
};
export type PresenceEmployee = {
id: string;
employeeNo: string;
name: string;
title: string;
employmentStatus: string;
department: { id: string; name: string } | null;
};
export type TeamPresenceItem = {
employee: PresenceEmployee;
presence: PresenceItem | null;
};
export type PresenceSummary = {
office: number;
remote: number;
out: number;
travel: number;
leave: number;
unspecified: number;
total: number;
};
export async function fetchMyPresence(date?: string) {
const suffix = date ? `?date=${encodeURIComponent(date)}` : "";
return apiRequestData<{ item: PresenceItem | null; date: string }>(`/api/presence/me${suffix}`);
}
export async function upsertMyPresence(payload: {
date?: string;
status: Extract<PresenceStatus, "office" | "remote" | "out" | "travel">;
timeRange?: PresenceTimeRange;
note?: string | null;
}) {
return apiRequestData<{ item: PresenceItem }>("/api/presence/me", {
method: "PUT",
body: JSON.stringify(payload),
});
}
export async function fetchTeamPresence(date?: string) {
const suffix = date ? `?date=${encodeURIComponent(date)}` : "";
return apiRequestData<{ date: string; summary: PresenceSummary; items: TeamPresenceItem[] }>(`/api/presence/team${suffix}`);
}
export async function fetchPresenceSummary(date?: string) {
const suffix = date ? `?date=${encodeURIComponent(date)}` : "";
return apiRequestData<{ date: string; summary: PresenceSummary }>(`/api/presence/summary${suffix}`);
}
+10 -1
查看文件
@@ -1,7 +1,16 @@
import { apiRequestData } from "./http";
export type GlobalSearchHit = {
entityType: "reimbursement" | "leave" | "purchase_request" | "travel_request" | "asset";
entityType:
| "employee"
| "announcement"
| "reimbursement"
| "leave"
| "purchase_request"
| "travel_request"
| "asset"
| "weekly_report"
| "todo";
entityId: string;
title: string;
subtitle: string | null;
+79
查看文件
@@ -0,0 +1,79 @@
import { apiRequestData } from "./http";
export type TodoPriority = "low" | "normal" | "high";
export type TodoStatus = "todo" | "doing" | "done" | "cancelled";
export type TodoSource = "manual" | "weekly_report";
export type TodoItem = {
id: string;
title: string;
description: string | null;
assigneeEmployeeId: string;
createdBy: string;
dueDate: string | null;
priority: TodoPriority;
status: TodoStatus;
source: TodoSource;
createdAt: string | null;
updatedAt: string | null;
completedAt: string | null;
assignee: { id: string; employeeNo: string; name: string } | null;
creator: { id: string; employeeNo: string; name: string } | null;
};
export async function fetchTodos(params: {
status?: TodoStatus;
scope?: "mine" | "team";
assigneeId?: string;
q?: string;
} = {}) {
const search = new URLSearchParams();
if (params.status) search.set("status", params.status);
if (params.scope) search.set("scope", params.scope);
if (params.assigneeId) search.set("assigneeId", params.assigneeId);
if (params.q) search.set("q", params.q);
const suffix = search.toString() ? `?${search.toString()}` : "";
return apiRequestData<{ items: TodoItem[] }>(`/api/todos${suffix}`);
}
export async function createTodo(payload: {
title: string;
description?: string | null;
assigneeEmployeeId?: string;
dueDate?: string | null;
priority?: TodoPriority;
source?: TodoSource;
}) {
return apiRequestData<{ item: TodoItem }>("/api/todos", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function patchTodo(id: string, payload: Partial<{
title: string;
description: string | null;
assigneeEmployeeId: string;
dueDate: string | null;
priority: TodoPriority;
status: TodoStatus;
}>) {
return apiRequestData<{ item: TodoItem }>(`/api/todos/${id}`, {
method: "PATCH",
body: JSON.stringify(payload),
});
}
export async function completeTodo(id: string) {
return apiRequestData<{ item: TodoItem }>(`/api/todos/${id}/complete`, {
method: "POST",
body: JSON.stringify({}),
});
}
export async function cancelTodo(id: string) {
return apiRequestData<{ item: TodoItem }>(`/api/todos/${id}/cancel`, {
method: "POST",
body: JSON.stringify({}),
});
}
+68
查看文件
@@ -0,0 +1,68 @@
import { apiRequestData } from "./http";
export type WeeklyReport = {
id: string;
employeeId: string;
weekStart: string;
status: "draft" | "submitted";
highlightsText: string;
blockersText: string;
nextWeekText: string;
needsHelp: boolean;
submittedAt: string | null;
createdAt: string | null;
updatedAt: string | null;
};
export type WeeklyReportTeamItem = {
employee: {
id: string;
employeeNo: string;
name: string;
title: string;
department: { id: string; name: string } | null;
};
report: WeeklyReport | null;
};
export type WeeklyReportTeamSummary = {
totalEmployees: number;
submittedCount: number;
draftCount: number;
missingCount: number;
needsHelpCount: number;
};
export async function fetchMyWeeklyReport(weekStart?: string) {
const suffix = weekStart ? `?weekStart=${encodeURIComponent(weekStart)}` : "";
return apiRequestData<{ item: WeeklyReport }>(`/api/weekly-reports/me${suffix}`);
}
export async function updateMyWeeklyReport(payload: {
weekStart?: string;
highlightsText?: string;
blockersText?: string;
nextWeekText?: string;
needsHelp?: boolean;
}) {
return apiRequestData<{ item: WeeklyReport }>("/api/weekly-reports/me", {
method: "PUT",
body: JSON.stringify(payload),
});
}
export async function submitMyWeeklyReport(payload: { weekStart?: string } = {}) {
return apiRequestData<{ item: WeeklyReport }>("/api/weekly-reports/me/submit", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function fetchTeamWeeklyReports(weekStart?: string) {
const suffix = weekStart ? `?weekStart=${encodeURIComponent(weekStart)}` : "";
return apiRequestData<{
weekStart: string;
summary: WeeklyReportTeamSummary;
items: WeeklyReportTeamItem[];
}>(`/api/weekly-reports/team${suffix}`);
}
+62
查看文件
@@ -0,0 +1,62 @@
import { apiRequestData } from "./http";
export type WorkspaceModuleVisibilityMode = "main" | "admin" | "hidden";
export type WorkspaceSettings = {
id: string | null;
persisted: boolean;
profile: "small_team_internal";
orgMode: "single_company";
primaryEntryMode: "password";
notificationStrategy: "in_app";
devicePriority: "desktop";
moduleVisibility: Record<string, WorkspaceModuleVisibilityMode>;
createdAt: string | null;
updatedAt: string | null;
updatedBy: string | null;
defaults?: Record<string, WorkspaceModuleVisibilityMode>;
};
export type NavigationItem = {
key: string;
label: string;
icon: string;
href: string;
placement: WorkspaceModuleVisibilityMode;
permissionCodes: string[];
featureFlagKey: string | null;
};
export type RolePreset = {
key: string;
label: string;
roleCodes: string[];
description: string;
};
export type NavigationPayload = {
workspace: WorkspaceSettings;
sections: {
main: NavigationItem[];
admin: NavigationItem[];
backup: NavigationItem[];
};
recommendedRolePresets: RolePreset[];
};
export async function fetchWorkspaceSettings() {
return apiRequestData<WorkspaceSettings>("/api/system/workspace");
}
export async function updateWorkspaceSettings(payload: {
moduleVisibility: Record<string, WorkspaceModuleVisibilityMode>;
}) {
return apiRequestData<{ item: WorkspaceSettings }>("/api/system/workspace", {
method: "PATCH",
body: JSON.stringify(payload),
});
}
export async function fetchSystemNavigation() {
return apiRequestData<NavigationPayload>("/api/system/navigation");
}