feat(ServiceRequest): прикрепление файла к строке «Приложения»
Каждую строку таблицы приложений теперь можно связать с реальным файлом — он хранится как встроенное вложение элемента списка (тот же механизм, что у общего блока «Вложения»), новых списков/библиотек в SharePoint под это заводить не нужно. В IAnnexRow добавлено fileName; выбранный, но ещё не сохранённый файл держится в памяти формы и грузится вместе с общими вложениями при save() (нужен id элемента). Удаление строки/замена файла открепляет уже загруженное вложение, чтобы не оставалось висящих файлов. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -65,6 +65,10 @@ const ServiceRequestForm: React.FC<IRequestFormProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [data, setData] = useState<IFormData>(EMPTY_DATA);
|
const [data, setData] = useState<IFormData>(EMPTY_DATA);
|
||||||
const [files, setFiles] = useState<File[]>([]);
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
|
// Файлы, выбранные для строк «Приложения», ещё не загруженные на сервер (нужен
|
||||||
|
// id элемента — см. save()). Ключ — fileName (= выбранное имя строки), не индекс
|
||||||
|
// строки: индекс «плывёт» при удалении строк, а имя однозначно и переживает это.
|
||||||
|
const [annexFiles, setAnnexFiles] = useState<Record<string, File>>({});
|
||||||
const [existingAttachments, setExistingAttachments] = useState<IAttachmentInfo[]>([]);
|
const [existingAttachments, setExistingAttachments] = useState<IAttachmentInfo[]>([]);
|
||||||
const [requestId, setRequestId] = useState<number | undefined>(editId);
|
const [requestId, setRequestId] = useState<number | undefined>(editId);
|
||||||
const [requestStatus, setRequestStatus] = useState<string | undefined>(undefined);
|
const [requestStatus, setRequestStatus] = useState<string | undefined>(undefined);
|
||||||
@@ -215,6 +219,56 @@ const ServiceRequestForm: React.FC<IRequestFormProps> = ({
|
|||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Ссылки на скачивание для уже загруженных файлов строк «Приложения» — тот же
|
||||||
|
// пул вложений элемента, что и у общего блока «Вложения» (см. attachmentUrls).
|
||||||
|
const attachmentUrls = useMemo(() => {
|
||||||
|
const map: Record<string, string> = {};
|
||||||
|
existingAttachments.forEach((a) => {
|
||||||
|
map[a.fileName] = a.url;
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [existingAttachments]);
|
||||||
|
|
||||||
|
// Выбрали файл для строки приложения — сохраняем в памяти до save() (нужен id
|
||||||
|
// элемента, см. saveServiceRequest). Если у строки уже был загруженный файл —
|
||||||
|
// сразу открепляем старый, иначе на элементе повиснет лишнее вложение.
|
||||||
|
const handlePickAnnexFile = (rowIndex: number, file: File): void => {
|
||||||
|
const prevFileName = data.Annexes[rowIndex]?.fileName;
|
||||||
|
if (prevFileName && requestId !== undefined && existingAttachments.some((a) => a.fileName === prevFileName)) {
|
||||||
|
deleteAttachment(editListId || dataListTitle(FORM_ID), requestId, prevFileName)
|
||||||
|
.then(() => setExistingAttachments((prev) => prev.filter((a) => a.fileName !== prevFileName)))
|
||||||
|
.catch(() => undefined);
|
||||||
|
}
|
||||||
|
setData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
Annexes: prev.Annexes.map((row, i) => (i === rowIndex ? { ...row, fileName: file.name } : row))
|
||||||
|
}));
|
||||||
|
setAnnexFiles((prev) => ({ ...prev, [file.name]: file }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Открепить файл строки: если он уже на сервере — удаляем вложение сразу
|
||||||
|
// (как handleRemoveAttachment), иначе просто забываем ещё не загруженный выбор.
|
||||||
|
const handleRemoveAnnexFile = (rowIndex: number): void => {
|
||||||
|
const fileName = data.Annexes[rowIndex]?.fileName;
|
||||||
|
setData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
Annexes: prev.Annexes.map((row, i) => (i === rowIndex ? { ...row, fileName: '' } : row))
|
||||||
|
}));
|
||||||
|
if (!fileName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setAnnexFiles((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[fileName];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
if (requestId !== undefined && existingAttachments.some((a) => a.fileName === fileName)) {
|
||||||
|
deleteAttachment(editListId || dataListTitle(FORM_ID), requestId, fileName)
|
||||||
|
.then(() => setExistingAttachments((prev) => prev.filter((a) => a.fileName !== fileName)))
|
||||||
|
.catch(() => undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const save = (status: string): (() => Promise<void>) => async (): Promise<void> => {
|
const save = (status: string): (() => Promise<void>) => async (): Promise<void> => {
|
||||||
// validate()/validateSubmit() уже гарантировали requestTypeId к моменту вызова
|
// validate()/validateSubmit() уже гарантировали requestTypeId к моменту вызова
|
||||||
// (см. useRequestSubmit.run — валидирует ДО save), но TS этого через границу
|
// (см. useRequestSubmit.run — валидирует ДО save), но TS этого через границу
|
||||||
@@ -230,9 +284,18 @@ const ServiceRequestForm: React.FC<IRequestFormProps> = ({
|
|||||||
if (code) {
|
if (code) {
|
||||||
setRequestCode(code);
|
setRequestCode(code);
|
||||||
}
|
}
|
||||||
if (files.length > 0) {
|
// Файлы строк «Приложения» — только те, чья строка ещё существует (строку
|
||||||
await uploadAttachments(writeListId || dataListTitle(FORM_ID), id, files);
|
// могли удалить, не открепляя файл явно — см. AnnexesSection.removeRow) и
|
||||||
|
// чьё имя всё ещё совпадает (замену уже отправляет handlePickAnnexFile).
|
||||||
|
const annexFileNames = new Set(data.Annexes.map((r) => r.fileName).filter(Boolean));
|
||||||
|
const pendingAnnexFiles = Object.entries(annexFiles)
|
||||||
|
.filter(([fileName]) => annexFileNames.has(fileName))
|
||||||
|
.map(([, file]) => file);
|
||||||
|
const allFiles = [...files, ...pendingAnnexFiles];
|
||||||
|
if (allFiles.length > 0) {
|
||||||
|
await uploadAttachments(writeListId || dataListTitle(FORM_ID), id, allFiles);
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
|
setAnnexFiles({});
|
||||||
setExistingAttachments(await getAttachments(writeListId || dataListTitle(FORM_ID), id));
|
setExistingAttachments(await getAttachments(writeListId || dataListTitle(FORM_ID), id));
|
||||||
}
|
}
|
||||||
if (onSaved) {
|
if (onSaved) {
|
||||||
@@ -264,7 +327,15 @@ const ServiceRequestForm: React.FC<IRequestFormProps> = ({
|
|||||||
const handleSaveDraft = (): Promise<void> => submit.run('draft', save(REQUEST_STATUS.draft), validate);
|
const handleSaveDraft = (): Promise<void> => submit.run('draft', save(REQUEST_STATUS.draft), validate);
|
||||||
const handleSubmit = (): Promise<void> => submit.run('submit', save(REQUEST_STATUS.onApproval), validateSubmit);
|
const handleSubmit = (): Promise<void> => submit.run('submit', save(REQUEST_STATUS.onApproval), validateSubmit);
|
||||||
|
|
||||||
const sectionProps = { data, set, context, onRequesterChange: handleRequesterChange };
|
const sectionProps = {
|
||||||
|
data,
|
||||||
|
set,
|
||||||
|
context,
|
||||||
|
onRequesterChange: handleRequesterChange,
|
||||||
|
attachmentUrls,
|
||||||
|
onPickAnnexFile: handlePickAnnexFile,
|
||||||
|
onRemoveAnnexFile: handleRemoveAnnexFile
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -121,7 +121,17 @@ const ServiceRequestView: React.FC<IServiceRequestViewProps> = ({ context, itemI
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sectionProps = { data, set, context, printOrgIndex };
|
// Ссылки на скачивание для файлов строк «Приложения» (см. AnnexesSection) —
|
||||||
|
// из того же пула вложений элемента, что и общий блок «Вложения» ниже.
|
||||||
|
const attachmentUrls = useMemo(() => {
|
||||||
|
const map: Record<string, string> = {};
|
||||||
|
attachments.forEach((a) => {
|
||||||
|
map[a.fileName] = a.url;
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [attachments]);
|
||||||
|
|
||||||
|
const sectionProps = { data, set, context, printOrgIndex, attachmentUrls };
|
||||||
|
|
||||||
const notify = (): void => {
|
const notify = (): void => {
|
||||||
if (!requestTypeId) return;
|
if (!requestTypeId) return;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { dataListTitle } from '../../shared/listTitles';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA, IAnnexRow, ICostGroup } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA, EMPTY_ANNEX_ROW, IAnnexRow, ICostGroup } from './model';
|
||||||
|
|
||||||
export interface ILoadedRequest {
|
export interface ILoadedRequest {
|
||||||
data: IFormData;
|
data: IFormData;
|
||||||
@@ -93,7 +93,9 @@ export async function loadServiceRequest(itemId: number, listId?: string): Promi
|
|||||||
EstimatedStartDate: toDateInput(item.EstimatedStartDate),
|
EstimatedStartDate: toDateInput(item.EstimatedStartDate),
|
||||||
EstimatedFinishDate: toDateInput(item.EstimatedFinishDate),
|
EstimatedFinishDate: toDateInput(item.EstimatedFinishDate),
|
||||||
GeneralRemarks: (item.GeneralRemarks as string) || '',
|
GeneralRemarks: (item.GeneralRemarks as string) || '',
|
||||||
Annexes: parseRows<IAnnexRow>(item.AnnexesJson),
|
// fileName — через spread с дефолтом: заявки, сохранённые до появления
|
||||||
|
// прикреплённых файлов у строки приложения, не имеют его в JSON вовсе.
|
||||||
|
Annexes: parseRows<IAnnexRow>(item.AnnexesJson).map((row) => ({ ...EMPTY_ANNEX_ROW, ...row })),
|
||||||
Costs: parseRows<ICostGroup>(item.EstimateJson)
|
Costs: parseRows<ICostGroup>(item.EstimateJson)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ export interface IAnnexRow {
|
|||||||
pages: string;
|
pages: string;
|
||||||
/** Документ реально приложен (галочка «Подтверждение» бумажной формы). */
|
/** Документ реально приложен (галочка «Подтверждение» бумажной формы). */
|
||||||
confirmed: boolean;
|
confirmed: boolean;
|
||||||
|
/**
|
||||||
|
* Имя реально прикреплённого файла — привязывает строку к одному из вложений
|
||||||
|
* элемента списка (см. shared/attachments). '' — файл к этой строке не
|
||||||
|
* прикреплён (только галочка «Подтверждение», как раньше). Отдельного списка/
|
||||||
|
* библиотеки под это не заводим: вложения — встроенная возможность любого
|
||||||
|
* списка SharePoint, здесь используется та же, что и у общего блока «Вложения».
|
||||||
|
*/
|
||||||
|
fileName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,7 +60,7 @@ export interface ICostGroup {
|
|||||||
items: ICostRow[];
|
items: ICostRow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EMPTY_ANNEX_ROW: IAnnexRow = { description: '', annexNo: '', pages: '', confirmed: false };
|
export const EMPTY_ANNEX_ROW: IAnnexRow = { description: '', annexNo: '', pages: '', confirmed: false, fileName: '' };
|
||||||
/** Пустая строка сметы — количество/цена вводятся руками, без значений по умолчанию. */
|
/** Пустая строка сметы — количество/цена вводятся руками, без значений по умолчанию. */
|
||||||
export const EMPTY_COST_ROW: ICostRow = { description: '', unit: '', qty: '', rate: '' };
|
export const EMPTY_COST_ROW: ICostRow = { description: '', unit: '', qty: '', rate: '' };
|
||||||
/** Новая группа сметы — сразу с одной пустой позицией, чтобы не жать «Добавить позицию» отдельно. */
|
/** Новая группа сметы — сразу с одной пустой позицией, чтобы не жать «Добавить позицию» отдельно. */
|
||||||
@@ -151,6 +159,26 @@ export interface ISectionProps {
|
|||||||
* поля отдельный обработчик. Не передан (режим чтения) — правка через set.
|
* поля отдельный обработчик. Не передан (режим чтения) — правка через set.
|
||||||
*/
|
*/
|
||||||
onRequesterChange?: (login: string) => void;
|
onRequesterChange?: (login: string) => void;
|
||||||
|
/**
|
||||||
|
* Только для AnnexesSection: ссылки на скачивание уже прикреплённых файлов
|
||||||
|
* (fileName → ServerRelativeUrl), из общего пула вложений элемента (см.
|
||||||
|
* shared/attachments). Не передан — ссылки не показываются (напр. новая
|
||||||
|
* заявка до первого сохранения, там вложений ещё нет).
|
||||||
|
*/
|
||||||
|
attachmentUrls?: Record<string, string>;
|
||||||
|
/**
|
||||||
|
* Только для AnnexesSection: выбрать файл для строки приложения — форма
|
||||||
|
* держит его в памяти (см. ServiceRequestForm.annexFiles) и реально
|
||||||
|
* загружает вместе с общими вложениями при сохранении (нужен id элемента).
|
||||||
|
* Не передан — прикрепление недоступно (режим просмотра).
|
||||||
|
*/
|
||||||
|
onPickAnnexFile?: (rowIndex: number, file: File) => void;
|
||||||
|
/**
|
||||||
|
* Только для AnnexesSection: открепить файл строки. Если он уже загружен на
|
||||||
|
* сервер — форма сразу удаляет вложение элемента, иначе просто забывает
|
||||||
|
* выбор (ещё не сохранённый). Не передан — открепление недоступно.
|
||||||
|
*/
|
||||||
|
onRemoveAnnexFile?: (rowIndex: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ревизия по умолчанию — как в бумажной форме («Рев:00»). */
|
/** Ревизия по умолчанию — как в бумажной форме («Рев:00»). */
|
||||||
|
|||||||
@@ -53,6 +53,10 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
.confirmed {
|
.confirmed {
|
||||||
width: 110px;
|
width: 110px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -63,6 +67,83 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Скрытый нативный <input type=file> — открывается кликом по кнопке .attach.
|
||||||
|
.fileInput {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attach {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 5px 9px;
|
||||||
|
border: 1px dashed $gray-300;
|
||||||
|
border-radius: $r-sm;
|
||||||
|
background: transparent;
|
||||||
|
color: $gray-700;
|
||||||
|
font-family: $font-sans;
|
||||||
|
font-size: $fs-caption;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: .18s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $gold-400;
|
||||||
|
color: $gold-600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileName {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $sp-2;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
a,
|
||||||
|
span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-family: $font-sans;
|
||||||
|
font-size: $fs-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $navy-700;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $gold-600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.removeFile {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 4px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: $r-sm;
|
||||||
|
background: transparent;
|
||||||
|
color: $gray-500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: .18s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $red-50;
|
||||||
|
color: $red-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.noFile {
|
||||||
|
color: $gray-400;
|
||||||
|
}
|
||||||
|
|
||||||
.remove {
|
.remove {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -3,15 +3,76 @@ import styles from './AnnexesSection.module.scss';
|
|||||||
import { Button, Checkbox, CollapseSection, Icon, TextInput, useFormReadOnly } from '../../../shared/ui';
|
import { Button, Checkbox, CollapseSection, Icon, TextInput, useFormReadOnly } from '../../../shared/ui';
|
||||||
import { EMPTY_ANNEX_ROW, IAnnexRow, ISectionProps } from '../model';
|
import { EMPTY_ANNEX_ROW, IAnnexRow, ISectionProps } from '../model';
|
||||||
|
|
||||||
|
/** Файл строки: ссылка на скачивание, если уже загружен, иначе — прикрепить/открепить. */
|
||||||
|
const AnnexFileCell: React.FC<{
|
||||||
|
row: IAnnexRow;
|
||||||
|
readOnly: boolean;
|
||||||
|
url?: string;
|
||||||
|
onPick?: (file: File) => void;
|
||||||
|
onRemove?: () => void;
|
||||||
|
}> = ({ row, readOnly, url, onPick, onRemove }) => {
|
||||||
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
if (!row.fileName) {
|
||||||
|
if (readOnly || !onPick) {
|
||||||
|
return <span className={styles.noFile}>—</span>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button type="button" className={styles.attach} onClick={() => inputRef.current?.click()}>
|
||||||
|
<Icon name="upload" size="sm" />
|
||||||
|
Прикрепить
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="file"
|
||||||
|
className={styles.fileInput}
|
||||||
|
onChange={(e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) onPick(file);
|
||||||
|
e.target.value = '';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={styles.fileName}>
|
||||||
|
{url ? (
|
||||||
|
<a href={url} target="_blank" rel="noopener noreferrer" title={row.fileName}>
|
||||||
|
{row.fileName}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span title={row.fileName}>{row.fileName}</span>
|
||||||
|
)}
|
||||||
|
{!readOnly && onRemove && (
|
||||||
|
<button type="button" className={styles.removeFile} title="Открепить файл" onClick={onRemove}>
|
||||||
|
<Icon name="close" size="sm" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Приложения к заявке (проектные чертежи, ТЗ, ведомости объёмов, перечень
|
* Приложения к заявке (проектные чертежи, ТЗ, ведомости объёмов, перечень
|
||||||
* материалов…) — соответствует таблице «Описание документа / Приложение №
|
* материалов…) — соответствует таблице «Описание документа / Приложение №
|
||||||
* / Кол-во стр. / Подтверждение» бумажной формы. Подтверждение — не «файл
|
* / Кол-во стр. / Подтверждение» бумажной формы плюс колонка «Файл»: сам
|
||||||
* реально прикреплён» (для этого есть общий блок «Вложения»), а «документ
|
* документ можно прикрепить прямо к строке (см. IAnnexRow.fileName). Файл
|
||||||
* существует и учтён в заявке» — так бумажная форма используется и без
|
* физически хранится как вложение элемента списка — та же встроенная
|
||||||
* приложенных файлов (напр. документ уже согласован отдельно).
|
* возможность SharePoint, что и у общего блока «Вложения» (см.
|
||||||
|
* shared/attachments), отдельная библиотека под это не нужна. «Подтверждение»
|
||||||
|
* остаётся отдельным полем: галочку можно поставить и без файла (документ
|
||||||
|
* согласован отдельно) — так вела себя бумажная форма.
|
||||||
*/
|
*/
|
||||||
export const AnnexesSection: React.FC<ISectionProps> = ({ data, set }) => {
|
export const AnnexesSection: React.FC<ISectionProps> = ({
|
||||||
|
data,
|
||||||
|
set,
|
||||||
|
attachmentUrls,
|
||||||
|
onPickAnnexFile,
|
||||||
|
onRemoveAnnexFile
|
||||||
|
}) => {
|
||||||
const readOnly = useFormReadOnly();
|
const readOnly = useFormReadOnly();
|
||||||
|
|
||||||
const setRow = (index: number, patch: Partial<IAnnexRow>): void => {
|
const setRow = (index: number, patch: Partial<IAnnexRow>): void => {
|
||||||
@@ -22,7 +83,15 @@ export const AnnexesSection: React.FC<ISectionProps> = ({ data, set }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const addRow = (): void => set('Annexes', [...data.Annexes, { ...EMPTY_ANNEX_ROW }]);
|
const addRow = (): void => set('Annexes', [...data.Annexes, { ...EMPTY_ANNEX_ROW }]);
|
||||||
const removeRow = (index: number): void => set('Annexes', data.Annexes.filter((_, i) => i !== index));
|
const removeRow = (index: number): void => {
|
||||||
|
// Строка с файлом (уже загруженным или ещё нет) — сначала открепляем через
|
||||||
|
// родителя (см. ServiceRequestForm.handleRemoveAnnexFile), иначе загруженное
|
||||||
|
// вложение останется висеть на элементе без ссылающейся на него строки.
|
||||||
|
if (data.Annexes[index]?.fileName) {
|
||||||
|
onRemoveAnnexFile?.(index);
|
||||||
|
}
|
||||||
|
set('Annexes', data.Annexes.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollapseSection title="Приложения" icon="document">
|
<CollapseSection title="Приложения" icon="document">
|
||||||
@@ -35,6 +104,7 @@ export const AnnexesSection: React.FC<ISectionProps> = ({ data, set }) => {
|
|||||||
<th>Описание документа</th>
|
<th>Описание документа</th>
|
||||||
<th className={styles.annexNo}>Приложение №</th>
|
<th className={styles.annexNo}>Приложение №</th>
|
||||||
<th className={styles.pages}>Кол-во стр.</th>
|
<th className={styles.pages}>Кол-во стр.</th>
|
||||||
|
<th className={styles.file}>Файл</th>
|
||||||
<th className={styles.confirmed}>Подтверждение</th>
|
<th className={styles.confirmed}>Подтверждение</th>
|
||||||
{!readOnly && <th className={styles.act} aria-label="Действия" />}
|
{!readOnly && <th className={styles.act} aria-label="Действия" />}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -42,7 +112,7 @@ export const AnnexesSection: React.FC<ISectionProps> = ({ data, set }) => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{data.Annexes.length === 0 ? (
|
{data.Annexes.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={readOnly ? 5 : 6} className={styles.empty}>
|
<td colSpan={readOnly ? 6 : 7} className={styles.empty}>
|
||||||
Приложения не добавлены
|
Приложения не добавлены
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -54,6 +124,9 @@ export const AnnexesSection: React.FC<ISectionProps> = ({ data, set }) => {
|
|||||||
<td>{row.description}</td>
|
<td>{row.description}</td>
|
||||||
<td className={styles.annexNo}>{row.annexNo}</td>
|
<td className={styles.annexNo}>{row.annexNo}</td>
|
||||||
<td className={styles.pages}>{row.pages}</td>
|
<td className={styles.pages}>{row.pages}</td>
|
||||||
|
<td className={styles.file}>
|
||||||
|
<AnnexFileCell row={row} readOnly url={attachmentUrls?.[row.fileName]} />
|
||||||
|
</td>
|
||||||
<td className={styles.confirmed}>
|
<td className={styles.confirmed}>
|
||||||
<Checkbox checked={row.confirmed} onChange={() => undefined} readOnly ariaLabel="Подтверждено" />
|
<Checkbox checked={row.confirmed} onChange={() => undefined} readOnly ariaLabel="Подтверждено" />
|
||||||
</td>
|
</td>
|
||||||
@@ -85,6 +158,15 @@ export const AnnexesSection: React.FC<ISectionProps> = ({ data, set }) => {
|
|||||||
borderless
|
borderless
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
<td className={styles.file}>
|
||||||
|
<AnnexFileCell
|
||||||
|
row={row}
|
||||||
|
readOnly={false}
|
||||||
|
url={attachmentUrls?.[row.fileName]}
|
||||||
|
onPick={onPickAnnexFile ? (file) => onPickAnnexFile(i, file) : undefined}
|
||||||
|
onRemove={onRemoveAnnexFile ? () => onRemoveAnnexFile(i) : undefined}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<td className={styles.confirmed}>
|
<td className={styles.confirmed}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={row.confirmed}
|
checked={row.confirmed}
|
||||||
|
|||||||
Reference in New Issue
Block a user