191 lines
5.7 KiB
JavaScript
191 lines
5.7 KiB
JavaScript
/**
|
|
* @file draftSync.js
|
|
* @description Virtual Drafts synchronizer for VFS drafts (drafts/quote, drafts/order, drafts/delivery).
|
|
* Connects frontend draft automats (quoteDraftAutomat, orderDraftAutomat, deliveryEditorAutomat)
|
|
* with backend VFS requests and broadcasts.
|
|
*
|
|
* Incoming Signals:
|
|
* - SSE 'draft:request': Backend requests a draft (read or list).
|
|
* - SSE 'draft': Backend broadcasts an external update to a draft.
|
|
*
|
|
* Outgoing Calls:
|
|
* - POST /api/vfs/drafts/response: Responds to backend requests with draft content or file list.
|
|
*/
|
|
|
|
import { deliveryEditorAutomat, createEmptyLineitem } from './deliveryEditorAutomat';
|
|
import { quoteDraftAutomat, orderDraftAutomat } from './manifestDraftAutomat';
|
|
import { vfs } from '../api/vfs';
|
|
import { getStoredClientId } from '../api/http';
|
|
import { getSseClientId } from './sseAutomat';
|
|
|
|
/**
|
|
* Normalizes a draft filename/path to its canonical key ('quote', 'order', 'delivery').
|
|
* Handles paths like 'drafts/quote', 'drafts/quote.json', '/api/vfs/drafts/order', 'quote', etc.
|
|
*
|
|
* @param {string} [name]
|
|
* @returns {string} Normalized key
|
|
*/
|
|
export function normalizeDraftName(name) {
|
|
if (!name) return '';
|
|
return String(name)
|
|
.trim()
|
|
.replace(/^\/?(api\/)?vfs\//, '')
|
|
.replace(/^drafts\//, '')
|
|
.replace(/\.json$/, '')
|
|
.toLowerCase();
|
|
}
|
|
|
|
/**
|
|
* Supported virtual draft names.
|
|
*/
|
|
export const SUPPORTED_DRAFTS = ['quote', 'order', 'delivery'];
|
|
|
|
/**
|
|
* Timestamps of last modifications per draft.
|
|
* @type {Map<string, string>}
|
|
*/
|
|
const draftTimestamps = new Map();
|
|
|
|
/**
|
|
* Resolves active draft content for a given draft name from its managing automat.
|
|
*
|
|
* @param {string} rawName
|
|
* @returns {object|null}
|
|
*/
|
|
export function getActiveDraftContent(rawName) {
|
|
const name = normalizeDraftName(rawName);
|
|
if (name === 'quote') {
|
|
return quoteDraftAutomat.actions.getManifest();
|
|
}
|
|
if (name === 'order') {
|
|
return orderDraftAutomat.actions.getManifest();
|
|
}
|
|
if (name === 'delivery') {
|
|
return deliveryEditorAutomat.actions.getManifest();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Generates the list of virtual draft files for the VFS listing.
|
|
*
|
|
* @returns {Array<{ name: string, size: number, updated_at: string }>}
|
|
*/
|
|
export function getActiveDraftsList() {
|
|
const now = new Date().toISOString();
|
|
return SUPPORTED_DRAFTS.map((name) => {
|
|
const content = getActiveDraftContent(name);
|
|
let size = 0;
|
|
if (content != null) {
|
|
try {
|
|
size = JSON.stringify(content).length;
|
|
} catch {}
|
|
}
|
|
const updatedAt = draftTimestamps.get(name) || (size > 0 ? now : '2026-10-14T00:00:00.000Z');
|
|
return {
|
|
name,
|
|
size,
|
|
updated_at: updatedAt,
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Applies incoming draft content update to the managing automat.
|
|
*
|
|
* @param {string} rawName
|
|
* @param {object|string} rawContent
|
|
*/
|
|
export function applyDraftUpdate(rawName, rawContent) {
|
|
const name = normalizeDraftName(rawName);
|
|
if (!name) return;
|
|
|
|
let content = rawContent;
|
|
if (typeof content === 'string') {
|
|
try {
|
|
content = JSON.parse(content);
|
|
} catch (err) {
|
|
console.error(`[draft] failed to parse incoming draft string for ${name}:`, err);
|
|
return;
|
|
}
|
|
}
|
|
|
|
draftTimestamps.set(name, new Date().toISOString());
|
|
|
|
try {
|
|
if (name === 'quote') {
|
|
quoteDraftAutomat.actions.setManifest(content);
|
|
} else if (name === 'order') {
|
|
orderDraftAutomat.actions.setManifest(content);
|
|
} else if (name === 'delivery') {
|
|
const manifest = content?.manifest || content || {};
|
|
const rawLines = manifest.lines ?? manifest.lineitems ?? manifest.items ?? [];
|
|
const lines = Array.isArray(rawLines)
|
|
? rawLines.map((l) => (l && l.key ? l : createEmptyLineitem(l)))
|
|
: [];
|
|
const partyId = manifest.partyId ?? manifest.deliveryEntity ?? manifest.supplierId ?? null;
|
|
const externalNumber = manifest.externalNumber ?? manifest.external_number ?? null;
|
|
|
|
deliveryEditorAutomat.actions.setDelivery({
|
|
...manifest,
|
|
partyId,
|
|
deliveryEntity: partyId,
|
|
externalNumber,
|
|
external_number: externalNumber,
|
|
lines,
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error(`[draft] error applying draft update to ${name} automat:`, err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles incoming 'draft:request' SSE event from backend.
|
|
*
|
|
* @param {{ requestId: string, action: 'read'|'list', name?: string, target?: string }} payload
|
|
* @returns {Promise<void>}
|
|
*/
|
|
export async function handleDraftRequest(payload) {
|
|
if (!payload || !payload.requestId) return;
|
|
const { requestId, action, name, target } = payload;
|
|
|
|
try {
|
|
if (action === 'read') {
|
|
const draftName = normalizeDraftName(name || target);
|
|
const content = getActiveDraftContent(draftName);
|
|
if (content != null) {
|
|
await vfs.respondDraft({ requestId, content });
|
|
} else {
|
|
await vfs.respondDraft({ requestId, error: 'Draft not open' });
|
|
}
|
|
} else if (action === 'list') {
|
|
const files = getActiveDraftsList();
|
|
await vfs.respondDraft({ requestId, files });
|
|
}
|
|
} catch (err) {
|
|
console.error('[draft] error handling draft:request:', err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles incoming 'draft' SSE broadcast from backend.
|
|
*
|
|
* @param {{ sender?: string, action: string, name?: string, target?: string, content?: any }} payload
|
|
*/
|
|
export function handleDraftEvent(payload) {
|
|
if (!payload) return;
|
|
const { sender, action, name, target, content } = payload;
|
|
|
|
const myClientId = getStoredClientId() || (typeof getSseClientId === 'function' ? getSseClientId() : null);
|
|
if (sender && myClientId && sender === myClientId) {
|
|
// Ignore self-echo
|
|
return;
|
|
}
|
|
|
|
if (action === 'update') {
|
|
const draftName = normalizeDraftName(name || target);
|
|
applyDraftUpdate(draftName, content);
|
|
}
|
|
}
|