feat(projects): explicit organization (executor) selector
Backend:
- POST /api/projects accepts optional organizationId; default = active org
- GET /api/projects (list) and GET /:id include organization {id,name,shortName}
- /:id and /:id/documents no longer filter by active org — direct link works
across organizations; UI offers to switch active to project's org
Frontend:
- Project create modal: «Фирма (исполнитель)» dropdown, default = active
- Projects list shows «Фирма» column when user has >1 organization
- ProjectEdit shows org-banner with project's organization;
if active org differs, banner has «переключить активную на эту →» button
- ProjectEdit fetches BankAccounts from project's org (not active)
- Bumped clients fetch limit to 1000 to match API max
Org of an existing project cannot be changed (would orphan documents/lines);
to switch fenced — create new project under desired org.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,14 +37,14 @@ export function ProjectEditPage() {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [savedAt, setSavedAt] = useState<Date | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [orgId, setOrgId] = useState<string | null>(null);
|
||||
const [activeOrgId, setActiveOrgId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
void Promise.all([
|
||||
api.get<Project>(`/api/projects/${id}`),
|
||||
api.get<{ items: DocumentSummary[] }>(`/api/projects/${id}/documents`),
|
||||
api.get<{ items: Client[] }>('/api/clients?limit=500'),
|
||||
api.get<{ items: Client[] }>('/api/clients?limit=1000'),
|
||||
api.get<{ items: DocumentTemplate[] }>('/api/templates'),
|
||||
api.get<{ id: string }>('/api/active-organization'),
|
||||
])
|
||||
@@ -54,13 +54,26 @@ export function ProjectEditPage() {
|
||||
setDocs(ds.items);
|
||||
setClients(cs.items);
|
||||
setTemplates(ts.items);
|
||||
setOrgId(ao.id);
|
||||
const ba = await api.get<{ items: BankAccount[] }>(`/api/organizations/${ao.id}/bank-accounts`);
|
||||
setActiveOrgId(ao.id);
|
||||
// Банк-счета и шаблоны проекта берём из ФИРМЫ ПРОЕКТА, а не из активной.
|
||||
const ba = await api.get<{ items: BankAccount[] }>(
|
||||
`/api/organizations/${p.organizationId}/bank-accounts`,
|
||||
);
|
||||
setBankAccounts(ba.items);
|
||||
})
|
||||
.catch((e) => setError(String(e)));
|
||||
}, [id]);
|
||||
|
||||
async function switchToProjectOrg() {
|
||||
if (!project) return;
|
||||
try {
|
||||
await api.post('/api/active-organization', { id: project.organizationId });
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!id || !draft) return;
|
||||
setSaving(true);
|
||||
@@ -94,6 +107,8 @@ export function ProjectEditPage() {
|
||||
if (error && !project) return <main className="content"><div className="error-text">{error}</div></main>;
|
||||
if (!project) return <main className="content"><p className="hint">Загрузка…</p></main>;
|
||||
|
||||
const isCrossOrg = activeOrgId && project.organizationId && activeOrgId !== project.organizationId;
|
||||
|
||||
return (
|
||||
<main className="content">
|
||||
<header className="page-head">
|
||||
@@ -101,6 +116,16 @@ export function ProjectEditPage() {
|
||||
<Button onClick={() => navigate('/projects')}>← К проектам</Button>
|
||||
</header>
|
||||
|
||||
<div className="org-banner">
|
||||
<span className="hint">Фирма-исполнитель:</span>
|
||||
<b>{project.organization?.shortName || project.organization?.name || '—'}</b>
|
||||
{isCrossOrg ? (
|
||||
<Button variant="ghost" onClick={switchToProjectOrg}>
|
||||
переключить активную на эту →
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<section className="form-grid">
|
||||
<Field
|
||||
label="Название"
|
||||
|
||||
Reference in New Issue
Block a user