This commit is contained in:
@@ -13,8 +13,11 @@ import {
|
|||||||
searchOwnRequests,
|
searchOwnRequests,
|
||||||
} from './myRequests.service';
|
} from './myRequests.service';
|
||||||
import { IMyRequest } from './models/IMyRequest';
|
import { IMyRequest } from './models/IMyRequest';
|
||||||
import { DataTable, IDataTableColumn } from '@tnsu/ui-kit';
|
import { DataTable, IDataTableColumn, IconButton } from '@tnsu/ui-kit';
|
||||||
import { REQUEST_STATUS, REQUEST_STATUS_CHOICES, IRequestType, StatusBadge } from '../../shared/approval';
|
import { REQUEST_STATUS, REQUEST_STATUS_CHOICES, IRequestType, StatusBadge } from '../../shared/approval';
|
||||||
|
import { confirmAndDeleteDraft, isDeletableStatus } from '../../shared/draftDeletion';
|
||||||
|
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||||
|
import { getFormComponent } from '../../forms/registry';
|
||||||
import { onRequestsChanged } from '../../shared/requestEvents';
|
import { onRequestsChanged } from '../../shared/requestEvents';
|
||||||
import { perfScope } from '../../shared/perfTrace';
|
import { perfScope } from '../../shared/perfTrace';
|
||||||
import { useRoutedQuery } from '../../shared/usePersistedQuery';
|
import { useRoutedQuery } from '../../shared/usePersistedQuery';
|
||||||
@@ -197,12 +200,23 @@ function formatDate(iso: string): string {
|
|||||||
// (доступ с клавиатуры: Enter/Space).
|
// (доступ с клавиатуры: Enter/Space).
|
||||||
//
|
//
|
||||||
// Набор колонок зависит от раздела и от фильтра — см. columnsFor.
|
// Набор колонок зависит от раздела и от фильтра — см. columnsFor.
|
||||||
|
/**
|
||||||
|
* Можно ли удалить строку прямо из таблицы: своя заявка в статусе «Черновик»
|
||||||
|
* и форма этого типа есть в портале (чужие типы из approval-route портал не
|
||||||
|
* открывает и не удаляет).
|
||||||
|
*/
|
||||||
|
function canDeleteRow(r: IMyRequest): boolean {
|
||||||
|
return r.kind === 'own' && isDeletableStatus(r.status) && !!getFormComponent(r.formKey);
|
||||||
|
}
|
||||||
|
|
||||||
const Table: React.FC<{
|
const Table: React.FC<{
|
||||||
rows: IMyRequest[];
|
rows: IMyRequest[];
|
||||||
view: ListView;
|
view: ListView;
|
||||||
statusFilter: string;
|
statusFilter: string;
|
||||||
onOpen: (request: IMyRequest) => void;
|
onOpen: (request: IMyRequest) => void;
|
||||||
}> = ({ rows, view, statusFilter, onOpen }) => {
|
/** Удалить черновик; задан только там, где в таблице бывают свои черновики. */
|
||||||
|
onDelete?: (request: IMyRequest) => void;
|
||||||
|
}> = ({ rows, view, statusFilter, onOpen, onDelete }) => {
|
||||||
const cols = columnsFor(view, statusFilter);
|
const cols = columnsFor(view, statusFilter);
|
||||||
|
|
||||||
// Колонки для DataTable строим динамически: базовые всегда, автор/статус/
|
// Колонки для DataTable строим динамически: базовые всегда, автор/статус/
|
||||||
@@ -241,6 +255,27 @@ const Table: React.FC<{
|
|||||||
render: (r) => r.decisionComment || <span className={styles.commentEmpty}>Без комментария</span>
|
render: (r) => r.decisionComment || <span className={styles.commentEmpty}>Без комментария</span>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (onDelete && rows.some(canDeleteRow)) {
|
||||||
|
columns.push({
|
||||||
|
key: 'actions',
|
||||||
|
title: '',
|
||||||
|
width: '56px',
|
||||||
|
render: (r) =>
|
||||||
|
canDeleteRow(r) ? (
|
||||||
|
<IconButton
|
||||||
|
icon="trash"
|
||||||
|
title="Удалить черновик"
|
||||||
|
danger
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
// Клик по всей строке открывает заявку — кнопке это не нужно.
|
||||||
|
e.stopPropagation();
|
||||||
|
onDelete(r);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.tableWrap}>
|
<div className={styles.tableWrap}>
|
||||||
@@ -300,6 +335,18 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
|
|||||||
const [rows, setRows] = React.useState<IMyRequest[] | undefined>(undefined);
|
const [rows, setRows] = React.useState<IMyRequest[] | undefined>(undefined);
|
||||||
const [totalRows, setTotalRows] = React.useState<number>(0);
|
const [totalRows, setTotalRows] = React.useState<number>(0);
|
||||||
const [error, setError] = React.useState<string>('');
|
const [error, setError] = React.useState<string>('');
|
||||||
|
const [deleteError, setDeleteError] = React.useState<string>('');
|
||||||
|
|
||||||
|
// Список перечитается сам: confirmAndDeleteDraft шлёт rp-requests-changed.
|
||||||
|
const handleDelete = (request: IMyRequest): void => {
|
||||||
|
setDeleteError('');
|
||||||
|
confirmAndDeleteDraft({
|
||||||
|
formKey: request.formKey,
|
||||||
|
itemId: request.id,
|
||||||
|
listId: request.listId,
|
||||||
|
code: request.code
|
||||||
|
}).catch((e) => setDeleteError(`Не удалось удалить черновик: ${friendlyErrorMessage(e)}`));
|
||||||
|
};
|
||||||
|
|
||||||
// input — то, что в поле ввода (меняется сразу); в маршрут значение уходит
|
// input — то, что в поле ввода (меняется сразу); в маршрут значение уходит
|
||||||
// после паузы в наборе. Данные грузятся по МАРШРУТНОМУ query — он и так уже
|
// после паузы в наборе. Данные грузятся по МАРШРУТНОМУ query — он и так уже
|
||||||
@@ -545,6 +592,7 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <div className={styles.error}>Не удалось загрузить заявки: {error}</div>}
|
{error && <div className={styles.error}>Не удалось загрузить заявки: {error}</div>}
|
||||||
|
{deleteError && <div className={styles.error}>{deleteError}</div>}
|
||||||
{!error && !loaded && <div className={styles.empty}>Загрузка…</div>}
|
{!error && !loaded && <div className={styles.empty}>Загрузка…</div>}
|
||||||
{!error && loaded && total === 0 && (
|
{!error && loaded && total === 0 && (
|
||||||
<div className={styles.empty}>
|
<div className={styles.empty}>
|
||||||
@@ -553,7 +601,13 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
|
|||||||
)}
|
)}
|
||||||
{!error && rows && total > 0 && (
|
{!error && rows && total > 0 && (
|
||||||
<>
|
<>
|
||||||
<Table rows={visible} view={view} statusFilter={statusFilter} onOpen={onOpen} />
|
<Table
|
||||||
|
rows={visible}
|
||||||
|
view={view}
|
||||||
|
statusFilter={statusFilter}
|
||||||
|
onOpen={onOpen}
|
||||||
|
onDelete={view === 'own:created' ? handleDelete : undefined}
|
||||||
|
/>
|
||||||
<Pager page={page} totalPages={totalPages} onChange={onPageChange} />
|
<Pager page={page} totalPages={totalPages} onChange={onPageChange} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
RequestNumber,
|
RequestNumber,
|
||||||
SubmitActions,
|
SubmitActions,
|
||||||
|
getDraftDeleteHandler,
|
||||||
useRequestSubmit
|
useRequestSubmit
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
@@ -277,6 +278,17 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({
|
|||||||
|
|
||||||
const sectionProps = { data, set, context, onEmployeeChange: handleEmployeeChange };
|
const sectionProps = { data, set, context, onEmployeeChange: handleEmployeeChange };
|
||||||
|
|
||||||
|
// «Удалить черновик» — только у сохранённой заявки в статусе «Черновик».
|
||||||
|
const handleDelete = getDraftDeleteHandler({
|
||||||
|
formKey: FORM_ID,
|
||||||
|
requestId,
|
||||||
|
requestStatus,
|
||||||
|
listId: editListId,
|
||||||
|
code: requestCode,
|
||||||
|
submit,
|
||||||
|
onDeleted: onCancel
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button type="button" className={styles.back} onClick={onCancel}>
|
<button type="button" className={styles.back} onClick={onCancel}>
|
||||||
@@ -328,6 +340,7 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<SubmitActions
|
<SubmitActions
|
||||||
|
onDelete={handleDelete}
|
||||||
submit={submit}
|
submit={submit}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
RequestNumber,
|
RequestNumber,
|
||||||
SubmitActions,
|
SubmitActions,
|
||||||
|
getDraftDeleteHandler,
|
||||||
useRequestSubmit
|
useRequestSubmit
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
@@ -270,6 +271,17 @@ const ConcreteRequestForm: React.FC<IRequestFormProps> = ({
|
|||||||
onForemanChange: handleForemanChange
|
onForemanChange: handleForemanChange
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// «Удалить черновик» — только у сохранённой заявки в статусе «Черновик».
|
||||||
|
const handleDelete = getDraftDeleteHandler({
|
||||||
|
formKey: FORM_ID,
|
||||||
|
requestId,
|
||||||
|
requestStatus,
|
||||||
|
listId: editListId,
|
||||||
|
code: requestCode,
|
||||||
|
submit,
|
||||||
|
onDeleted: onCancel
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button type="button" className={styles.back} onClick={onCancel}>
|
<button type="button" className={styles.back} onClick={onCancel}>
|
||||||
@@ -316,6 +328,7 @@ const ConcreteRequestForm: React.FC<IRequestFormProps> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SubmitActions
|
<SubmitActions
|
||||||
|
onDelete={handleDelete}
|
||||||
submit={submit}
|
submit={submit}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
RequestNumber,
|
RequestNumber,
|
||||||
SubmitActions,
|
SubmitActions,
|
||||||
|
getDraftDeleteHandler,
|
||||||
useRequestSubmit
|
useRequestSubmit
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
@@ -293,6 +294,17 @@ const DismissalForm: React.FC<IRequestFormProps> = ({
|
|||||||
onRemoveOrganization: handleRemoveOrganization
|
onRemoveOrganization: handleRemoveOrganization
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// «Удалить черновик» — только у сохранённой заявки в статусе «Черновик».
|
||||||
|
const handleDelete = getDraftDeleteHandler({
|
||||||
|
formKey: FORM_ID,
|
||||||
|
requestId,
|
||||||
|
requestStatus,
|
||||||
|
listId: editListId,
|
||||||
|
code: requestCode,
|
||||||
|
submit,
|
||||||
|
onDeleted: onCancel
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button type="button" className={styles.back} onClick={onCancel}>
|
<button type="button" className={styles.back} onClick={onCancel}>
|
||||||
@@ -332,6 +344,7 @@ const DismissalForm: React.FC<IRequestFormProps> = ({
|
|||||||
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
|
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
|
||||||
|
|
||||||
<SubmitActions
|
<SubmitActions
|
||||||
|
onDelete={handleDelete}
|
||||||
submit={submit}
|
submit={submit}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
RequestNumber,
|
RequestNumber,
|
||||||
SubmitActions,
|
SubmitActions,
|
||||||
|
getDraftDeleteHandler,
|
||||||
useRequestSubmit
|
useRequestSubmit
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
@@ -253,6 +254,17 @@ const EmployeeTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
|
|
||||||
const sectionProps = { data, set, context, onEmployeeChange: handleEmployeeChange };
|
const sectionProps = { data, set, context, onEmployeeChange: handleEmployeeChange };
|
||||||
|
|
||||||
|
// «Удалить черновик» — только у сохранённой заявки в статусе «Черновик».
|
||||||
|
const handleDelete = getDraftDeleteHandler({
|
||||||
|
formKey: FORM_ID,
|
||||||
|
requestId,
|
||||||
|
requestStatus,
|
||||||
|
listId: editListId,
|
||||||
|
code: requestCode,
|
||||||
|
submit,
|
||||||
|
onDeleted: onCancel
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button type="button" className={styles.back} onClick={onCancel}>
|
<button type="button" className={styles.back} onClick={onCancel}>
|
||||||
@@ -292,6 +304,7 @@ const EmployeeTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
|
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
|
||||||
|
|
||||||
<SubmitActions
|
<SubmitActions
|
||||||
|
onDelete={handleDelete}
|
||||||
submit={submit}
|
submit={submit}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
RequestNumber,
|
RequestNumber,
|
||||||
SubmitActions,
|
SubmitActions,
|
||||||
|
getDraftDeleteHandler,
|
||||||
useRequestSubmit
|
useRequestSubmit
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
@@ -361,6 +362,17 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
|
|||||||
const resumeAttachments = attachmentsOfCategory(existingAttachments, ATT_CATEGORY_RESUME);
|
const resumeAttachments = attachmentsOfCategory(existingAttachments, ATT_CATEGORY_RESUME);
|
||||||
const otherAttachments = uncategorizedAttachments(existingAttachments);
|
const otherAttachments = uncategorizedAttachments(existingAttachments);
|
||||||
|
|
||||||
|
// «Удалить черновик» — только у сохранённой заявки в статусе «Черновик».
|
||||||
|
const handleDelete = getDraftDeleteHandler({
|
||||||
|
formKey: FORM_ID,
|
||||||
|
requestId,
|
||||||
|
requestStatus,
|
||||||
|
listId: editListId,
|
||||||
|
code: requestCode,
|
||||||
|
submit,
|
||||||
|
onDeleted: onCancel
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button type="button" className={styles.back} onClick={onCancel}>
|
<button type="button" className={styles.back} onClick={onCancel}>
|
||||||
@@ -449,6 +461,7 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<SubmitActions
|
<SubmitActions
|
||||||
|
onDelete={handleDelete}
|
||||||
submit={submit}
|
submit={submit}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
submitValid={isSubmitValid}
|
submitValid={isSubmitValid}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
RequestNumber,
|
RequestNumber,
|
||||||
SubmitActions,
|
SubmitActions,
|
||||||
|
getDraftDeleteHandler,
|
||||||
useRequestSubmit
|
useRequestSubmit
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
@@ -374,6 +375,17 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
|
|||||||
|
|
||||||
const sectionProps = { data, set, context, onResponsibleChange: handleResponsibleChange };
|
const sectionProps = { data, set, context, onResponsibleChange: handleResponsibleChange };
|
||||||
|
|
||||||
|
// «Удалить черновик» — только у сохранённой заявки в статусе «Черновик».
|
||||||
|
const handleDelete = getDraftDeleteHandler({
|
||||||
|
formKey: FORM_ID,
|
||||||
|
requestId,
|
||||||
|
requestStatus,
|
||||||
|
listId: editListId,
|
||||||
|
code: requestCode,
|
||||||
|
submit,
|
||||||
|
onDeleted: onCancel
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button type="button" className={styles.back} onClick={onCancel}>
|
<button type="button" className={styles.back} onClick={onCancel}>
|
||||||
@@ -446,6 +458,7 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<SubmitActions
|
<SubmitActions
|
||||||
|
onDelete={handleDelete}
|
||||||
submit={submit}
|
submit={submit}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
submitValid={isSubmitValid}
|
submitValid={isSubmitValid}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
RequestNumber,
|
RequestNumber,
|
||||||
SubmitActions,
|
SubmitActions,
|
||||||
|
getDraftDeleteHandler,
|
||||||
useRequestSubmit
|
useRequestSubmit
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
@@ -390,6 +391,17 @@ const ReferralForm: React.FC<IRequestFormProps> = ({
|
|||||||
|
|
||||||
const sectionProps = { data, set, context, onReferrerChange: handleReferrerChange };
|
const sectionProps = { data, set, context, onReferrerChange: handleReferrerChange };
|
||||||
|
|
||||||
|
// «Удалить черновик» — только у сохранённой заявки в статусе «Черновик».
|
||||||
|
const handleDelete = getDraftDeleteHandler({
|
||||||
|
formKey: FORM_ID,
|
||||||
|
requestId,
|
||||||
|
requestStatus,
|
||||||
|
listId: editListId,
|
||||||
|
code: requestCode,
|
||||||
|
submit,
|
||||||
|
onDeleted: onCancel
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button type="button" className={styles.back} onClick={onCancel}>
|
<button type="button" className={styles.back} onClick={onCancel}>
|
||||||
@@ -467,6 +479,7 @@ const ReferralForm: React.FC<IRequestFormProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<SubmitActions
|
<SubmitActions
|
||||||
|
onDelete={handleDelete}
|
||||||
submit={submit}
|
submit={submit}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
formId={FORM_ID}
|
formId={FORM_ID}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
RequestNumber,
|
RequestNumber,
|
||||||
SubmitActions,
|
SubmitActions,
|
||||||
|
getDraftDeleteHandler,
|
||||||
useRequestSubmit
|
useRequestSubmit
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
@@ -327,6 +328,17 @@ const ServiceRequestForm: React.FC<IRequestFormProps> = ({
|
|||||||
onRemoveAnnexFile: handleRemoveAnnexFile
|
onRemoveAnnexFile: handleRemoveAnnexFile
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// «Удалить черновик» — только у сохранённой заявки в статусе «Черновик».
|
||||||
|
const handleDelete = getDraftDeleteHandler({
|
||||||
|
formKey: FORM_ID,
|
||||||
|
requestId,
|
||||||
|
requestStatus,
|
||||||
|
listId: editListId,
|
||||||
|
code: requestCode,
|
||||||
|
submit,
|
||||||
|
onDeleted: onCancel
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button type="button" className={styles.back} onClick={onCancel}>
|
<button type="button" className={styles.back} onClick={onCancel}>
|
||||||
@@ -376,6 +388,7 @@ const ServiceRequestForm: React.FC<IRequestFormProps> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SubmitActions
|
<SubmitActions
|
||||||
|
onDelete={handleDelete}
|
||||||
submit={submit}
|
submit={submit}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
RequestNumber,
|
RequestNumber,
|
||||||
SubmitActions,
|
SubmitActions,
|
||||||
|
getDraftDeleteHandler,
|
||||||
useRequestSubmit
|
useRequestSubmit
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
@@ -347,6 +348,17 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
|||||||
|
|
||||||
const sectionProps = { data, set, context };
|
const sectionProps = { data, set, context };
|
||||||
|
|
||||||
|
// «Удалить черновик» — только у сохранённой заявки в статусе «Черновик».
|
||||||
|
const handleDelete = getDraftDeleteHandler({
|
||||||
|
formKey: FORM_ID,
|
||||||
|
requestId,
|
||||||
|
requestStatus,
|
||||||
|
listId: editListId,
|
||||||
|
code: requestCode,
|
||||||
|
submit,
|
||||||
|
onDeleted: onCancel
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button type="button" className={styles.back} onClick={onCancel}>
|
<button type="button" className={styles.back} onClick={onCancel}>
|
||||||
@@ -404,6 +416,7 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SubmitActions
|
<SubmitActions
|
||||||
|
onDelete={handleDelete}
|
||||||
submit={submit}
|
submit={submit}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
formId="ticket1c"
|
formId="ticket1c"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
RequestNumber,
|
RequestNumber,
|
||||||
SubmitActions,
|
SubmitActions,
|
||||||
|
getDraftDeleteHandler,
|
||||||
useRequestSubmit
|
useRequestSubmit
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
@@ -432,6 +433,17 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
|
|
||||||
const sectionProps = { data, set, context, onRequesterChange: handleRequesterChange };
|
const sectionProps = { data, set, context, onRequesterChange: handleRequesterChange };
|
||||||
|
|
||||||
|
// «Удалить черновик» — только у сохранённой заявки в статусе «Черновик».
|
||||||
|
const handleDelete = getDraftDeleteHandler({
|
||||||
|
formKey: FORM_ID,
|
||||||
|
requestId,
|
||||||
|
requestStatus,
|
||||||
|
listId: editListId,
|
||||||
|
code: requestCode,
|
||||||
|
submit,
|
||||||
|
onDeleted: onCancel
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button type="button" className={styles.back} onClick={onCancel}>
|
<button type="button" className={styles.back} onClick={onCancel}>
|
||||||
@@ -503,6 +515,7 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<SubmitActions
|
<SubmitActions
|
||||||
|
onDelete={handleDelete}
|
||||||
submit={submit}
|
submit={submit}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
RequestNumber,
|
RequestNumber,
|
||||||
SubmitActions,
|
SubmitActions,
|
||||||
|
getDraftDeleteHandler,
|
||||||
useRequestSubmit
|
useRequestSubmit
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
@@ -265,6 +266,17 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
|||||||
|
|
||||||
const sectionProps = { data, set, context, onEmployeeChange: handleEmployeeChange };
|
const sectionProps = { data, set, context, onEmployeeChange: handleEmployeeChange };
|
||||||
|
|
||||||
|
// «Удалить черновик» — только у сохранённой заявки в статусе «Черновик».
|
||||||
|
const handleDelete = getDraftDeleteHandler({
|
||||||
|
formKey: FORM_ID,
|
||||||
|
requestId,
|
||||||
|
requestStatus,
|
||||||
|
listId: editListId,
|
||||||
|
code: requestCode,
|
||||||
|
submit,
|
||||||
|
onDeleted: onCancel
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button type="button" className={styles.back} onClick={onCancel}>
|
<button type="button" className={styles.back} onClick={onCancel}>
|
||||||
@@ -304,6 +316,7 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
|||||||
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
|
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
|
||||||
|
|
||||||
<SubmitActions
|
<SubmitActions
|
||||||
|
onDelete={handleDelete}
|
||||||
submit={submit}
|
submit={submit}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
RequestNumber,
|
RequestNumber,
|
||||||
SubmitActions,
|
SubmitActions,
|
||||||
|
getDraftDeleteHandler,
|
||||||
useRequestSubmit
|
useRequestSubmit
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
@@ -264,6 +265,17 @@ const VacationForm: React.FC<IRequestFormProps> = ({
|
|||||||
|
|
||||||
const sectionProps = { data, set, context, onEmployeeChange: handleEmployeeChange };
|
const sectionProps = { data, set, context, onEmployeeChange: handleEmployeeChange };
|
||||||
|
|
||||||
|
// «Удалить черновик» — только у сохранённой заявки в статусе «Черновик».
|
||||||
|
const handleDelete = getDraftDeleteHandler({
|
||||||
|
formKey: FORM_ID,
|
||||||
|
requestId,
|
||||||
|
requestStatus,
|
||||||
|
listId: editListId,
|
||||||
|
code: requestCode,
|
||||||
|
submit,
|
||||||
|
onDeleted: onCancel
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button type="button" className={styles.back} onClick={onCancel}>
|
<button type="button" className={styles.back} onClick={onCancel}>
|
||||||
@@ -303,6 +315,7 @@ const VacationForm: React.FC<IRequestFormProps> = ({
|
|||||||
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
|
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
|
||||||
|
|
||||||
<SubmitActions
|
<SubmitActions
|
||||||
|
onDelete={handleDelete}
|
||||||
submit={submit}
|
submit={submit}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
RequestNumber,
|
RequestNumber,
|
||||||
SubmitActions,
|
SubmitActions,
|
||||||
|
getDraftDeleteHandler,
|
||||||
useRequestSubmit
|
useRequestSubmit
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
@@ -324,6 +325,17 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
|
|||||||
|
|
||||||
const sectionProps = { data, set, context };
|
const sectionProps = { data, set, context };
|
||||||
|
|
||||||
|
// «Удалить черновик» — только у сохранённой заявки в статусе «Черновик».
|
||||||
|
const handleDelete = getDraftDeleteHandler({
|
||||||
|
formKey: FORM_ID,
|
||||||
|
requestId,
|
||||||
|
requestStatus,
|
||||||
|
listId: editListId,
|
||||||
|
code: requestCode,
|
||||||
|
submit,
|
||||||
|
onDeleted: onCancel
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button type="button" className={styles.back} onClick={onCancel}>
|
<button type="button" className={styles.back} onClick={onCancel}>
|
||||||
@@ -387,6 +399,7 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<SubmitActions
|
<SubmitActions
|
||||||
|
onDelete={handleDelete}
|
||||||
submit={submit}
|
submit={submit}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
formId={FORM_ID}
|
formId={FORM_ID}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { confirmModal } from '@tnsu/ui-kit';
|
||||||
|
import { RequestTypeId } from '../models/types';
|
||||||
|
import { REQUEST_STATUS, approvalServiceFor } from './approval';
|
||||||
|
import { resolveDataList } from './dataList';
|
||||||
|
import { emitRequestsChanged } from './requestEvents';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удаление СВОЕЙ заявки в статусе «Черновик» — из формы черновика и из таблицы
|
||||||
|
* «Мои заявки». Отправленную заявку (любой другой статус) удалить нельзя.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface IDraftRef {
|
||||||
|
/** Ключ формы (он же RequestTypeId портала) — по нему выводятся имена списков. */
|
||||||
|
formKey: RequestTypeId;
|
||||||
|
itemId: number;
|
||||||
|
/** Партиция, где лежит заявка; не задана — текущая (см. resolveDataList). */
|
||||||
|
listId?: string;
|
||||||
|
/** Уникальный код для текста подтверждения; '' — покажем Id. */
|
||||||
|
code?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NOT_DRAFT_MESSAGE = 'Заявка уже не черновик — удалить можно только черновик. Обновите страницу.';
|
||||||
|
|
||||||
|
/** Можно ли предлагать удаление заявки с таким статусом. */
|
||||||
|
export function isDeletableStatus(status: string | undefined): boolean {
|
||||||
|
return status === REQUEST_STATUS.draft;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удалить черновик: элемент списка данных — в корзину сайта (вместе с
|
||||||
|
* вложениями; восстановит администратор, если удалили по ошибке), строки
|
||||||
|
* маршрута — насовсем (у черновика это лишь заготовка, её перепишет следующая
|
||||||
|
* отправка — см. writeRoute).
|
||||||
|
*
|
||||||
|
* Статус перепроверяется по серверу: заявку могли отправить из другой вкладки,
|
||||||
|
* пока эта показывала её черновиком.
|
||||||
|
*
|
||||||
|
* Порядок — сначала заявка, потом маршрут: если второй шаг сорвётся, останутся
|
||||||
|
* лишь «осиротевшие» строки маршрута черновика, которые нигде не показываются;
|
||||||
|
* наоборот же остался бы живой черновик без маршрута.
|
||||||
|
*/
|
||||||
|
export async function deleteDraftRequest(ref: IDraftRef): Promise<void> {
|
||||||
|
const list = resolveDataList(ref.formKey, ref.listId);
|
||||||
|
const item = (await list.items.getById(ref.itemId).select('RequestStatus')()) as { RequestStatus?: string };
|
||||||
|
if (!isDeletableStatus(item.RequestStatus)) {
|
||||||
|
throw new Error(NOT_DRAFT_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
await list.items.getById(ref.itemId).recycle();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await approvalServiceFor(ref.formKey, ref.listId).deleteForRequest(ref.itemId);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[Удаление черновика] строки маршрута заявки ${ref.itemId} не удалены:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Спросить подтверждение и удалить черновик. true — удалён (таблицы уже
|
||||||
|
* оповещены), false — пользователь передумал. Ошибку удаления пробрасывает —
|
||||||
|
* показать её должен вызывающий, у него для этого своё место на экране.
|
||||||
|
*/
|
||||||
|
export async function confirmAndDeleteDraft(ref: IDraftRef): Promise<boolean> {
|
||||||
|
const ok = await confirmModal({
|
||||||
|
title: 'Удалить черновик',
|
||||||
|
message: `Удалить черновик №${ref.code || ref.itemId}? Это действие нельзя отменить.`,
|
||||||
|
confirmText: 'Удалить',
|
||||||
|
cancelText: 'Отмена',
|
||||||
|
danger: true
|
||||||
|
});
|
||||||
|
if (!ok) return false;
|
||||||
|
|
||||||
|
await deleteDraftRequest(ref);
|
||||||
|
emitRequestsChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -53,6 +53,12 @@ export interface ISubmitActionsProps {
|
|||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
/** Подпись основной кнопки. По умолчанию — «Отправить на согласование». */
|
/** Подпись основной кнопки. По умолчанию — «Отправить на согласование». */
|
||||||
submitLabel?: string;
|
submitLabel?: string;
|
||||||
|
/**
|
||||||
|
* Удалить черновик (см. getDraftDeleteHandler). Не задан — кнопки нет: заявка ещё
|
||||||
|
* не сохранена или уже не черновик. Заморозка подачи на удаление не влияет —
|
||||||
|
* это уборка своего, а не подача.
|
||||||
|
*/
|
||||||
|
onDelete?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Единый ряд кнопок форм подачи: Отмена / Сохранить черновик / Отправить.
|
// Единый ряд кнопок форм подачи: Отмена / Сохранить черновик / Отправить.
|
||||||
@@ -69,6 +75,7 @@ export const SubmitActions: React.FC<ISubmitActionsProps> = ({
|
|||||||
onSaveDraft,
|
onSaveDraft,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
submitLabel = 'Отправить на согласование',
|
submitLabel = 'Отправить на согласование',
|
||||||
|
onDelete,
|
||||||
}) => {
|
}) => {
|
||||||
const { saving, kind } = submit;
|
const { saving, kind } = submit;
|
||||||
const unfrozenForm = formId != null && UNFROZEN_FORMS.has(formId);
|
const unfrozenForm = formId != null && UNFROZEN_FORMS.has(formId);
|
||||||
@@ -97,6 +104,12 @@ export const SubmitActions: React.FC<ISubmitActionsProps> = ({
|
|||||||
<>
|
<>
|
||||||
{frozen && !readOnly && <Alert variant="warn">Подача заявок временно недоступна.</Alert>}
|
{frozen && !readOnly && <Alert variant="warn">Подача заявок временно недоступна.</Alert>}
|
||||||
<FormActions>
|
<FormActions>
|
||||||
|
{onDelete && (
|
||||||
|
<Button variant="danger" onClick={onDelete} disabled={saving}>
|
||||||
|
<Icon name="trash" size="sm" />
|
||||||
|
Удалить черновик
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button variant="secondary" onClick={onCancel} disabled={saving}>
|
<Button variant="secondary" onClick={onCancel} disabled={saving}>
|
||||||
Отмена
|
Отмена
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { RequestTypeId } from '../../../models/types';
|
||||||
|
import { confirmAndDeleteDraft, isDeletableStatus } from '../../draftDeletion';
|
||||||
|
import { friendlyErrorMessage } from '../../diagnostics';
|
||||||
|
import { markFormClean } from '../../unsavedChanges';
|
||||||
|
import { IRequestSubmit } from '../useRequestSubmit';
|
||||||
|
|
||||||
|
export interface IDraftDeletionParams {
|
||||||
|
formKey: RequestTypeId;
|
||||||
|
/** Id сохранённой заявки; undefined — новая, ещё не сохранённая (удалять нечего). */
|
||||||
|
requestId: number | undefined;
|
||||||
|
requestStatus: string | undefined;
|
||||||
|
listId?: string;
|
||||||
|
code?: string;
|
||||||
|
/** Ошибка удаления показывается там же, где ошибки сохранения формы. */
|
||||||
|
submit: IRequestSubmit;
|
||||||
|
/** После удаления — уйти с формы (обычно onCancel формы: к плиткам / закрыть панель). */
|
||||||
|
onDeleted: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработчик кнопки «Удалить черновик» для SubmitActions (проп onDelete).
|
||||||
|
* undefined — кнопку не показывать: заявка не сохранена или уже не черновик.
|
||||||
|
*/
|
||||||
|
export function getDraftDeleteHandler(params: IDraftDeletionParams): (() => void) | undefined {
|
||||||
|
const { formKey, requestId, requestStatus, listId, code, submit, onDeleted } = params;
|
||||||
|
if (!requestId || !isDeletableStatus(requestStatus)) return undefined;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
confirmAndDeleteDraft({ formKey, itemId: requestId, listId, code })
|
||||||
|
.then((deleted) => {
|
||||||
|
if (!deleted) return;
|
||||||
|
// Черновика больше нет — предупреждать о «несохранённых правках» при
|
||||||
|
// уходе с формы уже не о чем.
|
||||||
|
markFormClean();
|
||||||
|
onDeleted();
|
||||||
|
})
|
||||||
|
.catch((e) => submit.setError(`Не удалось удалить черновик: ${friendlyErrorMessage(e)}`));
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -46,6 +46,7 @@ export { OrgLetterhead } from './OrgLetterhead/OrgLetterhead';
|
|||||||
export { LinkField } from './LinkField/LinkField';
|
export { LinkField } from './LinkField/LinkField';
|
||||||
export { ErrorOverlay } from './ErrorOverlay/ErrorOverlay';
|
export { ErrorOverlay } from './ErrorOverlay/ErrorOverlay';
|
||||||
export { SubmitActions } from './SubmitActions/SubmitActions';
|
export { SubmitActions } from './SubmitActions/SubmitActions';
|
||||||
|
export { getDraftDeleteHandler } from './SubmitActions/getDraftDeleteHandler';
|
||||||
export { InitiatorLine } from './InitiatorLine/InitiatorLine';
|
export { InitiatorLine } from './InitiatorLine/InitiatorLine';
|
||||||
export type { IInitiatorLineProps } from './InitiatorLine/InitiatorLine';
|
export type { IInitiatorLineProps } from './InitiatorLine/InitiatorLine';
|
||||||
export { useRequestSubmit } from './useRequestSubmit';
|
export { useRequestSubmit } from './useRequestSubmit';
|
||||||
|
|||||||
Reference in New Issue
Block a user