EZQMS-1109: Add signature details for reviews/approvals (#6111)

* ezqms-1109: add signature details for reviews/approvals
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
Alexey Zinoviev 2024-07-23 08:09:54 +04:00 committed by GitHub
parent 93b798c99b
commit 8163a30b77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 99 additions and 47 deletions

View File

@ -14,7 +14,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Ref, SortingOrder } from '@hcengineering/core' import { Ref, SortingOrder } from '@hcengineering/core'
import { Label, Scroller, getUserTimezone } from '@hcengineering/ui' import { Label, Scroller } from '@hcengineering/ui'
import { createQuery } from '@hcengineering/presentation' import { createQuery } from '@hcengineering/presentation'
import documents, { DocumentApprovalRequest, DocumentReviewRequest } from '@hcengineering/controlled-documents' import documents, { DocumentApprovalRequest, DocumentReviewRequest } from '@hcengineering/controlled-documents'
import { employeeByIdStore, personAccountByIdStore } from '@hcengineering/contact-resources' import { employeeByIdStore, personAccountByIdStore } from '@hcengineering/contact-resources'
@ -23,6 +23,7 @@
import documentsRes from '../../plugin' import documentsRes from '../../plugin'
import { $controlledDocument as controlledDocument } from '../../stores/editors/document/editor' import { $controlledDocument as controlledDocument } from '../../stores/editors/document/editor'
import { formatSignatureDate } from '../../utils'
interface Signer { interface Signer {
id?: Ref<Person> id?: Ref<Person>
@ -38,7 +39,6 @@
const reviewQuery = createQuery() const reviewQuery = createQuery()
const approvalQuery = createQuery() const approvalQuery = createQuery()
const timeZone: string = getUserTimezone()
$: if ($controlledDocument !== undefined) { $: if ($controlledDocument !== undefined) {
reviewQuery.query( reviewQuery.query(
@ -92,7 +92,7 @@
id: $controlledDocument.author, id: $controlledDocument.author,
role: 'author', role: 'author',
name: getNameByEmployeeId($controlledDocument.author), name: getNameByEmployeeId($controlledDocument.author),
date: $controlledDocument.createdOn !== undefined ? formatDate($controlledDocument.createdOn) : '' date: $controlledDocument.createdOn !== undefined ? formatSignatureDate($controlledDocument.createdOn) : ''
} }
] ]
@ -105,7 +105,7 @@
id: rAcc?.person, id: rAcc?.person,
role: 'reviewer', role: 'reviewer',
name: getNameByEmployeeId(rAcc?.person), name: getNameByEmployeeId(rAcc?.person),
date: formatDate(date ?? reviewRequest.modifiedOn) date: formatSignatureDate(date ?? reviewRequest.modifiedOn)
}) })
}) })
} }
@ -119,25 +119,12 @@
id: aAcc?.person, id: aAcc?.person,
role: 'approver', role: 'approver',
name: getNameByEmployeeId(aAcc?.person), name: getNameByEmployeeId(aAcc?.person),
date: formatDate(date ?? approvalRequest.modifiedOn) date: formatSignatureDate(date ?? approvalRequest.modifiedOn)
}) })
}) })
} }
} }
function formatDate (date: number): string {
return new Date(date).toLocaleDateString('default', {
year: 'numeric',
month: 'short',
day: 'numeric',
timeZone,
timeZoneName: 'short',
hour: 'numeric',
minute: 'numeric',
second: 'numeric'
})
}
function getSignerLabel (role: 'author' | 'reviewer' | 'approver'): IntlString { function getSignerLabel (role: 'author' | 'reviewer' | 'approver'): IntlString {
switch (role) { switch (role) {
case 'author': case 'author':

View File

@ -2,11 +2,11 @@
import { slide } from 'svelte/transition' import { slide } from 'svelte/transition'
import documents, { DocumentRequest } from '@hcengineering/controlled-documents' import documents, { DocumentRequest } from '@hcengineering/controlled-documents'
import chunter from '@hcengineering/chunter' import chunter from '@hcengineering/chunter'
import { PersonAccount } from '@hcengineering/contact' import { type Person, type PersonAccount } from '@hcengineering/contact'
import { PersonAccountRefPresenter } from '@hcengineering/contact-resources' import { PersonAccountRefPresenter, personAccountByIdStore } from '@hcengineering/contact-resources'
import { Ref } from '@hcengineering/core' import { Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import { Chevron, Label } from '@hcengineering/ui' import { Chevron, Label, tooltip } from '@hcengineering/ui'
import { $documentSnapshots as documentSnapshots } from '../../../stores/editors/document' import { $documentSnapshots as documentSnapshots } from '../../../stores/editors/document'
import documentsRes from '../../../plugin' import documentsRes from '../../../plugin'
@ -14,13 +14,16 @@
import RejectedIcon from '../../icons/Rejected.svelte' import RejectedIcon from '../../icons/Rejected.svelte'
import CancelledIcon from '../../icons/Cancelled.svelte' import CancelledIcon from '../../icons/Cancelled.svelte'
import WaitingIcon from '../../icons/Waiting.svelte' import WaitingIcon from '../../icons/Waiting.svelte'
import SignatureInfo from './SignatureInfo.svelte'
export let request: DocumentRequest export let request: DocumentRequest
export let initiallyExpanded: boolean = false export let initiallyExpanded: boolean = false
interface PersonalApproval { interface PersonalApproval {
account: Ref<PersonAccount> account: Ref<PersonAccount>
person?: Ref<Person>
approved: 'approved' | 'rejected' | 'cancelled' | 'waiting' approved: 'approved' | 'rejected' | 'cancelled' | 'waiting'
timestamp?: number
} }
const client = getClient() const client = getClient()
@ -31,38 +34,45 @@
let rejectingMessage: string | undefined let rejectingMessage: string | undefined
let approvals: PersonalApproval[] = [] let approvals: PersonalApproval[] = []
$: if (request != null) { $: void getRequestData(request)
void getRequestData()
}
$: type = hierarchy.isDerived(request._class, documents.class.DocumentApprovalRequest) $: type = hierarchy.isDerived(request._class, documents.class.DocumentApprovalRequest)
? documents.string.Approval ? documents.string.Approval
: documents.string.Review : documents.string.Review
async function getRequestData (): Promise<void> { async function getRequestData (req: DocumentRequest): Promise<void> {
if (request !== undefined) { if (req == null) {
approvals = await getApprovals(request) return
}
approvals = await getApprovals(req, $personAccountByIdStore)
const rejectingComment = await client.findOne(chunter.class.ChatMessage, { const rejectingComment = await client.findOne(chunter.class.ChatMessage, {
attachedTo: request?._id, attachedTo: req?._id,
attachedToClass: request?._class attachedToClass: req?._class
}) })
rejectingMessage = rejectingComment?.message rejectingMessage = rejectingComment?.message
} }
}
async function getApprovals (req: DocumentRequest): Promise<PersonalApproval[]> { async function getApprovals (
req: DocumentRequest,
accountById: typeof $personAccountByIdStore
): Promise<PersonalApproval[]> {
const rejectedBy: PersonalApproval[] = const rejectedBy: PersonalApproval[] =
req.rejected !== undefined req.rejected !== undefined
? [ ? [
{ {
account: req.rejected, account: req.rejected,
approved: 'rejected' person: accountById.get(req.rejected)?.person,
approved: 'rejected',
timestamp: req.modifiedOn
} }
] ]
: [] : []
const approvedBy: PersonalApproval[] = req.approved.map((id) => ({ const approvedBy: PersonalApproval[] = req.approved.map((id, idx) => ({
account: id, account: id,
approved: 'approved' person: accountById.get(id)?.person,
approved: 'approved',
timestamp: req.approvedDates?.[idx] ?? req.modifiedOn
})) }))
const ignoredBy = req.requested const ignoredBy = req.requested
.filter((p) => p !== req?.rejected) .filter((p) => p !== req?.rejected)
@ -70,6 +80,7 @@
.map( .map(
(id): PersonalApproval => ({ (id): PersonalApproval => ({
account: id, account: id,
person: accountById.get(id)?.person,
approved: req?.rejected !== undefined ? 'cancelled' : 'waiting' approved: req?.rejected !== undefined ? 'cancelled' : 'waiting'
}) })
) )
@ -115,6 +126,19 @@
{#each approvals as approver} {#each approvals as approver}
<div class="approver"> <div class="approver">
<PersonAccountRefPresenter value={approver.account} avatarSize="x-small" /> <PersonAccountRefPresenter value={approver.account} avatarSize="x-small" />
{#key approver.timestamp}
<!-- For some reason tooltip is not interactive w/o remount -->
<span
use:tooltip={approver.timestamp !== undefined
? {
component: SignatureInfo,
props: {
id: approver.person,
timestamp: approver.timestamp
}
}
: undefined}
>
{#if approver.approved === 'approved'} {#if approver.approved === 'approved'}
<ApprovedIcon size="medium" fill={'var(--theme-docs-accepted-color)'} /> <ApprovedIcon size="medium" fill={'var(--theme-docs-accepted-color)'} />
{:else if approver.approved === 'rejected'} {:else if approver.approved === 'rejected'}
@ -124,6 +148,8 @@
{:else if approver.approved === 'waiting'} {:else if approver.approved === 'waiting'}
<WaitingIcon size="medium" /> <WaitingIcon size="medium" />
{/if} {/if}
</span>
{/key}
</div> </div>
{#if rejectingMessage !== undefined && approver.approved === 'rejected'} {#if rejectingMessage !== undefined && approver.approved === 'rejected'}
<div class="reject-message">{@html rejectingMessage}</div> <div class="reject-message">{@html rejectingMessage}</div>

View File

@ -0,0 +1,24 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2022, 2023, 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { formatSignatureDate } from '../../../utils'
export let id: string
export let timestamp: number
</script>
<div>ID: {id}</div>
<div>{formatSignatureDate(timestamp)}</div>

View File

@ -35,7 +35,7 @@ import contact, { type Employee, type PersonAccount } from '@hcengineering/conta
import request, { RequestStatus } from '@hcengineering/request' import request, { RequestStatus } from '@hcengineering/request'
import textEditor from '@hcengineering/text-editor' import textEditor from '@hcengineering/text-editor'
import { isEmptyMarkup } from '@hcengineering/text' import { isEmptyMarkup } from '@hcengineering/text'
import { getEventPositionElement, showPopup, type Location } from '@hcengineering/ui' import { getEventPositionElement, showPopup, getUserTimezone, type Location } from '@hcengineering/ui'
import { type KeyFilter } from '@hcengineering/view' import { type KeyFilter } from '@hcengineering/view'
import chunter from '@hcengineering/chunter' import chunter from '@hcengineering/chunter'
import documents, { import documents, {
@ -835,3 +835,18 @@ export async function createTemplate (space: OrgSpace): Promise<void> {
wizardOpened({ $$currentStep: 'info', location: { space: space._id, project: project ?? documents.ids.NoProject } }) wizardOpened({ $$currentStep: 'info', location: { space: space._id, project: project ?? documents.ids.NoProject } })
showPopup(documents.component.QmsTemplateWizard, {}) showPopup(documents.component.QmsTemplateWizard, {})
} }
export function formatSignatureDate (date: number): string {
const timeZone: string = getUserTimezone()
return new Date(date).toLocaleDateString('default', {
year: 'numeric',
month: 'short',
day: 'numeric',
timeZone,
timeZoneName: 'short',
hour: 'numeric',
minute: 'numeric',
second: 'numeric'
})
}

View File

@ -19,7 +19,7 @@ export class DocumentApprovalsPage extends DocumentCommonPage {
} }
async checkSuccessApproval (approvalName: string): Promise<void> { async checkSuccessApproval (approvalName: string): Promise<void> {
await expect(this.page.locator('svg[fill*="accepted"]').locator('xpath=..').locator('span.ap-label')).toHaveText( await expect(this.page.locator('svg[fill*="accepted"]').locator('xpath=../..').locator('span.ap-label')).toHaveText(
approvalName approvalName
) )
} }