mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-25 05:42:23 +02:00
429 lines
13 KiB
Svelte
429 lines
13 KiB
Svelte
<!--
|
|
// Copyright © 2023 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 { DateRangeMode, type MixinUpdate, Timestamp } from '@hcengineering/core'
|
|
import { IntlString } from '@hcengineering/platform'
|
|
import {
|
|
DatePresenter,
|
|
DropdownLabels,
|
|
Label,
|
|
DropdownTextItem,
|
|
RadioButton,
|
|
Scroller,
|
|
Toggle
|
|
} from '@hcengineering/ui'
|
|
import { getClient } from '@hcengineering/presentation'
|
|
import { UserBoxItems } from '@hcengineering/contact-resources'
|
|
import {
|
|
type Document,
|
|
ControlledDocumentState,
|
|
DEFAULT_PERIODIC_REVIEW_INTERVAL,
|
|
DocumentState,
|
|
type DocumentTraining,
|
|
periodicReviewIntervals,
|
|
ControlledDocument
|
|
} from '@hcengineering/controlled-documents'
|
|
import {
|
|
NullablePositiveNumberEditor,
|
|
TrainingRefEditor,
|
|
TrainingRequestRolesEditor
|
|
} from '@hcengineering/training-resources'
|
|
import { createDocumentTraining, getDocumentTrainingClass, updateDocumentTraining } from '../../docutils'
|
|
|
|
import documentsRes from '../../plugin'
|
|
import {
|
|
$isDocumentOwner as isDocumentOwner,
|
|
$controlledDocument as controlledDocument,
|
|
$documentState as documentState,
|
|
$documentAllVersionsDescSorted as documentAllVersionsDescSorted,
|
|
$documentTraining as documentTraining
|
|
} from '../../stores/editors/document/editor'
|
|
|
|
enum Severity {
|
|
Minor = 'minor',
|
|
Major = 'major'
|
|
}
|
|
|
|
const client = getClient()
|
|
|
|
async function changePlannedEffectiveDate (plannedEffectiveDate: Timestamp) {
|
|
if (!$controlledDocument) {
|
|
return
|
|
}
|
|
|
|
await client.update($controlledDocument, { plannedEffectiveDate })
|
|
}
|
|
|
|
let selectedDate: Timestamp =
|
|
$controlledDocument?.plannedEffectiveDate != null && $controlledDocument?.plannedEffectiveDate > 0
|
|
? $controlledDocument.plannedEffectiveDate
|
|
: Date.now()
|
|
|
|
let selected: IntlString | undefined = undefined
|
|
if ($controlledDocument?.plannedEffectiveDate === 0) {
|
|
selected = documentsRes.string.EffectiveImmediately
|
|
} else if ($controlledDocument?.plannedEffectiveDate != null) {
|
|
selected = documentsRes.string.EffectiveOn
|
|
}
|
|
|
|
async function changeSelectedDate (ev: CustomEvent) {
|
|
if (ev.detail !== undefined) {
|
|
selectedDate = ev.detail
|
|
await changePlannedEffectiveDate(ev.detail)
|
|
selected = documentsRes.string.EffectiveOn
|
|
}
|
|
}
|
|
|
|
const reviewIntervals: DropdownTextItem[] = []
|
|
for (const interval of periodicReviewIntervals) {
|
|
reviewIntervals.push({
|
|
id: interval.toString(),
|
|
label: interval.toString()
|
|
})
|
|
}
|
|
|
|
let selectedReviewInterval = $controlledDocument?.reviewInterval
|
|
? $controlledDocument.reviewInterval.toString()
|
|
: DEFAULT_PERIODIC_REVIEW_INTERVAL.toString()
|
|
async function changePeriodicReviewInterval (interval?: string) {
|
|
if ($controlledDocument == null || interval == null) {
|
|
return
|
|
}
|
|
|
|
const reviewInterval = Number(+interval)
|
|
if (!Number.isSafeInteger(reviewInterval)) {
|
|
return
|
|
}
|
|
|
|
await client.update($controlledDocument, { reviewInterval })
|
|
selectedReviewInterval = interval.toString()
|
|
}
|
|
|
|
$: canEdit =
|
|
$isDocumentOwner &&
|
|
$documentState != null &&
|
|
![
|
|
DocumentState.Archived,
|
|
DocumentState.Deleted,
|
|
DocumentState.Effective,
|
|
ControlledDocumentState.InApproval,
|
|
ControlledDocumentState.Approved,
|
|
ControlledDocumentState.ToReview
|
|
].includes($documentState)
|
|
|
|
const hierarchy = client.getHierarchy()
|
|
const documentTrainingClass = getDocumentTrainingClass(hierarchy)
|
|
|
|
async function toggleTraining (on: boolean): Promise<void> {
|
|
if (!$controlledDocument || !canEdit) {
|
|
return
|
|
}
|
|
|
|
if (on) {
|
|
if ($documentTraining === null) {
|
|
await createDocumentTraining(client, $controlledDocument, {
|
|
enabled: true,
|
|
training: null,
|
|
roles: [],
|
|
trainees: [],
|
|
maxAttempts: null,
|
|
dueDays: null
|
|
})
|
|
} else {
|
|
await updateTraining({ enabled: true })
|
|
}
|
|
} else {
|
|
await updateTraining({ enabled: false })
|
|
}
|
|
}
|
|
|
|
async function updateTraining (update: MixinUpdate<Document, DocumentTraining>): Promise<boolean> {
|
|
if (!$controlledDocument || !canEdit || !documentTraining) {
|
|
return false
|
|
}
|
|
await updateDocumentTraining(client, $controlledDocument, update)
|
|
return true
|
|
}
|
|
|
|
$: severity = getSeverity($controlledDocument, $documentAllVersionsDescSorted)
|
|
|
|
function getPreviousDocument (
|
|
document: ControlledDocument,
|
|
allVersionsDesc: ControlledDocument[]
|
|
): ControlledDocument | undefined {
|
|
return allVersionsDesc.find(
|
|
(d) => (d.major === document.major && d.minor < document.minor) || d.major < document.major
|
|
)
|
|
}
|
|
|
|
function getSeverity (document: ControlledDocument | null, allVersionsDesc: ControlledDocument[]): Severity {
|
|
if (document == null) {
|
|
return Severity.Minor
|
|
}
|
|
|
|
const prevDocument = getPreviousDocument(document, allVersionsDesc)
|
|
|
|
if (prevDocument == null) {
|
|
return document.major > 0 ? Severity.Major : Severity.Minor
|
|
} else {
|
|
return prevDocument.major < document.major ? Severity.Major : Severity.Minor
|
|
}
|
|
}
|
|
|
|
function getVersionForSeverity (severity: Severity): { major: number, minor: number } | undefined {
|
|
if ($controlledDocument == null) {
|
|
return
|
|
}
|
|
|
|
const prevDocument = getPreviousDocument($controlledDocument, $documentAllVersionsDescSorted)
|
|
|
|
if (severity === Severity.Major) {
|
|
return {
|
|
major: (prevDocument?.major ?? 0) + 1,
|
|
minor: 0
|
|
}
|
|
} else {
|
|
return {
|
|
major: prevDocument?.major ?? 0,
|
|
minor: (prevDocument?.minor ?? 0) + 1
|
|
}
|
|
}
|
|
}
|
|
|
|
async function handleSeverityChanged (newSeverity: Severity): Promise<void> {
|
|
if (!canEdit || $controlledDocument == null) {
|
|
return
|
|
}
|
|
|
|
const oldSeverity = getSeverity($controlledDocument, $documentAllVersionsDescSorted)
|
|
if (oldSeverity === newSeverity) {
|
|
return
|
|
}
|
|
|
|
const versionUpdate = getVersionForSeverity(newSeverity)
|
|
if (versionUpdate === undefined) {
|
|
return
|
|
}
|
|
|
|
await client.update($controlledDocument, versionUpdate)
|
|
}
|
|
</script>
|
|
|
|
<Scroller>
|
|
<div class="content">
|
|
<section class="section">
|
|
<header class="fs-title text-lg mb-4">
|
|
<Label label={documentsRes.string.ChangeSeverity} />
|
|
</header>
|
|
<div class="flex-col">
|
|
<RadioButton
|
|
group={severity}
|
|
id={Severity.Minor}
|
|
labelIntl={documentsRes.string.Minor}
|
|
labelGap="large"
|
|
value={Severity.Minor}
|
|
disabled={!canEdit}
|
|
action={() => {
|
|
void handleSeverityChanged(Severity.Minor)
|
|
}}
|
|
gap="large"
|
|
/>
|
|
<RadioButton
|
|
group={severity}
|
|
id={Severity.Major}
|
|
labelIntl={documentsRes.string.Major}
|
|
labelGap="large"
|
|
value={Severity.Major}
|
|
disabled={!canEdit}
|
|
action={() => {
|
|
void handleSeverityChanged(Severity.Major)
|
|
}}
|
|
gap="none"
|
|
/>
|
|
</div>
|
|
<header class="fs-title text-lg my-4">
|
|
<Label label={documentsRes.string.EffectiveDocumentLifecycle} />
|
|
</header>
|
|
<span class="fs-title text-normal">
|
|
<Label label={documentsRes.string.EffectiveDate} />
|
|
</span>
|
|
<div>
|
|
<div class="flex-col">
|
|
<RadioButton
|
|
bind:group={selected}
|
|
id={documentsRes.string.EffectiveImmediately}
|
|
labelIntl={documentsRes.string.EffectiveImmediately}
|
|
labelGap="large"
|
|
value={documentsRes.string.EffectiveImmediately}
|
|
disabled={!canEdit}
|
|
action={() => {
|
|
selected = documentsRes.string.EffectiveImmediately
|
|
changePlannedEffectiveDate(0)
|
|
}}
|
|
gap="large"
|
|
/>
|
|
<RadioButton
|
|
bind:group={selected}
|
|
id={documentsRes.string.EffectiveOn}
|
|
labelGap="large"
|
|
value={documentsRes.string.EffectiveOn}
|
|
disabled={!canEdit}
|
|
action={() => {
|
|
selected = documentsRes.string.EffectiveOn
|
|
changePlannedEffectiveDate(selectedDate)
|
|
}}
|
|
gap="none"
|
|
>
|
|
<div class="flex-row-center flex-gap-1">
|
|
<Label label={documentsRes.string.EffectiveOn} />
|
|
<DatePresenter
|
|
mode={DateRangeMode.DATETIME}
|
|
bind:value={selectedDate}
|
|
on:change={changeSelectedDate}
|
|
editable={canEdit}
|
|
/>
|
|
</div>
|
|
</RadioButton>
|
|
</div>
|
|
</div>
|
|
<div class="flex-row-center flex-gap-2">
|
|
<span class="whitespace-nowrap fs-title text-normal">
|
|
<Label label={documentsRes.string.PeriodicReviewToBeCompleted} />
|
|
</span>
|
|
<DropdownLabels
|
|
kind="regular"
|
|
selected={selectedReviewInterval}
|
|
placeholder={documentsRes.string.PeriodicReviewToBeCompleted}
|
|
items={reviewIntervals}
|
|
enableSearch={false}
|
|
multiselect={false}
|
|
allowDeselect={false}
|
|
on:selected={({ detail }) => changePeriodicReviewInterval(detail)}
|
|
disabled={!canEdit}
|
|
/>
|
|
<span>
|
|
<Label label={documentsRes.string.MonthsAfterEffectiveDate} />
|
|
</span>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="section pb-16">
|
|
<header class="flex-row-center mb-4 flex-gap-4">
|
|
<span class="fs-title text-lg">
|
|
<Label label={documentTrainingClass.label} />
|
|
</span>
|
|
|
|
<Toggle
|
|
disabled={!canEdit}
|
|
on={$documentTraining?.enabled}
|
|
on:change={(event) => toggleTraining(event.detail)}
|
|
/>
|
|
</header>
|
|
|
|
{#if $documentTraining !== null && $documentTraining.enabled}
|
|
{@const trainingAttribute = hierarchy.getAttribute(documentTrainingClass._id, 'training')}
|
|
<span class="fs-title text-normal">
|
|
<Label label={trainingAttribute.label} />
|
|
</span>
|
|
<TrainingRefEditor
|
|
kind="regular"
|
|
width="min-content"
|
|
size="medium"
|
|
readonly={!canEdit}
|
|
value={$documentTraining.training}
|
|
onChange={(trainingRef) => {
|
|
void updateTraining({ training: trainingRef ?? null })
|
|
}}
|
|
/>
|
|
|
|
{@const rolesAttribute = hierarchy.getAttribute(documentTrainingClass._id, 'roles')}
|
|
<span class="fs-title text-normal">
|
|
<Label label={rolesAttribute.label} />
|
|
</span>
|
|
<TrainingRequestRolesEditor
|
|
kind="regular"
|
|
width="max-content"
|
|
value={$documentTraining.roles}
|
|
onChange={(roles) => {
|
|
void updateTraining({ roles })
|
|
}}
|
|
/>
|
|
|
|
{@const traineesAttribute = hierarchy.getAttribute(documentTrainingClass._id, 'trainees')}
|
|
<span class="fs-title text-normal">
|
|
<Label label={traineesAttribute.label} />
|
|
</span>
|
|
<UserBoxItems
|
|
items={$documentTraining.trainees}
|
|
label={traineesAttribute.label}
|
|
readonly={!canEdit}
|
|
size="card"
|
|
on:update={(event) => {
|
|
void updateTraining({ trainees: event.detail })
|
|
}}
|
|
/>
|
|
|
|
<div class="flex-row-center flex-gap-2">
|
|
<span class="whitespace-nowrap fs-title text-normal">
|
|
<Label label={documentsRes.string.ToBePassedWithin} />
|
|
</span>
|
|
<NullablePositiveNumberEditor
|
|
kind="regular"
|
|
width="min-content"
|
|
value={$documentTraining.maxAttempts}
|
|
readonly={!canEdit}
|
|
onChange={(maxAttempts) => {
|
|
void updateTraining({ maxAttempts })
|
|
}}
|
|
/>
|
|
<Label label={documentsRes.string.AttemptsAnd} />
|
|
<NullablePositiveNumberEditor
|
|
kind="regular"
|
|
width="min-content"
|
|
value={$documentTraining.dueDays}
|
|
readonly={!canEdit}
|
|
onChange={(dueDays) => {
|
|
void updateTraining({ dueDays })
|
|
}}
|
|
/>
|
|
<Label label={documentsRes.string.DaysAfterEffectiveDate} />
|
|
</div>
|
|
{/if}
|
|
</section>
|
|
</div>
|
|
</Scroller>
|
|
|
|
<style lang="scss">
|
|
.content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex-wrap: nowrap;
|
|
gap: 2rem;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
padding: 1.5rem 3.25rem 4rem;
|
|
}
|
|
|
|
.section {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex-shrink: 0;
|
|
flex-wrap: nowrap;
|
|
gap: 1rem;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
}
|
|
</style>
|