diff --git a/src/components/custom/subordinate-selector.vue b/src/components/custom/subordinate-selector.vue new file mode 100644 index 0000000..a3cf2a9 --- /dev/null +++ b/src/components/custom/subordinate-selector.vue @@ -0,0 +1,108 @@ + + + + + + + 团队成员 + {{ props.data.subordinateCount }} + + + + + + + + {{ renderNodeLabel(node) }} + + + + + + + diff --git a/src/components/custom/team-context-panel.vue b/src/components/custom/team-context-panel.vue new file mode 100644 index 0000000..ddc16ab --- /dev/null +++ b/src/components/custom/team-context-panel.vue @@ -0,0 +1,163 @@ + + + + + + + + + + + + + 当前范围 + + {{ props.selectedLabel || (mode === 'self' ? '我自己' : '--') }} + + + + + 下属人数 + {{ props.subordinateCount }} + + + + {{ contextText }} + + + + + + + + + diff --git a/src/service/api/overtime-application.ts b/src/service/api/overtime-application.ts index 7cc8efe..6e7f189 100644 --- a/src/service/api/overtime-application.ts +++ b/src/service/api/overtime-application.ts @@ -34,6 +34,8 @@ type OvertimeApplicationApprovalRecordResponse = Omit< auditorUserId: StringIdResponse; }; +type TeamOvertimeSummaryResponse = Api.OvertimeApplication.TeamOvertimeSummary; + function normalizeBooleanFlag(value: boolean | number | string | null | undefined) { if (typeof value === 'boolean') { return value; @@ -94,6 +96,18 @@ function createPageQuery(params: Api.OvertimeApplication.OvertimeApplicationSear query.append('pageNo', String(params.pageNo ?? 1)); query.append('pageSize', String(params.pageSize ?? 10)); + if (params.applicantIds !== null && params.applicantIds !== undefined) { + if (params.applicantIds.length) { + params.applicantIds.forEach(item => { + if (item) { + query.append('applicantIds', item); + } + }); + } else { + query.append('applicantIds', ''); + } + } + if (params.keyword) { query.append('keyword', params.keyword); } @@ -287,6 +301,17 @@ export async function fetchGetOvertimeApplicationStatusDict() { ); } +export async function fetchGetTeamOvertimeSummary(params: Api.OvertimeApplication.TeamOvertimeSummaryParams = {}) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${OVERTIME_APPLICATION_PREFIX}/team/summary`, + method: 'get', + params + }); + + return mapServiceResult(result as ServiceRequestResult, data => data); +} + export function fetchExportOvertimeApplications(params: Api.OvertimeApplication.OvertimeApplicationSearchParams = {}) { const query = createPageQuery(params); diff --git a/src/service/api/system-manage.ts b/src/service/api/system-manage.ts index 53ab872..615421d 100644 --- a/src/service/api/system-manage.ts +++ b/src/service/api/system-manage.ts @@ -118,6 +118,11 @@ type UserManagementRelationTreeResponse = Omit< children?: UserManagementRelationTreeResponse[] | null; }; +type MySubordinateTreeNodeResponse = Omit & { + userId: string | number; + children?: MySubordinateTreeNodeResponse[] | null; +}; + function normalizeUserSimple(user: UserSimpleResponse): Api.SystemManage.UserSimple { return { ...user, @@ -181,6 +186,14 @@ function normalizeUserManagementRelationTree( }; } +function normalizeMySubordinateTreeNode(node: MySubordinateTreeNodeResponse): Api.SystemManage.MySubordinateTreeNode { + return { + ...node, + userId: normalizeStringId(node.userId), + children: node.children?.map(normalizeMySubordinateTreeNode) ?? null + }; +} + /** 获取角色分页 */ export async function fetchGetRolePage(params?: Api.SystemManage.RoleSearchParams) { const query = createRolePageQuery(params); @@ -712,6 +725,17 @@ export async function fetchGetUserManagementRelationQuery(query: UserManagementR ); } +/** 获取当前登录用户下属树 */ +export async function fetchGetMySubordinateTree() { + return request({ + ...safeJsonRequestConfig, + url: `${USER_MANAGEMENT_RELATION_PREFIX}/my-subordinate-tree`, + method: 'get' + }).then(result => + mapServiceResult(result as ServiceRequestResult, normalizeMySubordinateTreeNode) + ); +} + /** * 获取用户管理链路详情 * diff --git a/src/service/api/work-report.ts b/src/service/api/work-report.ts index 10cd504..923e594 100644 --- a/src/service/api/work-report.ts +++ b/src/service/api/work-report.ts @@ -99,6 +99,14 @@ type ProjectOptionResponse = Omit & { + userId: StringIdResponse; +}; + +type TeamReportSummaryResponse = Omit & { + unsubmittedUsers?: TeamReportPendingUserResponse[] | null; +}; + function normalizeBooleanFlag(value: boolean | number | string | null | undefined) { if (typeof value === 'boolean') return value; if (typeof value === 'number') return value === 1; @@ -173,6 +181,21 @@ function appendArray(query: URLSearchParams, key: string, values?: Array appendValue(query, key, value)); } +function appendNullableArrayFlag( + query: URLSearchParams, + key: string, + values?: Array | null +) { + if (values === null || values === undefined) return; + + if (!values.length) { + query.append(key, ''); + return; + } + + appendArray(query, key, values); +} + function createBasePageQuery(params: Api.WorkReport.Common.WorkReportBaseSearchParams = {}) { const query = new URLSearchParams(); @@ -189,16 +212,20 @@ function createBasePageQuery(params: Api.WorkReport.Common.WorkReportBaseSearchP function createWeeklyPageQuery(params: Api.WorkReport.Weekly.WeeklyReportSearchParams = {}) { const query = createBasePageQuery(params); + appendNullableArrayFlag(query, 'reporterIds', params.reporterIds); appendValue(query, 'isBusinessTrip', params.isBusinessTrip); return query.toString(); } function createMonthlyPageQuery(params: Api.WorkReport.Monthly.MonthlyReportSearchParams = {}) { - return createBasePageQuery(params).toString(); + const query = createBasePageQuery(params); + appendNullableArrayFlag(query, 'reporterIds', params.reporterIds); + return query.toString(); } function createProjectPageQuery(params: Api.WorkReport.Project.ProjectReportSearchParams = {}) { const query = createBasePageQuery(params); + appendNullableArrayFlag(query, 'projectOwnerIds', params.projectOwnerIds); appendValue(query, 'projectId', params.projectId); appendValue(query, 'flag', params.flag); return query.toString(); @@ -338,6 +365,17 @@ function normalizeProjectOption( }; } +function normalizeTeamReportSummary(response: TeamReportSummaryResponse): Api.WorkReport.Common.TeamReportSummary { + return { + ...response, + unsubmittedUsers: + response.unsubmittedUsers?.map(item => ({ + ...item, + userId: normalizeStringId(item.userId) + })) ?? [] + }; +} + function mapPage(data: PageResponse, mapper: (item: TInput) => TOutput) { return { total: normalizeTotal(data.total), @@ -440,6 +478,34 @@ export async function fetchGetWorkReportStatusDict() { return mapServiceResult(result as ServiceRequestResult, data => data); } +export async function fetchGetTeamReportSummary(params: Api.WorkReport.Common.TeamReportSummaryParams) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${WORK_REPORT_PREFIX}/team/summary`, + method: 'get', + params + }); + + return mapServiceResult(result as ServiceRequestResult, normalizeTeamReportSummary); +} + +export async function fetchRemindTeamReport(data: Api.WorkReport.Common.TeamReportRemindParams) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${WORK_REPORT_PREFIX}/team/remind`, + method: 'post', + data: { + ...data, + userIds: data.userIds && data.userIds.length ? data.userIds : undefined + } + }); + + return mapServiceResult( + result as ServiceRequestResult, + payload => payload + ); +} + export async function fetchGetWeeklyReportPage(params: Api.WorkReport.Weekly.WeeklyReportSearchParams = {}) { const query = createWeeklyPageQuery(params); const result = await request>({ diff --git a/src/typings/api/overtime-application.d.ts b/src/typings/api/overtime-application.d.ts index 0231680..82f6d00 100644 --- a/src/typings/api/overtime-application.d.ts +++ b/src/typings/api/overtime-application.d.ts @@ -32,6 +32,7 @@ declare namespace Api { type OvertimeApplicationSearchParams = CommonType.RecordNullable< Pick & { + applicantIds: string[] | null; keyword: string; applicantName: string; approverId: string; @@ -95,5 +96,17 @@ declare namespace Api { terminalFlag: boolean; allowEdit: boolean; } + + interface TeamOvertimeSummaryParams { + month?: string | null; + } + + interface TeamOvertimeSummary { + month: string; + totalApplicationCount: number; + pendingCount: number; + approvedCount: number; + rejectedCount: number; + } } } diff --git a/src/typings/api/system-manage.d.ts b/src/typings/api/system-manage.d.ts index 92a57dc..94d2af4 100644 --- a/src/typings/api/system-manage.d.ts +++ b/src/typings/api/system-manage.d.ts @@ -386,6 +386,24 @@ declare namespace Api { children?: UserManagementRelationTreeRespVO[] | null; } + /** + * 当前登录用户的下属树 + * + * 用于团队视角选择器;根节点代表“全部下属范围” + */ + interface MySubordinateTreeNode { + /** 用户 ID */ + userId: string; + /** 用户昵称 */ + userNickname: string; + /** 是否为当前登录用户根节点 */ + isRoot: boolean; + /** 全链路下属人数 */ + subordinateCount: number; + /** 下级用户列表 */ + children?: MySubordinateTreeNode[] | null; + } + /** * 用户管理链路保存参数 * diff --git a/src/typings/api/work-report.d.ts b/src/typings/api/work-report.d.ts index 72d448a..742c1b2 100644 --- a/src/typings/api/work-report.d.ts +++ b/src/typings/api/work-report.d.ts @@ -71,6 +71,34 @@ declare namespace Api { total: number; list: T[]; } + + interface TeamReportPendingUser { + userId: string; + userNickname: string; + } + + interface TeamReportSummary { + totalShouldSubmit: number; + submittedCount: number; + unsubmittedCount: number; + pendingApprovalCount: number; + unsubmittedUsers: TeamReportPendingUser[]; + } + + interface TeamReportSummaryParams { + reportType: ReportType; + periodKey: string; + } + + interface TeamReportRemindParams { + reportType: ReportType; + periodKey: string; + userIds?: string[] | null; + } + + interface TeamReportRemindResult { + remindedCount: number; + } } namespace Weekly { @@ -114,6 +142,7 @@ declare namespace Api { } type WeeklyReportSearchParams = Common.WorkReportBaseSearchParams & { + reporterIds?: string[] | null; isBusinessTrip?: boolean | string | null; }; @@ -164,7 +193,9 @@ declare namespace Api { planItems: Common.PersonalReportPlanItem[]; } - type MonthlyReportSearchParams = Common.WorkReportBaseSearchParams; + type MonthlyReportSearchParams = Common.WorkReportBaseSearchParams & { + reporterIds?: string[] | null; + }; interface MonthlyReportSaveParams { periodKey: string; @@ -266,6 +297,7 @@ declare namespace Api { } type ProjectReportSearchParams = Common.WorkReportBaseSearchParams & { + projectOwnerIds?: string[] | null; projectId?: string | null; flag?: number | null; }; diff --git a/src/typings/components.d.ts b/src/typings/components.d.ts index 6c5f986..277cdae 100644 --- a/src/typings/components.d.ts +++ b/src/typings/components.d.ts @@ -178,12 +178,14 @@ declare module 'vue' { RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] SoybeanAvatar: typeof import('./../components/custom/soybean-avatar.vue')['default'] + SubordinateSelector: typeof import('./../components/custom/subordinate-selector.vue')['default'] SvgIcon: typeof import('./../components/custom/svg-icon.vue')['default'] SystemLogo: typeof import('./../components/common/system-logo.vue')['default'] TableColumnSetting: typeof import('./../components/advanced/table-column-setting.vue')['default'] TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default'] TableSearchFields: typeof import('./../components/custom/table-search-fields.vue')['default'] TableSearchPanel: typeof import('./../components/custom/table-search-panel.vue')['default'] + TeamContextPanel: typeof import('./../components/custom/team-context-panel.vue')['default'] ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default'] UserPickerTrigger: typeof import('./../components/custom/business-user-picker/components/user-picker-trigger.vue')['default'] WaveBg: typeof import('./../components/custom/wave-bg.vue')['default'] diff --git a/src/views/personal-center/overtime-application/index.vue b/src/views/personal-center/overtime-application/index.vue index 34282f6..05dd58a 100644 --- a/src/views/personal-center/overtime-application/index.vue +++ b/src/views/personal-center/overtime-application/index.vue @@ -1,10 +1,24 @@ - - - - - - - - 加班申请 - {{ totalCount }} - - - - - - - - 导出 - - - - - - 新增 - - - + + + + + 本月申请单数 + {{ teamSummary?.totalApplicationCount ?? 0 }} + + + 本月待审批 + {{ teamSummary?.pendingCount ?? 0 }} + + + 本月已通过 + {{ teamSummary?.approvedCount ?? 0 }} + + + 本月已退回 + {{ teamSummary?.rejectedCount ?? 0 }} - - - - - - - - + - - + + - + + + + + + + + + 加班申请 + {{ totalCount }} + + + + + + + + 导出 + + + + + + 新增 + + + + + + + + + + + + + + + + + + + + diff --git a/src/views/personal-center/shared/team-dashboard.ts b/src/views/personal-center/shared/team-dashboard.ts new file mode 100644 index 0000000..a64a184 --- /dev/null +++ b/src/views/personal-center/shared/team-dashboard.ts @@ -0,0 +1,70 @@ +export type TeamViewMode = 'self' | 'team'; + +export interface TeamSelectableUser { + userId: string; + userNickname: string; +} + +export interface TeamSelectionState { + mode: TeamViewMode; + selectedUserId: string | null; + selectedUserIds: string[] | null; + isRootSelected: boolean; +} + +export interface TeamViewContext extends TeamSelectionState { + allSubordinateUserIds: string[]; + selectedLabel: string; +} + +export function resolveTeamQueryUserIds(context: TeamViewContext | null | undefined): string[] | null { + if (!context || context.mode !== 'team') { + return null; + } + + if (context.isRootSelected) { + return [...context.allSubordinateUserIds]; + } + + return context.selectedUserIds ? [...context.selectedUserIds] : []; +} + +export function collectSubordinateUserIds(root: Api.SystemManage.MySubordinateTreeNode | null | undefined): string[] { + if (!root) return []; + + const ids: string[] = []; + + const walk = (nodes?: Api.SystemManage.MySubordinateTreeNode[] | null) => { + nodes?.forEach(node => { + ids.push(node.userId); + walk(node.children ?? null); + }); + }; + + walk(root.children ?? null); + return ids; +} + +export function findSubordinateNode( + root: Api.SystemManage.MySubordinateTreeNode | null | undefined, + userId: string | null +): Api.SystemManage.MySubordinateTreeNode | null { + if (!root || !userId) return null; + + if (root.userId === userId) { + return root; + } + + const stack = [...(root.children ?? [])]; + while (stack.length) { + const current = stack.shift()!; + if (current.userId === userId) { + return current; + } + if (current.children?.length) { + stack.push(...current.children); + } + } + + return null; +} diff --git a/src/views/personal-center/work-report/index.vue b/src/views/personal-center/work-report/index.vue index 5618308..3286a12 100644 --- a/src/views/personal-center/work-report/index.vue +++ b/src/views/personal-center/work-report/index.vue @@ -1,16 +1,24 @@ @@ -274,11 +353,21 @@ defineExpose({ reload }); + + - {{ WORK_REPORT_TYPE_LABEL.monthly }} + {{ reportTitle }} {{ table.mobilePagination.value.total || 0 }} @@ -304,7 +393,13 @@ defineExpose({ reload }); - + diff --git a/src/views/personal-center/work-report/project/index.vue b/src/views/personal-center/work-report/project/index.vue index f033913..f6c2ca1 100644 --- a/src/views/personal-center/work-report/project/index.vue +++ b/src/views/personal-center/work-report/project/index.vue @@ -1,18 +1,19 @@ @@ -292,11 +369,21 @@ defineExpose({ reload }); @search="handleSearch" /> + + - {{ WORK_REPORT_TYPE_LABEL.project }} + {{ reportTitle }} {{ table.mobilePagination.value.total || 0 }} @@ -322,7 +409,13 @@ defineExpose({ reload }); - + diff --git a/src/views/personal-center/work-report/shared/components/team-report-summary.vue b/src/views/personal-center/work-report/shared/components/team-report-summary.vue new file mode 100644 index 0000000..7e1b3ee --- /dev/null +++ b/src/views/personal-center/work-report/shared/components/team-report-summary.vue @@ -0,0 +1,217 @@ + + + + + {{ props.periodLabel }} + + + + {{ card.label }} + + + + + {{ card.value }} + + + + 未提交人员 + + + {{ user.userNickname }} + + 催办 + + + + + + + + + + + {{ card.value }} + + + + + + + + diff --git a/src/views/personal-center/work-report/shared/types.ts b/src/views/personal-center/work-report/shared/types.ts index 950d667..cbd4c18 100644 --- a/src/views/personal-center/work-report/shared/types.ts +++ b/src/views/personal-center/work-report/shared/types.ts @@ -41,6 +41,16 @@ export const WORK_REPORT_TYPE_LABEL: Record = { project: '项目半月报' }; +export const TEAM_WORK_REPORT_TYPE_LABEL: Record = { + weekly: '团队周报', + monthly: '团队月报', + project: '团队项目半月报' +}; + +export function getWorkReportTypeDisplayLabel(reportType: WorkReportType, isTeamMode = false) { + return isTeamMode ? TEAM_WORK_REPORT_TYPE_LABEL[reportType] : WORK_REPORT_TYPE_LABEL[reportType]; +} + export const WORK_REPORT_STATUS_LABEL: Record = { draft: '待提交', pending_approval: '待审批', diff --git a/src/views/personal-center/work-report/shared/utils.ts b/src/views/personal-center/work-report/shared/utils.ts index c9c610f..29421c4 100644 --- a/src/views/personal-center/work-report/shared/utils.ts +++ b/src/views/personal-center/work-report/shared/utils.ts @@ -20,6 +20,14 @@ export interface WorkReportPeriodOption { }; } +export interface WorkReportResolvedPeriod { + periodKey: string; + periodLabel: string; + periodStartDate: string; + periodEndDate: string; + flag?: number; +} + function formatRangeLabel(start: dayjs.Dayjs, end: dayjs.Dayjs) { return `${start.format('YYYY-MM-DD')} 至 ${end.format('YYYY-MM-DD')}`; } @@ -192,3 +200,72 @@ export function getReportTypePeriodOptions(now = dayjs()) { project: getProjectPeriodOptions(now) } as const; } + +type PeriodRange = string[] | null | undefined; + +interface ResolveWorkReportSummaryPeriodOptions { + currentRow?: Partial | null; + periodRange?: PeriodRange; + flag?: number | null; +} + +function getRangeReferenceDate(periodRange?: PeriodRange) { + const [, endDate] = periodRange || []; + return endDate || periodRange?.[0] || ''; +} + +function inferProjectSummaryFlag(referenceDate: string, explicitFlag?: number | null) { + if (explicitFlag === 1 || explicitFlag === 2) { + return explicitFlag; + } + + const selectedDate = dayjs(referenceDate); + if (selectedDate.isValid()) { + return selectedDate.date() > 15 ? 2 : 1; + } + + return getProjectPeriodOptions()[0]?.flag || 1; +} + +export function resolveWorkReportSummaryPeriod( + reportType: WorkReportType, + options: ResolveWorkReportSummaryPeriodOptions = {} +): WorkReportResolvedPeriod { + const { currentRow, periodRange, flag } = options; + + if (currentRow?.periodKey && currentRow.periodStartDate && currentRow.periodEndDate) { + return { + periodKey: currentRow.periodKey, + periodLabel: currentRow.periodLabel || '', + periodStartDate: currentRow.periodStartDate, + periodEndDate: currentRow.periodEndDate, + flag: currentRow.flag + }; + } + + const referenceDate = getRangeReferenceDate(periodRange); + const fallbackNow = dayjs(); + + if (reportType === 'weekly') { + return referenceDate ? buildWeeklyPeriodFromDate(referenceDate) : buildWeeklyPeriodFromDate(fallbackNow); + } + + if (reportType === 'monthly') { + return referenceDate ? buildMonthlyPeriodFromMonth(referenceDate) : buildMonthlyPeriodFromMonth(fallbackNow); + } + + if (referenceDate) { + const resolvedFlag = inferProjectSummaryFlag(referenceDate, flag); + return { + ...buildProjectPeriodFromMonth(referenceDate, resolvedFlag), + flag: resolvedFlag + }; + } + + const fallbackOption = getProjectPeriodOptions(fallbackNow)[0]; + + return { + ...fallbackOption.period, + flag: fallbackOption.flag + }; +} diff --git a/src/views/personal-center/work-report/weekly/index.vue b/src/views/personal-center/work-report/weekly/index.vue index 93f6b2a..cc69fb6 100644 --- a/src/views/personal-center/work-report/weekly/index.vue +++ b/src/views/personal-center/work-report/weekly/index.vue @@ -1,18 +1,19 @@ @@ -303,11 +382,21 @@ defineExpose({ reload }); + + - {{ WORK_REPORT_TYPE_LABEL.weekly }} + {{ reportTitle }} {{ table.mobilePagination.value.total || 0 }} @@ -333,7 +422,13 @@ defineExpose({ reload }); - + diff --git a/src/views/personal-center/work-report/weekly/modules/fill-page.vue b/src/views/personal-center/work-report/weekly/modules/fill-page.vue index 6b2ebcc..a806c21 100644 --- a/src/views/personal-center/work-report/weekly/modules/fill-page.vue +++ b/src/views/personal-center/work-report/weekly/modules/fill-page.vue @@ -1,6 +1,6 @@
{{ contextText }}
加班申请
{{ WORK_REPORT_TYPE_LABEL.monthly }}
{{ reportTitle }}
{{ WORK_REPORT_TYPE_LABEL.project }}
{{ WORK_REPORT_TYPE_LABEL.weekly }}