mirror of
https://github.com/hcengineering/platform.git
synced 2025-01-22 11:26:58 +00:00
EZQMS-1317: authors signature on review and approval request & block review bypass (#7631)
Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com>
This commit is contained in:
parent
71d72ba20c
commit
b949744500
@ -44,6 +44,7 @@
|
||||
export let withoutFooter = false
|
||||
export let closeIcon: AnySvelteComponent = Close
|
||||
export let shadow: boolean = false
|
||||
export let className: string = ''
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -66,7 +67,7 @@
|
||||
|
||||
<svelte:element
|
||||
this={isForm ? 'form' : 'div'}
|
||||
class="root"
|
||||
class="root {className}"
|
||||
class:shadow
|
||||
class:embedded
|
||||
on:submit|preventDefault={isForm && shouldSubmitOnEnter ? submit : undefined}
|
||||
|
@ -84,6 +84,8 @@
|
||||
"ProvideRejectionReason": "Type rejection reason...",
|
||||
"RejectionReason": "Rejection reason",
|
||||
"ConfirmReviewCompletion": "Confirm review completion",
|
||||
"ConfirmApprovalSubmission": "Confirm submission for approval",
|
||||
"ConfirmReviewSubmission": "Confirm submission for review",
|
||||
"AddApprovalTitle": "Send your document to approvers",
|
||||
"AddApprovalDescription1": "To prepare your document for approval it should fit few requirements:",
|
||||
"AddApprovalDescription2": "has status 'Draft' or 'Reviewed'",
|
||||
|
@ -76,6 +76,8 @@
|
||||
"ProvideRejectionReason": "Tapez la raison du rejet...",
|
||||
"RejectionReason": "Raison du rejet",
|
||||
"ConfirmReviewCompletion": "Confirmer l'achèvement de la révision",
|
||||
"ConfirmApprovalSubmission": "Confirmer la soumission pour approbation",
|
||||
"ConfirmReviewSubmission": "Confirmer la soumission pour examen",
|
||||
"AddApprovalTitle": "Envoyer votre document aux approuveurs",
|
||||
"AddApprovalDescription1": "Pour préparer votre document à l'approbation, il doit remplir quelques conditions :",
|
||||
"AddApprovalDescription2": "a le statut 'Brouillon' ou 'Révisé'",
|
||||
|
@ -84,6 +84,8 @@
|
||||
"ProvideRejectionReason": "Напишите причину отказа",
|
||||
"RejectionReason": "Причина отказа",
|
||||
"ConfirmReviewCompletion": "Подтвердите окончание рецензии",
|
||||
"ConfirmApprovalSubmission": "Подтвердите отправку на рецензцию",
|
||||
"ConfirmReviewSubmission": "Подтвердите отправку на утверждение",
|
||||
"AddApprovalTitle": "Отправьте ваш документ на утверждение",
|
||||
"AddApprovalDescription1": "Для отправки вашего документа на утверждение, он должен удовлетворять следующим требованиям:",
|
||||
"AddApprovalDescription2": "иметь статус 'Рабочая копия' или 'Рецензии получены'",
|
||||
|
@ -165,7 +165,8 @@
|
||||
|
||||
const teamPopupData: TeamPopupData = {
|
||||
controlledDoc: $controlledDocument,
|
||||
requestClass
|
||||
requestClass,
|
||||
requireSignature: true
|
||||
}
|
||||
|
||||
showPopup(TeamPopup, teamPopupData, 'center')
|
||||
|
@ -109,6 +109,7 @@
|
||||
on:close
|
||||
width="32rem"
|
||||
shadow={true}
|
||||
className={'signature-dialog'}
|
||||
>
|
||||
<div class="flex-col flex-gap-2">
|
||||
<StylishEdit
|
||||
|
@ -6,7 +6,7 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { RequestStatus } from '@hcengineering/request'
|
||||
import { Label, ModernDialog } from '@hcengineering/ui'
|
||||
import { Label, ModernDialog, showPopup } from '@hcengineering/ui'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import contact, { Employee, PersonAccount } from '@hcengineering/contact'
|
||||
import { Class, Ref } from '@hcengineering/core'
|
||||
@ -20,10 +20,12 @@
|
||||
|
||||
import documentsRes from '../plugin'
|
||||
import { sendApprovalRequest, sendReviewRequest } from '../utils'
|
||||
import SignatureDialog from './SignatureDialog.svelte'
|
||||
|
||||
export let controlledDoc: ControlledDocument
|
||||
export let requestClass: Ref<Class<DocumentRequest>>
|
||||
export let readonly: boolean = false
|
||||
export let requireSignature: boolean = false
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
@ -67,13 +69,34 @@
|
||||
let users: Ref<Employee>[] = controlledDoc[docField] ?? []
|
||||
|
||||
async function submit (): Promise<void> {
|
||||
loading = true
|
||||
const complete = async (): Promise<void> => {
|
||||
loading = true
|
||||
|
||||
await sendRequestFunc?.(client, controlledDoc, users)
|
||||
await sendRequestFunc?.(client, controlledDoc, users)
|
||||
|
||||
loading = false
|
||||
loading = false
|
||||
|
||||
dispatch('close')
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
if (requireSignature) {
|
||||
showPopup(
|
||||
SignatureDialog,
|
||||
{
|
||||
confirmationTitle: isReviewRequest
|
||||
? documentsRes.string.ConfirmReviewSubmission
|
||||
: documentsRes.string.ConfirmApprovalSubmission
|
||||
},
|
||||
'center',
|
||||
async (res) => {
|
||||
if (!res) return
|
||||
|
||||
await complete()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
await complete()
|
||||
}
|
||||
}
|
||||
|
||||
$: canSubmit = docRequest === undefined && users.length > 0
|
||||
|
@ -87,12 +87,19 @@
|
||||
return rawName !== undefined ? formatName(rawName) : ''
|
||||
}
|
||||
|
||||
const authorSignDate =
|
||||
reviewRequest !== undefined
|
||||
? reviewRequest.createdOn
|
||||
: approvalRequest !== undefined
|
||||
? approvalRequest.createdOn
|
||||
: $controlledDocument.createdOn
|
||||
|
||||
signers = [
|
||||
{
|
||||
id: $controlledDocument.author,
|
||||
role: 'author',
|
||||
name: getNameByEmployeeId($controlledDocument.author),
|
||||
date: $controlledDocument.createdOn !== undefined ? formatSignatureDate($controlledDocument.createdOn) : ''
|
||||
date: authorSignDate !== undefined ? formatSignatureDate(authorSignDate) : ''
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -116,9 +116,11 @@ export default mergeIds(documentsId, documents, {
|
||||
|
||||
ConfirmApproval: '' as IntlString,
|
||||
ConfirmRejection: '' as IntlString,
|
||||
ConfirmApprovalSubmission: '' as IntlString,
|
||||
ProvideRejectionReason: '' as IntlString,
|
||||
RejectionReason: '' as IntlString,
|
||||
ConfirmReviewCompletion: '' as IntlString,
|
||||
ConfirmReviewSubmission: '' as IntlString,
|
||||
AddApprovalTitle: '' as IntlString,
|
||||
AddApprovalDescription1: '' as IntlString,
|
||||
AddApprovalDescription2: '' as IntlString,
|
||||
|
@ -64,6 +64,10 @@ export const documentAllVersionsUpdated = createEvent<ControlledDocument[]>(
|
||||
|
||||
export const reviewRequestUpdated = createEvent<DocumentReviewRequest>(generateActionName('reviewRequestUpdated'))
|
||||
|
||||
export const reviewRequestHistoryUpdated = createEvent<DocumentReviewRequest[]>(
|
||||
generateActionName('reviewRequestHistoryUpdated')
|
||||
)
|
||||
|
||||
export const approvalRequestUpdated = createEvent<DocumentApprovalRequest>(generateActionName('approvalRequestUpdated'))
|
||||
|
||||
export const editorModeUpdated = createEvent<EditorMode>(generateActionName('editorModeUpdated'))
|
||||
|
@ -17,17 +17,24 @@ import { ControlledDocumentState, DocumentState } from '@hcengineering/controlle
|
||||
import { TrainingState } from '@hcengineering/training'
|
||||
import { combine } from 'effector'
|
||||
import { $documentComments } from './documentComments'
|
||||
import { $documentState, $isLatestVersion, $training } from './editor'
|
||||
import { $controlledDocument, $documentState, $isLatestVersion, $reviewRequestHistory, $training } from './editor'
|
||||
|
||||
export const $canSendForApproval = combine(
|
||||
$controlledDocument,
|
||||
$isLatestVersion,
|
||||
$documentState,
|
||||
$documentComments,
|
||||
$training,
|
||||
(isLatestVersion, state, comments, training) => {
|
||||
$reviewRequestHistory,
|
||||
(document, isLatestVersion, state, comments, training, reviewHistory) => {
|
||||
let haveBeenReviewedOnce = false
|
||||
if (document !== null) {
|
||||
const reviews = (reviewHistory ?? []).filter((review) => review.attachedTo === document._id)
|
||||
if (reviews.length > 0) haveBeenReviewedOnce = true
|
||||
}
|
||||
return (
|
||||
isLatestVersion &&
|
||||
(state === DocumentState.Draft || state === ControlledDocumentState.Reviewed) &&
|
||||
((state === DocumentState.Draft && !haveBeenReviewedOnce) || state === ControlledDocumentState.Reviewed) &&
|
||||
comments.every((comment) => comment.resolved) &&
|
||||
(training === null || training.state === TrainingState.Released)
|
||||
)
|
||||
|
@ -43,6 +43,7 @@ import {
|
||||
documentAllVersionsUpdated,
|
||||
editorModeUpdated,
|
||||
reviewRequestUpdated,
|
||||
reviewRequestHistoryUpdated,
|
||||
rightPanelTabChanged,
|
||||
documentSnapshotsUpdated,
|
||||
trainingUpdated,
|
||||
@ -122,6 +123,10 @@ export const $reviewRequest = createStore<DocumentReviewRequest | null>(null)
|
||||
.on(reviewRequestUpdated, (_, payload) => payload)
|
||||
.reset(controlledDocumentClosed)
|
||||
|
||||
export const $reviewRequestHistory = createStore<DocumentReviewRequest[] | null>(null)
|
||||
.on(reviewRequestHistoryUpdated, (_, payload) => payload)
|
||||
.reset(controlledDocumentClosed)
|
||||
|
||||
export const $approvalRequest = createStore<DocumentApprovalRequest | null>(null)
|
||||
.on(approvalRequestUpdated, (_, payload) => payload)
|
||||
.reset(controlledDocumentClosed)
|
||||
|
@ -39,7 +39,8 @@ import {
|
||||
documentSnapshotsUpdated,
|
||||
trainingUpdated,
|
||||
projectUpdated,
|
||||
projectDocumentsUpdated
|
||||
projectDocumentsUpdated,
|
||||
reviewRequestHistoryUpdated
|
||||
} from './actions'
|
||||
import { $documentCommentsFilter } from './documentComments'
|
||||
import { $controlledDocument, $documentTraining } from './editor'
|
||||
@ -48,6 +49,7 @@ const controlledDocumentQuery = createQuery(true)
|
||||
const documentVersionsQuery = createQuery(true)
|
||||
const documentSnapshotsQuery = createQuery(true)
|
||||
const reviewRequestQuery = createQuery(true)
|
||||
const reviewRequestHistoryQuery = createQuery(true)
|
||||
const approvalRequestQuery = createQuery(true)
|
||||
const documentCommentsQuery = createQuery(true)
|
||||
const workingCopyMetadataQuery = createQuery(true)
|
||||
@ -125,6 +127,25 @@ const queryReviewRequestFx = createEffect(
|
||||
}
|
||||
)
|
||||
|
||||
const queryReviewRequestHistoryFx = createEffect(
|
||||
(payload: { _id: Ref<ControlledDocument>, _class: Ref<Class<ControlledDocument>> }) => {
|
||||
const { _id, _class } = payload
|
||||
if (_id == null || _class == null) {
|
||||
reviewRequestHistoryQuery.unsubscribe()
|
||||
return
|
||||
}
|
||||
reviewRequestHistoryQuery.query(
|
||||
documents.class.DocumentReviewRequest,
|
||||
{ attachedTo: _id, attachedToClass: _class },
|
||||
(result) => {
|
||||
if (result !== null && result !== undefined && result.length > 0) {
|
||||
reviewRequestHistoryUpdated(result)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const queryApprovalRequestFx = createEffect(
|
||||
(payload: { _id: Ref<ControlledDocument>, _class: Ref<Class<ControlledDocument>> }) => {
|
||||
const { _id, _class } = payload
|
||||
@ -259,6 +280,7 @@ forward({
|
||||
to: [
|
||||
queryControlledDocumentFx,
|
||||
queryReviewRequestFx,
|
||||
queryReviewRequestHistoryFx,
|
||||
queryApprovalRequestFx,
|
||||
querySavedAttachmentsFx,
|
||||
queryProjectFx,
|
||||
|
@ -185,6 +185,7 @@ export async function getDocumentMetaLinkFragment (document: Doc): Promise<Locat
|
||||
export interface TeamPopupData {
|
||||
controlledDoc: ControlledDocument
|
||||
requestClass: Ref<Class<DocumentRequest>>
|
||||
requireSignature?: boolean
|
||||
}
|
||||
|
||||
export async function sendReviewRequest (
|
||||
|
@ -226,7 +226,7 @@ test.describe('QMS. PDF Download and Preview', () => {
|
||||
const documentContentPageSecond = new DocumentContentPage(userSecondPage)
|
||||
const documentsPageSecond = new DocumentsPage(userSecondPage)
|
||||
await documentsPageSecond.openDocument(approveDocument.title)
|
||||
await documentContentPageSecond.clickApproveButton()
|
||||
await documentContentPageSecond.confirmApproval()
|
||||
await documentsPageSecond.openDocument(approveDocument.title)
|
||||
})
|
||||
await test.step('5. check if reviewers and approvers are visible', async () => {
|
||||
@ -275,7 +275,7 @@ test.describe('QMS. PDF Download and Preview', () => {
|
||||
const documentContentPageSecond = new DocumentContentPage(userSecondPage)
|
||||
const documentsPageSecond = new DocumentsPage(userSecondPage)
|
||||
await documentsPageSecond.openDocument(approveDocument.title)
|
||||
await documentContentPageSecond.clickApproveButton()
|
||||
await documentContentPageSecond.confirmApproval()
|
||||
await documentsPageSecond.openDocument(approveDocument.title)
|
||||
})
|
||||
await test.step('5. check if reviewers and approvers are visible', async () => {
|
||||
|
@ -926,7 +926,29 @@ test.describe('QMS. Documents tests', () => {
|
||||
await attachScreenshot('TESTS-206_check_document.png', page)
|
||||
})
|
||||
|
||||
await test.step('7. Send for Approval', async () => {
|
||||
await test.step('7. Send for Review', async () => {
|
||||
await documentContentPage.buttonSendForReview.click()
|
||||
await documentContentPage.fillSelectReviewersForm([])
|
||||
await documentContentPage.checkDocumentStatus(DocumentStatus.IN_REVIEW)
|
||||
await documentContentPage.checkDocument({
|
||||
...documentDetails,
|
||||
status: DocumentStatus.IN_REVIEW
|
||||
})
|
||||
await attachScreenshot('TESTS-206_send_for_review_2.png', page)
|
||||
})
|
||||
|
||||
await test.step('8. Complete Review', async () => {
|
||||
const documentContentPageSecond = new DocumentContentPage(userSecondPage)
|
||||
|
||||
await documentContentPageSecond.completeReview()
|
||||
|
||||
await documentContentPageSecond.checkDocumentStatus(DocumentStatus.REVIEWED)
|
||||
await documentContentPageSecond.checkCurrentRights(DocumentRights.VIEWING)
|
||||
|
||||
await attachScreenshot('TESTS-206_complete_review_2.png', page)
|
||||
})
|
||||
|
||||
await test.step('9. Send for Approval', async () => {
|
||||
await documentContentPage.buttonSendForApproval.click()
|
||||
await documentContentPage.fillSelectApproversForm([reviewer])
|
||||
await documentContentPage.checkDocumentStatus(DocumentStatus.IN_APPROVAL)
|
||||
@ -938,7 +960,7 @@ test.describe('QMS. Documents tests', () => {
|
||||
await documentContentPage.checkCurrentRights(DocumentRights.VIEWING)
|
||||
})
|
||||
|
||||
await test.step('8. Approve document', async () => {
|
||||
await test.step('10. Approve document', async () => {
|
||||
const documentsPageSecond = new DocumentsPage(userSecondPage)
|
||||
await documentsPageSecond.openDocument(completeDocument.title)
|
||||
|
||||
@ -957,7 +979,7 @@ test.describe('QMS. Documents tests', () => {
|
||||
await attachScreenshot('TESTS-206_approve_document.png', page)
|
||||
})
|
||||
|
||||
await test.step('9. Check document', async () => {
|
||||
await test.step('11. Check document', async () => {
|
||||
await documentContentPage.checkDocumentStatus(DocumentStatus.EFFECTIVE)
|
||||
await documentContentPage.checkDocument({
|
||||
...documentDetails,
|
||||
@ -969,7 +991,7 @@ test.describe('QMS. Documents tests', () => {
|
||||
await attachScreenshot('TESTS-206_check_document.png', page)
|
||||
})
|
||||
|
||||
await test.step('10. Check History tab', async () => {
|
||||
await test.step('12. Check History tab', async () => {
|
||||
await documentContentPage.buttonHistoryTab.first().click()
|
||||
|
||||
const documentHistoryPage = new DocumentHistoryPage(page)
|
||||
|
@ -31,6 +31,7 @@ export class DocumentContentPage extends DocumentCommonPage {
|
||||
readonly buttonCompleteReview: Locator
|
||||
readonly inputPassword: Locator
|
||||
readonly buttonSubmit: Locator
|
||||
readonly buttonSubmitSignature: Locator
|
||||
readonly buttonReject: Locator
|
||||
readonly inputRejectionReason: Locator
|
||||
readonly buttonApprove: Locator
|
||||
@ -137,6 +138,7 @@ export class DocumentContentPage extends DocumentCommonPage {
|
||||
})
|
||||
this.inputPassword = page.locator('input[name="documents:string:Password"]')
|
||||
this.buttonSubmit = page.locator('div.popup button[type="submit"]')
|
||||
this.buttonSubmitSignature = page.locator('.signature-dialog button[type="submit"]')
|
||||
this.buttonReject = page.locator('button[type="button"] > span', { hasText: 'Reject' })
|
||||
this.inputRejectionReason = page.locator('div.popup div[id="rejection-reason"] input')
|
||||
this.buttonApprove = page.locator('button[type="button"] > span', { hasText: 'Approve' })
|
||||
@ -727,6 +729,7 @@ export class DocumentContentPage extends DocumentCommonPage {
|
||||
}
|
||||
await this.textSelectReviewersPopup.click({ force: true })
|
||||
await this.buttonSelectMemberSubmit.click()
|
||||
await this.confirmSubmission()
|
||||
}
|
||||
|
||||
async fillSelectApproversForm (approvers: Array<string>): Promise<void> {
|
||||
@ -736,6 +739,7 @@ export class DocumentContentPage extends DocumentCommonPage {
|
||||
}
|
||||
await this.textSelectApproversPopup.click({ force: true })
|
||||
await this.buttonSelectMemberSubmit.click()
|
||||
await this.confirmSubmission()
|
||||
}
|
||||
|
||||
async checkCurrentRights (right: DocumentRights): Promise<void> {
|
||||
@ -772,6 +776,7 @@ export class DocumentContentPage extends DocumentCommonPage {
|
||||
await this.addReasonAndImpactToTheDocument(reason, impact)
|
||||
await this.buttonSendForApproval.click()
|
||||
await this.buttonSelectMemberSubmit.click()
|
||||
await this.confirmSubmission()
|
||||
await this.checkDocumentStatus(DocumentStatus.IN_APPROVAL)
|
||||
await this.checkDocument({
|
||||
...documentDetails,
|
||||
@ -817,6 +822,11 @@ export class DocumentContentPage extends DocumentCommonPage {
|
||||
await this.buttonSubmit.click()
|
||||
}
|
||||
|
||||
async confirmSubmission (): Promise<void> {
|
||||
await this.inputPassword.fill(PlatformPassword)
|
||||
await this.buttonSubmitSignature.click()
|
||||
}
|
||||
|
||||
async changeCurrentRight (newRight: DocumentRights): Promise<void> {
|
||||
await this.buttonCurrentRights.click()
|
||||
await this.selectMenuItem(this.page, newRight)
|
||||
|
Loading…
Reference in New Issue
Block a user