月报补上团队统计视图,镜像已有的日报团队视图(第9章遗留缺口)
MonthlyReportList 原来只有"我的月报"一种形态,没有 team/managed 入口——日报有的 "团队统计"(应提交/已提交/未提交计数)月报完全没有,路由/组件都不支持。核对 im_be 源码确认后端本身是对称的:TenantMonthlyReportList 的 scope=managed 分支 跟日报字面上是同一段 canUseManagedReports(visibility) 权限校验(真实请求验证: 无权限账号收到 403 PERMISSION_DENIED,不是之前日报那种曾经误报全公司数据的 旧版本行为——那个越权问题后端已经修过了),GET /reports/monthly/statistics 的响应字段(expected_count/submitted_count/missing_count)也和日报完全一致。 唯一确认过的不对称点:月报没有对应的"提醒未提交成员"接口(daily有 POST /reports/daily/reminders,翻遍 im_http.go 月报没有任何同类路由)——团队视图 因此没有画这个按钮,画一个点了必然404的按钮不算镜像,是给用户挖坑。 路由从单一的 monthly-report-list 拆成 monthly-report-mine/monthly-report-team (对齐 daily-report-mine/daily-report-team 的命名),两处 tinode-web.jsx 的硬编码 白名单、APP_ROUTES、详情/编辑页的返回目标都已同步更新。 联调中发现并顺手修正一个刚写的真bug:team 分支的空列表判断沿用了旧的无条件 "暂无月报"文案,没有区分"真的没数据"和"403加载失败"——用真实账号触发403验证时 第一次跑出来就是这个问题,同一轮改完立刻测出来并修正(跟本次会话更早修的 DailyReportList 同款问题是同一个模式,这次是抄自己代码时漏抄了一半)。 真实验证:我的月报视图不受影响;团队统计视图真实触发403(该账号只是普通员工), 正确显示"月报列表加载失败"而不是误导性的"暂无月报";点击"我的月报"correctly 跳回单人视图。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d01547d3de
commit
abef778c7f
@@ -136,7 +136,7 @@ const SIDE_PANEL_VIEWS = ['acc_general','archive','blocked','contacts','cred','c
|
||||
'workbench','organization-directory','meeting-organizer','meeting-attendee',
|
||||
'meeting-create','meeting-detail',
|
||||
'daily-report-editor','daily-report-mine','daily-report-team','daily-report-detail',
|
||||
'monthly-report-editor','monthly-report-list','monthly-report-detail',
|
||||
'monthly-report-editor','monthly-report-mine','monthly-report-team','monthly-report-detail',
|
||||
'executive-mailbox','executive-mailbox-notice',
|
||||
'personal-center','personal-profile','profile-nickname','profile-birth-date',
|
||||
'profile-signature','profile-gender','profile-region','personal-qrcode','android-settings',
|
||||
@@ -2543,7 +2543,7 @@ class TinodeWeb extends React.Component {
|
||||
'call-minimized'].includes(activeProductView);
|
||||
const workplaceCollaborationActive = ['workbench', 'organization-directory', 'meeting-organizer',
|
||||
'meeting-attendee', 'meeting-create', 'meeting-detail', 'daily-report-editor', 'daily-report-mine', 'daily-report-team',
|
||||
'daily-report-detail', 'monthly-report-editor', 'monthly-report-list',
|
||||
'daily-report-detail', 'monthly-report-editor', 'monthly-report-mine', 'monthly-report-team',
|
||||
'monthly-report-detail', 'executive-mailbox', 'executive-mailbox-notice'].includes(activeProductView);
|
||||
const personalSettingsActive = ['personal-center', 'personal-profile', 'profile-nickname',
|
||||
'profile-birth-date', 'profile-signature', 'profile-gender', 'profile-region', 'personal-qrcode', 'android-settings',
|
||||
|
||||
@@ -25,7 +25,7 @@ const navigate = (view, params) => {
|
||||
export const WORKPLACE_COLLABORATION_VIEWS = [
|
||||
'workbench', 'organization-directory', 'meeting-organizer', 'meeting-attendee', 'meeting-create', 'meeting-detail',
|
||||
'daily-report-editor', 'daily-report-mine', 'daily-report-team', 'daily-report-detail',
|
||||
'monthly-report-editor', 'monthly-report-list', 'monthly-report-detail',
|
||||
'monthly-report-editor', 'monthly-report-mine', 'monthly-report-team', 'monthly-report-detail',
|
||||
'executive-mailbox', 'executive-mailbox-notice'
|
||||
];
|
||||
|
||||
@@ -108,7 +108,7 @@ function useUserNames(ids, myUserId) {
|
||||
// "not yet available" toast rather than guessing a feature with zero design reference.
|
||||
const APP_TONES = {organization: 'red', attendance: 'orange', meetings: 'blue', daily_reports: 'orange', monthly_reports: 'violet', executive_mailbox: 'green'};
|
||||
const APP_ICONS = {organization: 'account_tree', attendance: 'schedule', meetings: 'notifications_active', daily_reports: 'task_alt', monthly_reports: 'event_note', executive_mailbox: 'mail_outline'};
|
||||
const APP_ROUTES = {organization: 'organization-directory', meetings: 'meeting-attendee', daily_reports: 'daily-report-mine', monthly_reports: 'monthly-report-list', executive_mailbox: 'executive-mailbox'};
|
||||
const APP_ROUTES = {organization: 'organization-directory', meetings: 'meeting-attendee', daily_reports: 'daily-report-mine', monthly_reports: 'monthly-report-mine', executive_mailbox: 'executive-mailbox'};
|
||||
|
||||
function Workbench({onToast}) {
|
||||
// `me` and `modules` decide whether this screen renders its real content or a blank/loading
|
||||
@@ -851,7 +851,7 @@ function MonthlyReportEditor({onToast}) {
|
||||
.finally(() => setSubmitting(false));
|
||||
};
|
||||
|
||||
return <div className="workplace-page report-editor-page"><Header title="月报" back="monthly-report-list" />
|
||||
return <div className="workplace-page report-editor-page"><Header title="月报" back="monthly-report-mine" />
|
||||
<main>
|
||||
<section className="daily-report-block-header"><strong>月报汇总</strong></section>
|
||||
<label className="report-field"><span><b>*</b>本月工作总结</span>
|
||||
@@ -864,37 +864,65 @@ function MonthlyReportEditor({onToast}) {
|
||||
</div>;
|
||||
}
|
||||
|
||||
function MonthlyReportList({onToast}) {
|
||||
// Not routed through the shared useCachedData hook (unlike this file's other list/detail pages) --
|
||||
// that hook's own fetch failure is silently swallowed (see use-cached-data.js's catch) with no way
|
||||
// for a caller to observe it, only a second `loading` return value nothing here destructures. A
|
||||
// plain local fetch lets this page track its own error state instead.
|
||||
const [reports, setReports] = React.useState(() => readCachedData('monthly-reports:mine') || null);
|
||||
// 团队统计视图镜像 DailyReportList 的同款设计(这个页面原来只有"我的月报"一种,没有任何管理员/
|
||||
// 团队入口,日报有的东西月报完全没有)。核对 im_be 源码确认后端接口本身是对称的:
|
||||
// TenantMonthlyReportList 的 scope=managed 分支跟日报那边字面上是同一段
|
||||
// canUseManagedReports(visibility) 权限校验代码,GET /reports/monthly/statistics 的响应字段
|
||||
// (expected_count/submitted_count/missing_count)也跟日报统计完全一致——真实请求过一次确认
|
||||
// 无权限账号会收到 403 PERMISSION_DENIED 而不是被拒之前那种误报全公司数据。
|
||||
// 唯一确认过不对称的一点:月报没有"提醒未提交成员"这个后端能力(日报有
|
||||
// POST /reports/daily/reminders,月报连对应路由都不存在),所以团队视图不画这个按钮——
|
||||
// 画一个点了必炸404的按钮不算"镜像",是给用户挖坑。
|
||||
function MonthlyReportList({team, myUserId, onToast}) {
|
||||
const cacheKey = 'monthly-reports:' + (team ? 'managed' : 'mine');
|
||||
const statsCacheKey = 'monthly-stats:' + toMonthInputValue(new Date());
|
||||
const [reports, setReports] = React.useState(() => readCachedData(cacheKey) || null);
|
||||
const [loadError, setLoadError] = React.useState(false);
|
||||
const [query, setQuery] = React.useState('');
|
||||
const [stats, setStats] = React.useState(() => (team && readCachedData(statsCacheKey)) || null);
|
||||
const ids = (reports || []).map(r => r.author_id);
|
||||
const names = useUserNames(ids, myUserId);
|
||||
|
||||
React.useEffect(() => {
|
||||
getMonthlyReports({scope: 'mine'}).then(d => {
|
||||
const load = () => {
|
||||
setLoadError(false);
|
||||
getMonthlyReports({scope: team ? 'managed' : 'mine'}).then(d => {
|
||||
const list = d.reports || d.items || [];
|
||||
writeCachedData('monthly-reports:mine', list);
|
||||
writeCachedData(cacheKey, list);
|
||||
setReports(list);
|
||||
}).catch(err => { onToast(err.message || '月报列表加载失败'); setLoadError(true); });
|
||||
}, []);
|
||||
}).catch(err => { onToast(err.message || '月报列表加载失败'); setLoadError(true); setReports(prev => prev || []); });
|
||||
if (team) getMonthlyStatistics(toMonthInputValue(new Date())).then(s => { writeCachedData(statsCacheKey, s); setStats(s); }).catch(() => {});
|
||||
};
|
||||
React.useEffect(load, [team]);
|
||||
|
||||
const filtered = (reports || []).filter(r => (r.month || '').includes(query) || (r.work_summary_short || '').includes(query));
|
||||
const filtered = (reports || []).filter(r => {
|
||||
const hay = team ? `${names[r.author_id] || ''}${r.work_summary_short || ''}` : r.month;
|
||||
return (hay || '').includes(query);
|
||||
});
|
||||
|
||||
return <div className="workplace-page report-list-page">
|
||||
<Header title="月报" action="add" actionLabel="填写月报" onAction={() => navigate('monthly-report-editor')} />
|
||||
<Header title="月报" action="add" actionLabel="填写月报" onAction={() => navigate('monthly-report-editor')} back={team ? 'monthly-report-mine' : 'workbench'} />
|
||||
<main>
|
||||
{team && stats ? <section className="report-statistics">
|
||||
<header><span><small>{formatMonthLabel(toMonthInputValue(new Date()))}</small><strong>全公司</strong></span>
|
||||
<button type="button" onClick={() => navigate('monthly-report-mine')}>我的月报</button></header>
|
||||
<div><span><b>{stats.expected_count || 0}</b><small>应提交</small></span>
|
||||
<span><b>{stats.submitted_count || 0}</b><small>已提交</small></span>
|
||||
<span><b>{(stats.expected_count || 0) - (stats.submitted_count || 0)}</b><small>未提交</small></span></div>
|
||||
</section> : null}
|
||||
<label className="workplace-search"><Icon>search</Icon>
|
||||
<input value={query} onChange={e => setQuery(e.target.value)} placeholder="搜索月份或内容" /></label>
|
||||
<input value={query} onChange={e => setQuery(e.target.value)} placeholder={team ? '搜索姓名或内容' : '搜索月份或内容'} /></label>
|
||||
{!team ? <nav className="report-role-switch">
|
||||
<button type="button" className="active">我的月报</button>
|
||||
<button type="button" onClick={() => navigate('monthly-report-team')}>团队统计</button>
|
||||
</nav> : null}
|
||||
<section className="report-cards">
|
||||
{reports == null ? <p className="organization-loading">{loadError ? '月报列表加载失败' : '加载中…'}</p> :
|
||||
filtered.length === 0 ? <p className="organization-loading">暂无月报</p> :
|
||||
{reports == null ? <p className="organization-loading">加载中…</p> :
|
||||
filtered.length === 0 ? <p className="organization-loading">{loadError ? '月报列表加载失败' : '暂无月报'}</p> :
|
||||
filtered.map(r => <button type="button" key={r.report_id}
|
||||
onClick={() => navigate((r.action || (r.status === 'submitted' ? 'view' : 'edit')) === 'view' ? 'monthly-report-detail' : 'monthly-report-editor', {report_id: r.report_id})}>
|
||||
<span className="report-month"><Icon>calendar_month</Icon></span>
|
||||
<span><strong>{formatMonthLabel(r.month)}</strong><small>工作总结与下月计划</small></span>
|
||||
<span><strong>{team ? (names[r.author_id] || r.author_id) : formatMonthLabel(r.month)}</strong>
|
||||
<small>{team ? formatMonthLabel(r.month) : '工作总结与下月计划'}</small></span>
|
||||
<em className={r.status}>{STATUS_LABEL[r.status] || r.status}</em>
|
||||
<Icon>chevron_right</Icon>
|
||||
</button>)}
|
||||
@@ -915,7 +943,7 @@ function MonthlyReportDetail({hashParams, myUserId, onToast}) {
|
||||
setReport(readCachedData('monthly-report:' + reportId) || null);
|
||||
getMonthlyReport(reportId).then(r => { writeCachedData('monthly-report:' + reportId, r); setReport(r); }).catch(err => { onToast(err.message || '月报加载失败'); setLoadError(true); });
|
||||
}, [reportId]);
|
||||
if (!report) return <div className="workplace-page"><Header title="查看详情" back="monthly-report-list" /><main><p className="organization-loading">{loadError ? '月报加载失败' : '加载中…'}</p></main></div>;
|
||||
if (!report) return <div className="workplace-page"><Header title="查看详情" back="monthly-report-mine" /><main><p className="organization-loading">{loadError ? '月报加载失败' : '加载中…'}</p></main></div>;
|
||||
|
||||
// reopenMonthlyReport is self-service only (server keys the UPDATE off the caller's own uid --
|
||||
// see the comment on the im-api.js export), so this button only ever makes sense for the report's
|
||||
@@ -929,7 +957,7 @@ function MonthlyReportDetail({hashParams, myUserId, onToast}) {
|
||||
.finally(() => setReopening(false));
|
||||
};
|
||||
|
||||
return <div className="workplace-page"><Header title="查看详情" back="monthly-report-list" />
|
||||
return <div className="workplace-page"><Header title="查看详情" back="monthly-report-mine" />
|
||||
<main className="report-detail">
|
||||
<section className="report-author"><span><strong>{names[report.author_id] || '…'}</strong><small className="role-badge">员工</small></span>
|
||||
<em className={report.status}>{STATUS_LABEL[report.status] || report.status}</em></section>
|
||||
@@ -1053,7 +1081,7 @@ export default function WorkplaceCollaborationView({view, hashParams, myUserId,
|
||||
else if (view === 'daily-report-editor') content = <DailyReportEditor onToast={setToast} />;
|
||||
else if (view === 'monthly-report-editor') content = <MonthlyReportEditor onToast={setToast} />;
|
||||
else if (view === 'daily-report-mine' || view === 'daily-report-team') content = <DailyReportList team={view === 'daily-report-team'} myUserId={myUserId} onToast={setToast} />;
|
||||
else if (view === 'monthly-report-list') content = <MonthlyReportList onToast={setToast} />;
|
||||
else if (view === 'monthly-report-mine' || view === 'monthly-report-team') content = <MonthlyReportList team={view === 'monthly-report-team'} myUserId={myUserId} onToast={setToast} />;
|
||||
else if (view === 'daily-report-detail') content = <DailyReportDetail hashParams={hashParams} myUserId={myUserId} onToast={setToast} />;
|
||||
else if (view === 'monthly-report-detail') content = <MonthlyReportDetail hashParams={hashParams} myUserId={myUserId} onToast={setToast} />;
|
||||
else content = <ExecutiveMailbox tinode={tinode} noticeOnly={view === 'executive-mailbox-notice'} onToast={setToast} />;
|
||||
|
||||
Reference in New Issue
Block a user