feat: 完成 R13/R14/R15/R16 小团队管理核心能力
这个提交包含在:
@@ -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,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";
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({}),
|
||||
});
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户