feat: port stroy-bot-project-assigner as resources page

Add the "resources" section (?page=resources&projectId=): professions and
companies assigned to a project, with the same lists, fields, texts and
optimistic add/remove with rollback as the legacy web part.

PnPjs is replaced by SharePoint REST through the app's IHttpClient so the
bundle needs no new dependency: the same $select/$expand/$filter, PnP paging
replaced by following odata.nextLink, adds as POST to items with the same
body, deletes as POST with X-HTTP-Method DELETE and IF-MATCH *. Known legacy
edge cases (temporary negative id) are kept and listed in the checklist.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-10-15 17:18:19 +05:00
parent 11bf491095
commit 55ce772eaa
15 changed files with 1083 additions and 0 deletions
@@ -0,0 +1,34 @@
import * as React from 'react';
import { Button } from '@tnsu/ui-kit';
import { IAssignedResource } from './resourcesTypes';
import { RESOURCES_TEXTS } from './texts';
import styles from './ResourcesPage.module.scss';
const TEXTS = RESOURCES_TEXTS.assigned;
interface Props {
items: IAssignedResource[];
onRemove: (itemId: number) => void;
}
/** Правая колонка «Привязаны к проекту». */
export const AssignedPanel: React.FC<Props> = ({ items, onRemove }) => (
<div className={styles.panelColumn}>
<div className={styles.panelTitle}>{TEXTS.title}</div>
{items.length === 0 && <div className={styles.emptyMsg}>{TEXTS.empty}</div>}
{items.map((item) => (
<div key={item.id} className={styles.itemRow}>
<span className={styles.itemName}>{item.title}</span>
<Button
variant="danger"
size="sm"
title={TEXTS.remove}
aria-label={TEXTS.remove}
onClick={() => onRemove(item.id)}
>
−
</Button>
</div>
))}
</div>
);