Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
I
integrity_practice_management_new
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
David Yang
integrity_practice_management_new
Commits
93b2457f
Commit
93b2457f
authored
Sep 24, 2026
by
drigle
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
update: 推送流程
parent
858cfe78
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
705 additions
and
0 deletions
+705
-0
test-auth-redirect.cjs
scripts/test-auth-redirect.cjs
+21
-0
test-self-inspection-flow.cjs
scripts/test-self-inspection-flow.cjs
+133
-0
test-self-inspection-push.cjs
scripts/test-self-inspection-push.cjs
+36
-0
SelfInspectionFlowPage.jsx
.../integrity_practice_management/SelfInspectionFlowPage.jsx
+216
-0
selfInspectionFlowService.js
...ntegrity_practice_management/selfInspectionFlowService.js
+267
-0
authRedirect.js
src/utils/authRedirect.js
+32
-0
No files found.
scripts/test-auth-redirect.cjs
0 → 100644
View file @
93b2457f
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const ts = require('typescript');
const result = {};
let navigations = [];
const origin = 'https://example.test';
const location = { origin, pathname: '/integrity_practice_management_new/self_inspection_form', href: origin + '/integrity_practice_management_new/self_inspection_form?id=flow%26one&type=editor#row2', replace: url => navigations.push(url) };
vm.runInNewContext(ts.transpileModule(fs.readFileSync('src/utils/authRedirect.js','utf8'), {compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText, {exports:result, URL, window:{location}, require:()=>({APPLICATION_BASE_PATH:'/integrity_practice_management_new/',ROUTE_TITLES:{'/notifications':'通知'},SELF_INSPECTION_FLOW_PATH:'/self_inspection_form'})});
const target = '/self_inspection_form?id=flow%26one&type=editor#row2';
assert.equal(result.normalizeAuthRedirect(location.href), target);
assert.equal(result.normalizeAuthRedirect(target), target);
assert.equal(new URLSearchParams(result.getLoginRoute(location.href).split('?')[1]).get('redirect'),target);
for(const input of ['https://evil.test/self_inspection_form','//evil.test/self_inspection_form','javascript:alert(1)','/login?redirect=/login','/another_app/form']) assert.equal(result.normalizeAuthRedirect(input),'/');
result.redirectToLogin();
assert.equal(navigations.length,1);
assert.ok(navigations[0].startsWith('/integrity_practice_management_new/login?redirect='));
location.pathname='/integrity_practice_management_new/login';
result.redirectToLogin();
assert.equal(navigations.length,1);
console.log('PASS: deep links, query/hash, base normalization, external/login-loop rejection, 401 redirect');
scripts/test-self-inspection-flow.cjs
0 → 100644
View file @
93b2457f
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const ts = require('typescript');
const source = fs.readFileSync('src/pages/integrity_practice_management/selfInspectionFlowService.js', 'utf8');
const compiled = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } }).outputText;
let message, rows, parents, calls, failure, denyField;
const clone = (value) => JSON.parse(JSON.stringify(value));
const subject = { fields: [{ name: 'self_inspection_result', isRequired: true }, { name: 'attachment', multiple: true }] };
const api = {
loadRecord: async (id) => clone(id === 'p1' ? parents[0] : message),
loadSubject: async () => clone(subject),
loadUser: async (id) => ({ _id: id, username: id, display_name: id }),
queryRecords: async ({ subject }) => ({ records: clone(subject === 'self_check_the_project_results' ? rows : parents) }),
updateRecord: async (id, data) => {
calls.push(['update', id, clone(data)]);
if (failure === 'save' && id === 'r1') throw Error('save failed');
if (id === 'm1') Object.assign(message.metadata, data.metadata);
},
batchUpdateRecords: async (data) => { calls.push(['batch', clone(data)]); },
executeAction: async (action, data) => {
calls.push([action, clone(data)]);
if (failure === 'oa') throw Error('oa failed');
return { data: { taskId: `task-${calls.length}` } };
},
createNotice: async (data) => { calls.push(['notice', clone(data)]); return { name: 'notice' }; },
sendNotice: async () => { if (failure === 'notice') throw Error('notice failed'); },
};
const exportsObject = {};
vm.runInNewContext(compiled, { exports: exportsObject, require(name) {
if (name.endsWith('DataService')) return api;
if (name.endsWith('subjectView')) return { getSelectOptions: () => [], isFieldDisabledByView: (field) => field === denyField };
if (name.endsWith('application')) return { getSelfInspectionFlowUrl: (name) => `https://test/integrity_practice_management_new/self_inspection_form?id=${name}` };
return { createIntegrityPracticeMaterial: async () => { calls.push(['material']); return { id: 'material1' }; } };
}});
const service = exportsObject;
function reset() {
message = { _id: 'm1', name: 'M001', subject: { name: 'message_reference_id' }, metadata: {
subject_name: 'self_check_the_project_results', restriction_value: ['r1'], reference_record_id: ['p1'],
editor: ['editor', 'editor2'], auditor: ['auditor', 'auditor2'], sender: 'sender',
editor_status: '未完成', auditor_status: '未完成', sender_status: '未完成', progress_id_editor: ['old-editor'],
}};
rows = [{ _id: 'r1', metadata: { self_inspection_result: '已检查' } }];
parents = [{ _id: 'p1', subject: { name: 'self_check_the_results_of_the_working_draft' }, metadata: { project_outcome: [{ _id: 'r1' }] } }]; calls = []; failure = null; denyField = null;
}
const user = (id) => ({ _id: id, username: id });
const transition = (action, id) => service.transitionSelfInspectionFlow({ messageName: 'M001', currentUser: user(id), action });
(async () => {
reset();
await assert.rejects(() => service.loadSelfInspectionFlow('M001', user('guest')), /参与人/);
message.metadata.subject_name = 'problem_rectification';
await assert.rejects(() => service.loadSelfInspectionFlow('M001', user('editor')), /不是廉洁/);
reset();
await assert.rejects(() => transition('approve', 'auditor'), /状态已变化/);
assert.equal(calls.length, 0);
await transition('submit', 'editor');
assert.equal(message.metadata.editor_status, '已完成');
assert.equal(calls.filter(([name]) => name === 'oa_notice:createProgress').length, 2);
assert.ok(calls.find(([name, value]) => name === 'notice' && value.content.includes('/integrity_practice_management_new/')));
await transition('withdraw', 'editor2');
assert.equal(message.metadata.editor_status, '未完成');
assert.equal(calls.filter(([name]) => name === 'notice').length, 1);
await transition('submit', 'editor');
await transition('reject', 'auditor2');
assert.equal(message.metadata.editor_status, '未完成');
assert.ok(calls.some(([name, value]) => name === 'batch' && value.data['metadata.review_status'] === '已驳回'));
assert.ok(!calls.some(([name, value]) => name === 'batch' && value.data['metadata.status'] === '已驳回'));
await transition('submit', 'editor');
await transition('approve', 'auditor');
await assert.rejects(() => transition('withdraw', 'editor'), /状态已变化/);
await transition('finalReject', 'sender');
await transition('submit', 'editor');
assert.equal(message.metadata.sender_status, '未完成');
await transition('approve', 'auditor');
await transition('finalApprove', 'sender');
assert.equal(service.getFlowPermissions(message, user('sender')).step, 'completed');
await assert.rejects(() => transition('finalApprove', 'sender'), /状态已变化/);
reset();
message.metadata.editor.push('sender');
assert.equal(service.getFlowPermissions(message, user('sender')).canSubmit, true);
await assert.rejects(() => service.saveSelfInspectionResults({ messageName: 'M001', currentUser: user('editor'), changes: { alien: { self_inspection_result: 'bad' } } }), /以外/);
rows[0].metadata.self_inspection_result = '';
await assert.rejects(() => transition('submit', 'editor'), /请填写/);
assert.equal(calls.length, 0);
reset();
assert.equal(service.getFlowPermissions(message, user('auditor')).canEdit, true);
message.metadata.editor_status = '已完成';
assert.equal(service.getFlowPermissions(message, user('editor')).canEdit, false);
assert.equal(service.getFlowPermissions(message, user('auditor')).canEdit, true);
message.metadata.auditor_status = '已完成';
assert.equal(service.getFlowPermissions(message, user('auditor')).canEdit, false);
assert.equal(service.getFlowPermissions(message, user('sender')).canEdit, true);
reset(); denyField = 'metadata.self_inspection_result';
await assert.rejects(() => service.saveSelfInspectionResults({ messageName: 'M001', currentUser: user('editor'), changes: { r1: { self_inspection_result: 'unauthorized' } } }), /视图不允许/);
assert.equal(calls.length, 0);
reset(); message.metadata.restriction_value = [];
const recovered = await service.loadSelfInspectionFlow('M001', user('editor'));
assert.equal(recovered.rows[0]._id, 'r1');
assert.equal(calls.length, 0, 'reading an old flow must not write');
await service.saveSelfInspectionResults({ messageName: 'M001', currentUser: user('editor'), changes: { r1: { self_inspection_result: 'recovered' } } });
await transition('submit', 'editor');
assert.ok(calls.some(([kind,data]) => kind === 'batch' && data.filter._id.$in.includes('r1')));
reset(); parents[0].metadata.project_outcome = ['other'];
assert.equal((await service.loadSelfInspectionFlow('M001',user('editor'))).rows[0]._id,'r1', 'explicit flow IDs remain authoritative');
reset(); message.metadata.restriction_value = []; parents[0].metadata.project_outcome = [];
await assert.rejects(() => service.loadSelfInspectionFlow('M001', user('editor')), /均没有自查项目/);
reset();
assert.equal(service.getFlowApprovalStatus(message, '已审批'), '未审批');
assert.equal(service.getFlowApprovalStatus(message, '已审批', true), '未审批');
message.metadata.editor_status = '已完成';
assert.equal(service.getFlowApprovalStatus(message, '已审批'), '未审批');
message.metadata.auditor_status = '已完成';
assert.equal(service.getFlowApprovalStatus(message, '已审批'), '已审批');
assert.equal(service.getFlowApprovalStatus(message, '已审批', true), '未审批');
message.metadata.sender_status = '已完成';
assert.equal(service.getFlowApprovalStatus(message, '已审批', true), '已审批');
message.metadata.editor_status = '未完成';
assert.equal(service.getFlowApprovalStatus(message, '已驳回', true), '已驳回');
assert.equal(service.getFlowApprovalStatus(message, '已审批'), '未审批');
reset(); failure = 'oa';
await assert.rejects(() => transition('submit', 'editor'), /oa failed/);
assert.equal(message.metadata.editor_status, '未完成');
reset(); failure = 'notice';
await assert.rejects(() => transition('submit', 'editor'), /状态已更新/);
assert.equal(message.metadata.editor_status, '已完成');
reset(); failure = 'save';
const pending = { r1: [{ fileAsset: { url: '/file.pdf' }, materialRecordId: null }] };
const save = () => service.saveSelfInspectionResults({ messageName: 'M001', currentUser: user('editor'), changes: { r1: { attachment: [{ name: 'file.pdf', url: '/file.pdf' }] } }, pendingMaterials: pending });
await assert.rejects(save, /save failed/); failure = null; await save();
assert.equal(calls.filter(([name]) => name === 'material').length, 1);
assert.equal(pending.r1[0].materialRecordId, 'material1');
console.log('PASS: permissions, wrong subject, transitions, withdrawal, rejection/resubmission, terminal state, required fields, OA failure, partial notification failure, material retry');
})().catch((error) => { console.error(error); process.exitCode = 1; });
scripts/test-self-inspection-push.cjs
0 → 100644
View file @
93b2457f
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const ts = require('typescript');
const exportsObject = {};
let writes = [], empty = false;
const toArray = value => value == null ? [] : Array.isArray(value) ? value : [value];
const api = {
loadRecord: async id => ({ _id: id, name: id, title: id, metadata: { project_outcome: empty && id === 'p2' ? [] : [{ _id: 'r1' }, 'r2', 'r2'] } }),
getSubjectView: async () => [{ name: 'view' }],
batchUpdateRecords: async value => writes.push(['batch', value]),
updateRecord: async (...args) => writes.push(['update', ...args]),
createRecord: async value => { writes.push(['create', value]); return { _id: 'flow', name: 'flow' }; },
loadUser: async id => ({ _id: id, username: id }),
executeAction: async (...args) => { writes.push(['oa', ...args]); return { data: { taskId: 'task' } }; },
createNotice: async value => { writes.push(['notice', value]); return { name: 'notice' }; },
sendNotice: async () => {},
};
const compiled = ts.transpileModule(fs.readFileSync('src/pages/integrity_practice_management/integrityPracticeWorkbenchService.js', 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } }).outputText;
vm.runInNewContext(compiled, { exports: exportsObject, require(name) {
if (name.endsWith('DataService')) return api;
if (name.endsWith('subjectView')) return { toArray };
if (name.endsWith('application')) return { getSelfInspectionFlowUrl: () => '/flow' };
if (name === 'dayjs') return { default: () => ({ format: () => '2026-09-20' }) };
return {};
}});
const push = records => exportsObject.executeConfiguredAction('integrityRiskManagerService.pushSelfInspectionForm', { selectedRecords: records, userInfo: { _id: 'sender' }, assignment: { fillers: ['editor'], auditors: ['auditor'] } });
(async () => {
await push([{ _id: 'p1', metadata: {} }]);
const created = writes.find(([kind]) => kind === 'create')[1];
assert.equal(JSON.stringify(created.metadata.restriction_value), '["r1","r2"]');
writes = []; empty = true;
await assert.rejects(() => push([{ _id: 'p1' }, { _id: 'p2' }]), /没有关联自查项目/);
assert.equal(writes.length, 0);
console.log('PASS: push hydrates omitted list references, normalizes IDs, and validates entire batch before any writes');
})().catch(error => { console.error(error); process.exitCode = 1; });
src/pages/integrity_practice_management/SelfInspectionFlowPage.jsx
0 → 100644
View file @
93b2457f
import
{
Reload
,
Info
}
from
'@/components/AppIcons'
;
import
{
DataTable
,
DataTableText
,
DataTableTextPair
}
from
'@/components/DataTable'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
Alert
,
AlertDialog
,
Button
,
Card
,
Chip
,
Spinner
,
TextArea
,
Toast
,
Tooltip
,
toast
}
from
'@heroui/react'
;
import
{
useCallback
,
useContext
,
useEffect
,
useRef
,
useState
}
from
'react'
;
import
{
useSearchParams
}
from
'umi'
;
import
IntegrityPracticeMaterialField
from
'./components/IntegrityPracticeMaterialField'
;
import
{
flowIds
,
getEditableResultFields
,
getFlowApprovalStatus
,
loadSelfInspectionFlow
,
saveSelfInspectionResults
,
toFlowId
,
transitionSelfInspectionFlow
,
}
from
'./selfInspectionFlowService'
;
const
ACTION_LABELS
=
{
submit
:
'提交审核'
,
withdraw
:
'撤回提交'
,
approve
:
'审核通过'
,
reject
:
'审核驳回'
,
finalApprove
:
'最终通过'
,
finalReject
:
'最终驳回'
,
};
const
STEP_LABELS
=
{
editor
:
'待填写'
,
auditor
:
'待审核'
,
sender
:
'待最终审核'
,
completed
:
'已完成'
};
function
StatusChip
({
value
})
{
return
<
Chip
size=
"sm"
color=
{
value
===
'已审批'
||
value
===
'已完成'
?
'success'
:
value
===
'已驳回'
?
'danger'
:
'warning'
}
>
{
value
||
'未审批'
}
</
Chip
>;
}
export
default
function
SelfInspectionFlowPage
()
{
const
currentUser
=
useContext
(
UserContext
);
const
[
searchParams
]
=
useSearchParams
();
const
messageName
=
searchParams
.
get
(
'id'
)
||
''
;
const
[
flow
,
setFlow
]
=
useState
(
null
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
busy
,
setBusy
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
changes
,
setChanges
]
=
useState
({});
const
[
validation
,
setValidation
]
=
useState
({});
const
[
confirmAction
,
setConfirmAction
]
=
useState
(
null
);
const
[
session
,
setSession
]
=
useState
(
0
);
const
[
compact
,
setCompact
]
=
useState
(()
=>
window
.
innerWidth
<
768
);
const
pendingMaterials
=
useRef
({});
const
busyRef
=
useRef
(
false
);
const
requestVersion
=
useRef
(
0
);
const
reload
=
useCallback
(
async
()
=>
{
const
version
=
++
requestVersion
.
current
;
setLoading
(
true
);
try
{
const
result
=
await
loadSelfInspectionFlow
(
messageName
,
currentUser
);
if
(
version
!==
requestVersion
.
current
)
return
;
setFlow
(
result
);
setChanges
({});
setValidation
({});
pendingMaterials
.
current
=
{};
setSession
((
value
)
=>
value
+
1
);
}
catch
(
requestError
)
{
if
(
version
===
requestVersion
.
current
)
{
setFlow
(
null
);
setError
(
requestError
?.
message
||
'自查底稿加载失败,请重试。'
);
}
}
finally
{
if
(
version
===
requestVersion
.
current
)
setLoading
(
false
);
}
},
[
messageName
,
currentUser
?.
_id
]);
useEffect
(()
=>
{
setError
(
''
);
setFlow
(
null
);
reload
();
return
()
=>
{
requestVersion
.
current
+=
1
;
};
},
[
reload
]);
useEffect
(()
=>
{
document
.
title
=
'自查底稿填写与审核 - 廉洁从业管理'
;
},
[]);
useEffect
(()
=>
{
const
media
=
window
.
matchMedia
(
'(max-width: 767px)'
);
const
update
=
()
=>
setCompact
(
media
.
matches
);
update
();
media
.
addEventListener
(
'change'
,
update
);
return
()
=>
media
.
removeEventListener
(
'change'
,
update
);
},
[]);
useEffect
(()
=>
{
if
(
!
Object
.
keys
(
changes
).
length
)
return
undefined
;
const
guard
=
(
event
)
=>
{
event
.
preventDefault
();
event
.
returnValue
=
''
;
};
window
.
addEventListener
(
'beforeunload'
,
guard
);
return
()
=>
window
.
removeEventListener
(
'beforeunload'
,
guard
);
},
[
changes
]);
const
permissions
=
flow
?.
permissions
||
{};
const
rows
=
flow
?.
rows
||
[];
const
metadata
=
flow
?.
messageRecord
?.
metadata
||
{};
const
editableFields
=
getEditableResultFields
(
flow
?.
subject
,
flow
?.
view
,
currentUser
);
const
resultField
=
editableFields
.
find
((
field
)
=>
field
.
name
===
'self_inspection_result'
);
const
attachmentField
=
editableFields
.
find
((
field
)
=>
field
.
name
===
'attachment'
);
const
hasValidation
=
Object
.
values
(
validation
).
some
(
Boolean
);
const
disabled
=
busy
||
loading
||
hasValidation
;
const
hasChanges
=
Object
.
keys
(
changes
).
length
>
0
;
const
userLabel
=
(
ids
)
=>
flowIds
(
ids
).
map
((
id
)
=>
{
const
user
=
flow
?.
users
.
find
((
item
)
=>
toFlowId
(
item
)
===
id
);
return
user
?.
display_name
||
user
?.
username
||
id
;
}).
join
(
'、'
)
||
'未配置'
;
const
draftFor
=
(
row
)
=>
({
...
row
.
metadata
,
...
changes
[
row
.
_id
]
});
const
changeField
=
(
id
,
field
,
value
)
=>
setChanges
((
previous
)
=>
({
...
previous
,
[
id
]:
{
...
previous
[
id
],
[
field
]:
value
},
}));
const
run
=
async
(
action
,
row
)
=>
{
if
(
busyRef
.
current
||
hasValidation
)
return
;
busyRef
.
current
=
true
;
setBusy
(
true
);
setError
(
''
);
try
{
if
(
action
===
'save'
||
(
hasChanges
&&
action
!==
'withdraw'
))
{
const
updates
=
row
?
{
[
row
.
_id
]:
changes
[
row
.
_id
]
||
{}
}
:
changes
;
await
saveSelfInspectionResults
({
messageName
,
currentUser
,
changes
:
updates
,
pendingMaterials
:
pendingMaterials
.
current
});
if
(
action
===
'save'
)
{
// Preserve unsaved edits in other rows when saving one row.
setFlow
((
previous
)
=>
({
...
previous
,
rows
:
previous
.
rows
.
map
((
item
)
=>
updates
[
item
.
_id
]
?
{
...
item
,
metadata
:
{
...
item
.
metadata
,
...
updates
[
item
.
_id
]
}
}
:
item
)
}));
setChanges
((
previous
)
=>
Object
.
fromEntries
(
Object
.
entries
(
previous
).
filter
(([
id
])
=>
!
Object
.
prototype
.
hasOwnProperty
.
call
(
updates
,
id
))));
toast
.
success
(
'自查结果已保存'
,
{
timeout
:
3500
});
return
;
}
}
await
transitionSelfInspectionFlow
({
messageName
,
currentUser
,
action
});
toast
.
success
(
`
${
ACTION_LABELS
[
action
]}
成功`
,
{
timeout
:
3500
});
setConfirmAction
(
null
);
await
reload
();
}
catch
(
requestError
)
{
setError
(
requestError
?.
message
||
'操作失败,请稍后重试。'
);
setConfirmAction
(
null
);
// Keep local drafts on failure; refresh explicitly after checking partial writes.
}
finally
{
busyRef
.
current
=
false
;
setBusy
(
false
);
}
};
const
columns
=
[
{
key
:
'project'
,
title
:
'自查项目'
,
minWidth
:
240
,
width
:
240
,
isPrimary
:
true
,
render
:
(
row
)
=>
<
DataTableTextPair
primary=
{
row
.
metadata
?.
project
||
row
.
title
}
secondary=
{
row
.
metadata
?.
inspection_item
}
/>
},
{
key
:
'content'
,
title
:
'自查要点及关注风险点'
,
minWidth
:
300
,
width
:
300
,
render
:
(
row
)
=>
<
DataTableText
text=
{
row
.
metadata
?.
content
}
showTooltip
maxLines=
{
3
}
/>
},
{
key
:
'self_inspection_result'
,
title
:
<
span
>
自查结果
{
resultField
?.
isRequired
||
resultField
?.
required
?
<
span
className=
"text-danger"
>
*
</
span
>
:
null
}
</
span
>,
minWidth
:
320
,
width
:
320
,
render
:
(
row
)
=>
permissions
.
canEdit
&&
resultField
?
(
<
TextArea
aria
-
label=
{
`${row.metadata?.project || row.title} 自查结果`
}
rows=
{
4
}
variant=
"secondary"
isDisabled=
{
busy
}
isRequired=
{
Boolean
(
resultField
.
isRequired
||
resultField
.
required
)
}
value=
{
draftFor
(
row
).
self_inspection_result
||
''
}
onChange=
{
(
event
)
=>
changeField
(
row
.
_id
,
'self_inspection_result'
,
event
.
target
.
value
)
}
/>
)
:
<
DataTableText
text=
{
row
.
metadata
?.
self_inspection_result
}
showTooltip
maxLines=
{
3
}
/>
},
{
key
:
'attachment'
,
title
:
'附件'
,
minWidth
:
300
,
width
:
300
,
render
:
(
row
)
=>
permissions
.
canEdit
&&
attachmentField
?
(
<
div
inert=
{
busy
?
true
:
undefined
}
>
<
IntegrityPracticeMaterialField
key=
{
`${session}-${row._id}`
}
editSessionId=
{
session
}
fieldPath=
{
row
.
_id
}
multiple=
{
Boolean
(
attachmentField
.
multiple
)
}
value=
{
draftFor
(
row
).
attachment
}
onChange=
{
(
value
)
=>
changeField
(
row
.
_id
,
'attachment'
,
value
)
}
onValidationChange=
{
(
id
,
message
)
=>
setValidation
((
previous
)
=>
({
...
previous
,
[
id
]:
message
}))
}
onPendingLocalMaterialChange=
{
(
id
,
change
)
=>
{
const
current
=
pendingMaterials
.
current
[
id
]
||
[];
pendingMaterials
.
current
[
id
]
=
change
.
type
===
'replace'
?
change
.
materials
:
change
.
type
===
'remove'
?
current
.
filter
((
item
)
=>
item
.
clientId
!==
change
.
clientId
)
:
[...
new
Map
([...
current
,
...
change
.
materials
].
map
((
item
)
=>
[
item
.
clientId
,
item
])).
values
()];
}
}
/>
</
div
>
)
:
<
div
className=
"space-y-2"
>
{
(
Array
.
isArray
(
row
.
metadata
?.
attachment
)
?
row
.
metadata
.
attachment
:
[
row
.
metadata
?.
attachment
]).
filter
(
Boolean
).
map
((
file
,
index
)
=>
(
<
a
key=
{
`${file.url}-${index}`
}
className=
"block truncate text-accent"
href=
{
file
.
url
}
target=
"_blank"
rel=
"noreferrer"
>
{
file
.
name
||
'查看附件'
}
</
a
>
))
}
</
div
>
},
{
key
:
'status'
,
title
:
'审批状态'
,
minWidth
:
180
,
width
:
180
,
render
:
(
row
)
=>
<
div
className=
"space-y-2"
><
div
className=
"flex items-center justify-between gap-2"
><
span
className=
"text-xs text-muted"
>
审核
</
span
><
StatusChip
value=
{
getFlowApprovalStatus
(
flow
?.
messageRecord
,
row
.
metadata
?.
status
,
false
)
}
/></
div
><
div
className=
"flex items-center justify-between gap-2"
><
span
className=
"text-xs text-muted"
>
最终审核
</
span
><
StatusChip
value=
{
getFlowApprovalStatus
(
flow
?.
messageRecord
,
row
.
metadata
?.
final_status
,
true
)
}
/></
div
></
div
>
},
...(
permissions
.
canEdit
?
[{
key
:
'actions'
,
title
:
'操作'
,
minWidth
:
104
,
width
:
104
,
isActions
:
true
,
render
:
(
row
)
=>
<
Button
size=
"sm"
variant=
"secondary"
isDisabled=
{
disabled
||
!
changes
[
row
.
_id
]
}
onPress=
{
()
=>
run
(
'save'
,
row
)
}
>
保存
</
Button
>
}]
:
[]),
];
const
availableActions
=
[
permissions
.
canSubmit
&&
'submit'
,
permissions
.
canWithdraw
&&
'withdraw'
,
permissions
.
canAudit
&&
'reject'
,
permissions
.
canAudit
&&
'approve'
,
permissions
.
canFinalAudit
&&
'finalReject'
,
permissions
.
canFinalAudit
&&
'finalApprove'
,
].
filter
(
Boolean
);
return
(
<
main
className=
"min-h-dvh min-w-0 overflow-x-hidden bg-background px-4 py-4 text-foreground sm:px-6 sm:py-6 lg:px-12"
>
<
div
className=
"flex min-w-0 flex-col gap-6"
>
<
Card
><
Card
.
Content
className=
"flex flex-wrap items-start justify-between gap-4"
>
<
div
className=
"min-w-0 space-y-3"
>
<
div
className=
"flex flex-wrap items-center gap-3"
><
h1
className=
"text-lg font-medium"
>
自查底稿填写与审核
</
h1
>
{
flow
?
<
StatusChip
value=
{
STEP_LABELS
[
permissions
.
step
]
}
/>
:
null
}
</
div
>
<
dl
className=
"flex flex-wrap gap-x-6 gap-y-2 text-sm"
>
<
div
><
dt
className=
"inline text-muted"
>
流程编号:
</
dt
><
dd
className=
"inline break-all"
>
{
messageName
||
'未提供'
}
</
dd
></
div
>
{
flow
?
<><
div
><
dt
className=
"inline text-muted"
>
填报人:
</
dt
><
dd
className=
"inline"
>
{
userLabel
(
metadata
.
editor
)
}
</
dd
></
div
><
div
><
dt
className=
"inline text-muted"
>
审核人:
</
dt
><
dd
className=
"inline"
>
{
userLabel
(
metadata
.
auditor
)
}
</
dd
></
div
><
div
><
dt
className=
"inline text-muted"
>
发起人:
</
dt
><
dd
className=
"inline"
>
{
userLabel
(
metadata
.
sender
)
}
</
dd
></
div
></>
:
null
}
</
dl
>
{
flow
?.
drafts
.
map
((
draft
)
=>
<
div
className=
"flex flex-wrap items-center gap-3 text-sm"
key=
{
draft
.
_id
}
><
span
>
{
draft
.
title
}
</
span
><
span
className=
"text-muted"
>
{
draft
.
metadata
?.
actual_filling_department
}
</
span
><
span
>
审核:
<
StatusChip
value=
{
getFlowApprovalStatus
(
flow
?.
messageRecord
,
draft
.
metadata
?.
review_status
,
false
)
}
/></
span
><
span
>
最终审核:
<
StatusChip
value=
{
getFlowApprovalStatus
(
flow
?.
messageRecord
,
draft
.
metadata
?.
final_review_status
,
true
)
}
/></
span
></
div
>)
}
</
div
>
<
Tooltip
><
Button
aria
-
label=
"刷新流程"
isIconOnly
size=
"sm"
variant=
"secondary"
isDisabled=
{
disabled
}
onPress=
{
()
=>
{
if
(
hasChanges
)
setConfirmAction
(
'refresh'
);
else
{
setError
(
''
);
reload
();
}
}
}
><
Reload
size=
{
16
}
/></
Button
><
Tooltip
.
Content
>
刷新流程
</
Tooltip
.
Content
></
Tooltip
>
</
Card
.
Content
></
Card
>
{
error
?
<
Alert
status=
"danger"
><
Alert
.
Content
><
Alert
.
Description
>
{
error
}
</
Alert
.
Description
></
Alert
.
Content
></
Alert
>
:
null
}
{
loading
?
<
div
className=
"grid min-h-56 place-items-center"
role=
"status"
aria
-
label=
"正在加载自查底稿"
><
Spinner
/></
div
>
:
flow
?
(
<
section
className=
"flex min-w-0 flex-col gap-4"
>
<
div
className=
"flex flex-wrap items-center justify-between gap-3"
>
<
div
className=
"inline-flex items-center gap-1.5"
><
h2
className=
"text-base font-medium"
>
自查项目
</
h2
><
Tooltip
><
Button
isIconOnly
aria
-
label=
"填写说明"
size=
"sm"
variant=
"ghost"
><
Info
size=
{
14
}
/></
Button
><
Tooltip
.
Content
>
在表格中填写自查结果并保存;提交审核会保存当前全部修改。审核与最终审核按整份底稿处理。
</
Tooltip
.
Content
></
Tooltip
></
div
>
<
div
className=
"flex flex-wrap items-center gap-2"
>
{
permissions
.
canEdit
?
<
Button
size=
"sm"
variant=
"secondary"
isDisabled=
{
disabled
||
!
hasChanges
}
onPress=
{
()
=>
run
(
'save'
)
}
>
保存全部
</
Button
>
:
null
}
{
availableActions
.
map
((
action
)
=>
<
Button
key=
{
action
}
size=
"sm"
variant=
{
action
.
toLowerCase
().
includes
(
'reject'
)
?
'danger-soft'
:
action
===
'withdraw'
?
'secondary'
:
'primary'
}
isDisabled=
{
disabled
||
!
rows
.
length
}
onPress=
{
()
=>
setConfirmAction
(
action
)
}
>
{
ACTION_LABELS
[
action
]
}
</
Button
>)
}
</
div
>
</
div
>
<
DataTable
ariaLabel=
"廉洁从业自查项目"
columns=
{
columns
}
rows=
{
rows
}
pinColumns=
{
!
compact
}
getRowKey=
{
(
row
)
=>
row
.
_id
}
emptyTitle=
"当前流程没有关联的自查项目"
/>
</
section
>
)
:
null
}
</
div
>
<
AlertDialog
isOpen=
{
Boolean
(
confirmAction
)
}
onOpenChange=
{
(
open
)
=>
{
if
(
!
open
&&
!
busy
)
setConfirmAction
(
null
);
}
}
>
<
AlertDialog
.
Backdrop
isKeyboardDismissDisabled=
{
busy
}
><
AlertDialog
.
Container
size=
"md"
><
AlertDialog
.
Dialog
>
<
AlertDialog
.
Header
><
AlertDialog
.
Heading
>
{
confirmAction
===
'refresh'
?
'放弃未保存的修改?'
:
`确认${ACTION_LABELS[confirmAction] || '操作'}?`
}
</
AlertDialog
.
Heading
></
AlertDialog
.
Header
>
<
AlertDialog
.
Body
>
{
confirmAction
===
'refresh'
?
'刷新后将重新加载已保存的数据。'
:
confirmAction
===
'submit'
?
'将保存当前修改并把整份底稿交给审核人,提交后不能继续填写。'
:
confirmAction
?.
toLowerCase
().
includes
(
'reject'
)
?
'整份底稿将退回填报人重新填写。'
:
confirmAction
===
'withdraw'
?
'撤回后恢复填写,审核人的当前待办将结束。'
:
'本次操作作用于当前流程关联的全部自查项目。'
}
</
AlertDialog
.
Body
>
<
AlertDialog
.
Footer
><
Button
variant=
"secondary"
isDisabled=
{
busy
}
onPress=
{
()
=>
setConfirmAction
(
null
)
}
>
取消
</
Button
><
Button
variant=
{
confirmAction
?.
toLowerCase
().
includes
(
'reject'
)
?
'danger'
:
'primary'
}
isPending=
{
busy
}
isDisabled=
{
busy
}
onPress=
{
()
=>
{
if
(
confirmAction
===
'refresh'
)
{
setConfirmAction
(
null
);
setError
(
''
);
reload
();
}
else
run
(
confirmAction
);
}
}
>
确认
</
Button
></
AlertDialog
.
Footer
>
</
AlertDialog
.
Dialog
></
AlertDialog
.
Container
></
AlertDialog
.
Backdrop
>
</
AlertDialog
>
<
Toast
.
Provider
maxVisibleToasts=
{
3
}
placement=
"top"
/>
</
main
>
);
}
src/pages/integrity_practice_management/selfInspectionFlowService.js
0 → 100644
View file @
93b2457f
import
{
batchUpdateRecords
,
createNotice
,
executeAction
,
loadRecord
,
loadSubject
,
loadUser
,
queryRecords
,
sendNotice
,
updateRecord
,
getSubjectViewDetail
,
}
from
'@/services/DataService'
;
import
{
getSelectOptions
,
isFieldDisabledByView
}
from
'@/utils/subjectView'
;
import
{
getSelfInspectionFlowUrl
}
from
'@/config/application'
;
import
{
createIntegrityPracticeMaterial
}
from
'./integrityPracticeMaterialService'
;
export
const
RESULT_SUBJECT
=
'self_check_the_project_results'
;
export
const
DRAFT_SUBJECT
=
'self_check_the_results_of_the_working_draft'
;
const
PENDING
=
'未完成'
;
const
COMPLETED
=
'已完成'
;
const
EDITABLE_FIELDS
=
[
'self_inspection_result'
,
'attachment'
];
// Business records can retain results from an earlier push. Only show a
// decision when the corresponding node of THIS workflow has handled it.
export
function
getFlowApprovalStatus
(
messageRecord
,
value
,
final
=
false
)
{
const
metadata
=
messageRecord
?.
metadata
||
{};
const
handled
=
metadata
[
final
?
'sender_status'
:
'auditor_status'
]
===
COMPLETED
;
if
(
!
handled
)
return
'未审批'
;
if
(
metadata
.
editor_status
!==
COMPLETED
)
{
return
value
===
'已驳回'
?
'已驳回'
:
'未审批'
;
}
if
(
final
&&
metadata
.
auditor_status
!==
COMPLETED
)
return
'未审批'
;
return
value
||
'未审批'
;
}
export
function
toFlowId
(
value
)
{
return
typeof
value
===
'object'
&&
value
?
String
(
value
.
_id
||
value
.
name
||
''
)
:
String
(
value
||
''
);
}
export
function
flowIds
(
value
)
{
const
values
=
Array
.
isArray
(
value
)
?
value
:
typeof
value
===
'string'
?
value
.
split
(
','
)
:
value
?
[
value
]
:
[];
return
[...
new
Set
(
values
.
map
(
toFlowId
).
map
((
id
)
=>
id
.
trim
()).
filter
(
Boolean
))];
}
export
function
getFlowPermissions
(
messageRecord
,
currentUser
)
{
const
metadata
=
messageRecord
?.
metadata
||
{};
const
id
=
toFlowId
(
currentUser
);
const
isEditor
=
Boolean
(
id
)
&&
flowIds
(
metadata
.
editor
).
includes
(
id
);
const
isAuditor
=
Boolean
(
id
)
&&
flowIds
(
metadata
.
auditor
).
includes
(
id
);
const
isSender
=
Boolean
(
id
)
&&
toFlowId
(
metadata
.
sender
)
===
id
;
const
step
=
metadata
.
editor_status
!==
COMPLETED
?
'editor'
:
metadata
.
auditor_status
!==
COMPLETED
?
'auditor'
:
metadata
.
sender_status
!==
COMPLETED
?
'sender'
:
'completed'
;
return
{
step
,
isEditor
,
isAuditor
,
isSender
,
canView
:
isEditor
||
isAuditor
||
isSender
,
// Preserve the original integrity page's editing roles, independently of action roles.
canEdit
:
isSender
||
(
isEditor
?
metadata
.
editor_status
!==
COMPLETED
:
isAuditor
&&
(
metadata
.
editor_status
!==
COMPLETED
||
metadata
.
auditor_status
!==
COMPLETED
)),
canSubmit
:
step
===
'editor'
&&
isEditor
,
canWithdraw
:
step
===
'auditor'
&&
isEditor
,
canAudit
:
step
===
'auditor'
&&
isAuditor
,
canFinalAudit
:
step
===
'sender'
&&
isSender
,
};
}
async
function
readMessage
(
messageName
,
currentUser
)
{
if
(
!
messageName
)
throw
new
Error
(
'缺少流程 ID,无法加载自查底稿。'
);
const
messageRecord
=
await
loadRecord
(
messageName
);
if
(
toFlowId
(
messageRecord
?.
subject
?.
name
||
messageRecord
?.
subject
)
!==
'message_reference_id'
||
messageRecord
?.
metadata
?.
subject_name
!==
RESULT_SUBJECT
)
{
throw
new
Error
(
'该链接不是廉洁从业自查底稿流程。'
);
}
const
permissions
=
getFlowPermissions
(
messageRecord
,
currentUser
);
if
(
!
permissions
.
canView
)
throw
new
Error
(
'您不是当前流程的参与人,无法查看或处理该底稿。'
);
if
(
!
flowIds
(
messageRecord
.
metadata
.
restriction_value
).
length
)
{
const
drafts
=
await
Promise
.
all
(
flowIds
(
messageRecord
.
metadata
.
reference_record_id
).
map
((
id
)
=>
loadRecord
(
id
)));
if
(
drafts
.
some
((
draft
)
=>
(
draft
.
subject
?.
name
||
draft
.
subject
)
!==
DRAFT_SUBJECT
))
{
throw
new
Error
(
'流程关联的记录不是自查底稿填写表。'
);
}
messageRecord
.
metadata
.
restriction_value
=
flowIds
(
drafts
.
flatMap
((
draft
)
=>
draft
.
metadata
?.
project_outcome
||
[]));
if
(
!
messageRecord
.
metadata
.
restriction_value
.
length
)
throw
new
Error
(
'当前流程及关联底稿均没有自查项目,请检查底稿的项目结果,无需重复推送。'
);
}
return
{
messageRecord
,
permissions
};
}
async
function
queryRelated
(
subject
,
ids
)
{
const
records
=
[];
// Query in bounded chunks rather than silently omitting rows beyond the API limit.
for
(
let
offset
=
0
;
offset
<
ids
.
length
;
offset
+=
1000
)
{
const
batch
=
ids
.
slice
(
offset
,
offset
+
1000
);
const
result
=
await
queryRecords
({
subject
,
page
:
0
,
page_size
:
1000
,
mode
:
'full'
,
filter
:
{
_id
:
{
$in
:
batch
}
}
});
records
.
push
(...(
result
?.
records
||
[]));
}
const
byId
=
new
Map
(
records
.
map
((
record
)
=>
[
record
.
_id
,
record
]));
if
(
ids
.
some
((
id
)
=>
!
byId
.
has
(
id
)))
throw
new
Error
(
'部分关联记录不存在或无权读取,请联系流程发起人。'
);
return
ids
.
map
((
id
)
=>
byId
.
get
(
id
));
}
export
async
function
loadSelfInspectionFlow
(
messageName
,
currentUser
)
{
const
{
messageRecord
,
permissions
}
=
await
readMessage
(
messageName
,
currentUser
);
const
metadata
=
messageRecord
.
metadata
;
const
participantIds
=
flowIds
([
metadata
.
sender
,
...
flowIds
(
metadata
.
editor
),
...
flowIds
(
metadata
.
auditor
)]);
const
[
subject
,
rows
,
drafts
,
users
,
view
]
=
await
Promise
.
all
([
loadSubject
(
RESULT_SUBJECT
),
queryRelated
(
RESULT_SUBJECT
,
flowIds
(
metadata
.
restriction_value
)),
queryRelated
(
DRAFT_SUBJECT
,
flowIds
(
metadata
.
reference_record_id
)),
Promise
.
all
(
participantIds
.
map
(
async
(
id
)
=>
{
try
{
return
await
loadUser
(
id
);
}
catch
{
return
{
_id
:
id
,
display_name
:
id
};
}
})),
metadata
.
view_name
?
getSubjectViewDetail
(
RESULT_SUBJECT
,
metadata
.
view_name
)
:
Promise
.
resolve
(
null
),
]);
return
{
messageRecord
,
permissions
,
subject
,
rows
,
drafts
,
users
,
view
};
}
async
function
assertAction
(
messageName
,
currentUser
,
permission
)
{
const
flow
=
await
readMessage
(
messageName
,
currentUser
);
if
(
!
flow
.
permissions
[
permission
])
throw
new
Error
(
'流程状态已变化或您无权执行此操作,请刷新后重试。'
);
if
(
!
flowIds
(
flow
.
messageRecord
.
metadata
.
restriction_value
).
length
)
throw
new
Error
(
'该流程没有关联的自查项目,无法提交或审批。'
);
return
flow
.
messageRecord
;
}
export
function
getEditableResultFields
(
subject
,
view
,
currentUser
)
{
return
(
subject
?.
fields
||
[]).
filter
((
field
)
=>
EDITABLE_FIELDS
.
includes
(
field
.
name
)
&&
!
field
.
isPrimaryKey
&&
!
field
.
primary_key
&&
(
view
===
undefined
||
!
isFieldDisabledByView
(
`metadata.
${
field
.
name
}
`
,
view
,
currentUser
)));
}
function
validatePatch
(
patch
,
subject
)
{
const
fields
=
getEditableResultFields
(
subject
);
const
metadata
=
{};
for
(
const
field
of
fields
)
{
if
(
!
Object
.
prototype
.
hasOwnProperty
.
call
(
patch
,
field
.
name
))
continue
;
const
value
=
patch
[
field
.
name
];
const
empty
=
value
==
null
||
(
typeof
value
===
'string'
&&
!
value
.
trim
())
||
(
Array
.
isArray
(
value
)
&&
!
value
.
length
);
if
((
field
.
isRequired
||
field
.
required
)
&&
empty
)
throw
new
Error
(
`请填写
${
field
.
label
||
field
.
name
}
。`
);
if
(
field
.
name
===
'attachment'
)
{
const
files
=
Array
.
isArray
(
value
)
?
value
:
value
?
[
value
]
:
[];
if
(
files
.
some
((
file
)
=>
!
file
?.
url
))
throw
new
Error
(
'附件缺少有效地址,请重新上传。'
);
const
normalized
=
files
.
map
(({
name
,
type
,
size
,
url
})
=>
({
name
,
type
,
size
,
url
}));
metadata
.
attachment
=
field
.
multiple
?
normalized
:
normalized
[
0
]
||
null
;
}
else
{
metadata
[
field
.
name
]
=
String
(
value
??
''
);
}
}
return
metadata
;
}
export
async
function
saveSelfInspectionResults
({
messageName
,
currentUser
,
changes
,
pendingMaterials
=
{}
})
{
const
messageRecord
=
await
assertAction
(
messageName
,
currentUser
,
'canEdit'
);
const
allowedIds
=
new
Set
(
flowIds
(
messageRecord
.
metadata
.
restriction_value
));
const
subject
=
await
loadSubject
(
RESULT_SUBJECT
);
const
view
=
messageRecord
.
metadata
.
view_name
?
await
getSubjectViewDetail
(
RESULT_SUBJECT
,
messageRecord
.
metadata
.
view_name
)
:
null
;
const
writableFields
=
new
Set
(
getEditableResultFields
(
subject
,
view
,
currentUser
).
map
((
field
)
=>
field
.
name
));
const
updates
=
Object
.
entries
(
changes
).
map
(([
id
,
patch
])
=>
{
if
(
!
allowedIds
.
has
(
id
))
throw
new
Error
(
'不能修改当前流程以外的自查项目。'
);
if
(
Object
.
keys
(
patch
).
some
((
field
)
=>
!
writableFields
.
has
(
field
)))
throw
new
Error
(
'当前视图不允许修改该字段,请刷新后重试。'
);
return
{
id
,
metadata
:
validatePatch
(
patch
,
subject
)
};
});
for
(
const
{
id
,
metadata
}
of
updates
)
{
const
retainedUrls
=
new
Set
((
Array
.
isArray
(
metadata
.
attachment
)
?
metadata
.
attachment
:
[
metadata
.
attachment
]).
filter
(
Boolean
).
map
((
file
)
=>
file
.
url
));
for
(
const
material
of
pendingMaterials
[
id
]
||
[])
{
if
(
!
retainedUrls
.
has
(
material
.
fileAsset
?.
url
)
||
material
.
materialRecordId
)
continue
;
const
created
=
await
createIntegrityPracticeMaterial
({
fileAsset
:
material
.
fileAsset
,
currentUser
});
// Keep successful material IDs through a partial failure so retry does not create duplicates.
material
.
materialRecordId
=
created
.
id
;
}
if
(
Object
.
keys
(
metadata
).
length
)
await
updateRecord
(
id
,
{
metadata
});
}
}
async
function
createTasks
(
recipientIds
,
currentUser
,
messageRecord
,
title
)
{
if
(
!
recipientIds
.
length
)
throw
new
Error
(
'流程未配置下一步处理人,请联系发起人。'
);
const
taskIds
=
[];
for
(
const
id
of
recipientIds
)
{
const
recipient
=
await
loadUser
(
id
);
const
response
=
await
executeAction
(
'oa_notice:createProgress'
,
{
title
,
classify
:
'流程待办'
,
contentType
:
'流程待办'
,
receiveUserId
:
id
,
receiveUserName
:
recipient
.
username
,
receiveDisplayName
:
recipient
.
display_name
,
sendUserId
:
currentUser
.
_id
,
sendUserName
:
currentUser
.
username
,
sendDisplayName
:
currentUser
.
display_name
,
returnUrl
:
getSelfInspectionFlowUrl
(
messageRecord
.
name
||
messageRecord
.
_id
),
});
if
(
!
response
?.
data
?.
taskId
)
throw
new
Error
(
'OA 未返回有效的待办 ID,流程尚未完成,请联系管理员核对待办。'
);
taskIds
.
push
(
response
.
data
.
taskId
);
}
return
taskIds
;
}
async
function
completeTasks
(
value
)
{
for
(
const
taskId
of
flowIds
(
value
))
await
executeAction
(
'oa_notice:updateProgressStatus'
,
{
taskId
});
}
async
function
notify
(
messageRecord
,
currentUser
,
recipients
,
title
)
{
const
notice
=
await
createNotice
({
title
,
content
:
`请点击链接处理自查底稿:
${
getSelfInspectionFlowUrl
(
messageRecord
.
name
||
messageRecord
.
_id
)}
`
,
type
:
'internal'
,
from
:
currentUser
.
_id
,
to
:
recipients
,
settings
:
{},
});
await
sendNotice
(
notice
.
name
);
}
async
function
updateBusinessRecords
(
metadata
,
resultData
,
draftData
)
{
if
(
Object
.
keys
(
resultData
).
length
)
{
await
batchUpdateRecords
({
subject
:
RESULT_SUBJECT
,
filter
:
{
_id
:
{
$in
:
flowIds
(
metadata
.
restriction_value
)
}
},
data
:
resultData
});
}
const
draftIds
=
flowIds
(
metadata
.
reference_record_id
);
if
(
draftIds
.
length
&&
Object
.
keys
(
draftData
).
length
)
{
await
batchUpdateRecords
({
subject
:
DRAFT_SUBJECT
,
filter
:
{
_id
:
{
$in
:
draftIds
}
},
data
:
draftData
});
}
}
export
async
function
transitionSelfInspectionFlow
({
messageName
,
currentUser
,
action
})
{
const
permission
=
{
submit
:
'canSubmit'
,
withdraw
:
'canWithdraw'
,
approve
:
'canAudit'
,
reject
:
'canAudit'
,
finalApprove
:
'canFinalAudit'
,
finalReject
:
'canFinalAudit'
,
}[
action
];
if
(
!
permission
)
throw
new
Error
(
'不支持的流程操作。'
);
const
messageRecord
=
await
assertAction
(
messageName
,
currentUser
,
permission
);
const
metadata
=
messageRecord
.
metadata
;
let
patch
;
let
recipients
=
[];
let
title
=
''
;
if
(
action
===
'withdraw'
)
{
await
completeTasks
(
metadata
.
progress_id_auditor
);
patch
=
{
editor_status
:
PENDING
,
auditor_status
:
PENDING
,
progress_id_auditor
:
[]
};
}
else
if
(
action
===
'submit'
)
{
// Validate the persisted complete dataset as well as edited rows.
const
[
subject
,
rows
]
=
await
Promise
.
all
([
loadSubject
(
RESULT_SUBJECT
),
queryRelated
(
RESULT_SUBJECT
,
flowIds
(
metadata
.
restriction_value
)),
]);
rows
.
forEach
((
row
)
=>
validatePatch
(
Object
.
fromEntries
(
EDITABLE_FIELDS
.
map
((
field
)
=>
[
field
,
row
.
metadata
?.[
field
]])),
subject
));
recipients
=
flowIds
(
metadata
.
auditor
);
title
=
'廉洁从业自查底稿待审核'
;
const
tasks
=
await
createTasks
(
recipients
,
currentUser
,
messageRecord
,
title
);
await
updateBusinessRecords
(
metadata
,
{
'metadata.review_people'
:
recipients
,
'metadata.status'
:
'未审批'
,
'metadata.final_status'
:
'未审批'
},
{
'metadata.auditor'
:
recipients
,
'metadata.review_status'
:
'未审批'
,
'metadata.final_review_status'
:
'未审批'
});
await
completeTasks
(
metadata
.
progress_id_editor
);
patch
=
{
editor_status
:
COMPLETED
,
auditor_status
:
PENDING
,
sender_status
:
PENDING
,
progress_id_auditor
:
tasks
};
}
else
if
(
action
===
'approve'
)
{
recipients
=
flowIds
(
metadata
.
sender
);
title
=
'廉洁从业自查底稿待最终审核'
;
const
tasks
=
await
createTasks
(
recipients
,
currentUser
,
messageRecord
,
title
);
await
updateBusinessRecords
(
metadata
,
{
'metadata.status'
:
'已审批'
,
'metadata.review_people'
:
flowIds
(
metadata
.
auditor
)
},
{
'metadata.review_status'
:
'已审批'
,
'metadata.auditor'
:
flowIds
(
metadata
.
auditor
)
});
await
completeTasks
(
metadata
.
progress_id_auditor
);
patch
=
{
auditor_status
:
COMPLETED
,
sender_status
:
PENDING
,
progress_id_sender
:
tasks
[
0
]
};
}
else
if
(
action
===
'finalApprove'
)
{
await
updateBusinessRecords
(
metadata
,
{
'metadata.final_status'
:
'已审批'
},
{
'metadata.final_review_status'
:
'已审批'
});
await
completeTasks
(
metadata
.
progress_id_sender
);
patch
=
{
sender_status
:
COMPLETED
};
}
else
{
const
final
=
action
===
'finalReject'
;
recipients
=
flowIds
(
metadata
.
editor
);
title
=
`廉洁从业自查底稿
${
final
?
'最终审核'
:
'审核'
}
驳回,请重新填写`
;
const
tasks
=
await
createTasks
(
recipients
,
currentUser
,
messageRecord
,
title
);
const
subject
=
await
loadSubject
(
RESULT_SUBJECT
);
const
statusOptions
=
getSelectOptions
(
subject
.
fields
?.
find
((
field
)
=>
field
.
name
===
'status'
));
// Legacy schemas allowed 已驳回; the current snapshot allows only 未审批 / 已审批.
const
rejectedStatus
=
statusOptions
.
some
((
option
)
=>
option
.
value
===
'已驳回'
)
?
'已驳回'
:
'未审批'
;
await
updateBusinessRecords
(
metadata
,
final
?
{
'metadata.final_status'
:
'已驳回'
}
:
{
'metadata.status'
:
rejectedStatus
,
'metadata.review_people'
:
flowIds
(
metadata
.
auditor
)
},
final
?
{
'metadata.final_review_status'
:
'已驳回'
}
:
{
'metadata.review_status'
:
'已驳回'
,
'metadata.auditor'
:
flowIds
(
metadata
.
auditor
)
});
await
completeTasks
(
final
?
metadata
.
progress_id_sender
:
metadata
.
progress_id_auditor
);
patch
=
{
editor_status
:
PENDING
,
auditor_status
:
COMPLETED
,
sender_status
:
final
?
COMPLETED
:
PENDING
,
progress_id_editor
:
tasks
};
}
await
updateRecord
(
messageRecord
.
_id
,
{
metadata
:
patch
});
if
(
recipients
.
length
)
{
try
{
await
notify
(
messageRecord
,
currentUser
,
recipients
,
title
);
}
catch
(
error
)
{
throw
new
Error
(
`流程状态已更新,但站内通知发送失败:
${
error
?.
message
||
'请联系管理员核对通知'
}
。请刷新查看当前状态。
`); }
}
}
src/utils/authRedirect.js
0 → 100644
View file @
93b2457f
import
{
APPLICATION_BASE_PATH
,
ROUTE_TITLES
,
SELF_INSPECTION_FLOW_PATH
}
from
'@/config/application'
;
// Router navigation uses paths without the deployment base; browser navigation
// uses the base exactly once. Keep query/hash intact throughout the round trip.
export
function
normalizeAuthRedirect
(
value
,
origin
=
window
.
location
.
origin
)
{
if
(
!
value
)
return
'/'
;
try
{
const
target
=
new
URL
(
value
,
origin
);
if
(
target
.
origin
!==
origin
)
return
'/'
;
const
base
=
APPLICATION_BASE_PATH
.
replace
(
/
\/
+$/
,
''
);
let
path
=
target
.
pathname
;
if
(
path
===
base
)
path
=
'/'
;
else
if
(
path
.
startsWith
(
`
${
base
}
/`
))
path
=
path
.
slice
(
base
.
length
);
path
=
path
.
replace
(
/
\/
+$/
,
''
)
||
'/'
;
if
(
path
!==
'/'
&&
path
!==
SELF_INSPECTION_FLOW_PATH
&&
!
Object
.
hasOwn
(
ROUTE_TITLES
,
path
))
return
'/'
;
return
`
${
path
}${
target
.
search
}${
target
.
hash
}
`
;
}
catch
{
return
'/'
;
}
}
export
function
getLoginRoute
(
value
,
origin
=
window
.
location
.
origin
)
{
return
`/login?redirect=
${
encodeURIComponent
(
normalizeAuthRedirect
(
value
,
origin
))}
`
;
}
export
function
redirectToLogin
()
{
const
base
=
APPLICATION_BASE_PATH
.
replace
(
/
\/
+$/
,
''
);
// A failed login/captcha request must not reload login or nest redirect URLs.
if
(
window
.
location
.
pathname
.
replace
(
/
\/
+$/
,
''
)
===
`
${
base
}
/login`
||
window
.
location
.
pathname
===
'/login'
)
return
;
window
.
location
.
replace
(
`
${
base
}${
getLoginRoute
(
window
.
location
.
href
)}
`
);
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment