@things-factory/erp-systemever 10.1.117 → 10.1.118
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/dist-server/index.d.ts +8 -0
- package/dist-server/index.js +9 -4
- package/dist-server/index.js.map +1 -1
- package/dist-server/systemever-adapter.js +12 -20
- package/dist-server/systemever-adapter.js.map +1 -1
- package/dist-server/systemever-connector.d.ts +1 -0
- package/dist-server/systemever-connector.js +88 -0
- package/dist-server/systemever-connector.js.map +1 -0
- package/dist-server/systemever-query-policy.d.ts +30 -0
- package/dist-server/systemever-query-policy.js +37 -0
- package/dist-server/systemever-query-policy.js.map +1 -0
- package/dist-server/systemever-query-response.d.ts +2 -0
- package/dist-server/systemever-query-response.js +26 -0
- package/dist-server/systemever-query-response.js.map +1 -0
- package/dist-server/systemever-query-runner.d.ts +16 -0
- package/dist-server/systemever-query-runner.js +90 -0
- package/dist-server/systemever-query-runner.js.map +1 -0
- package/dist-server/systemever-query-task.d.ts +7 -0
- package/dist-server/systemever-query-task.js +65 -0
- package/dist-server/systemever-query-task.js.map +1 -0
- package/dist-server/systemever-schedule-task.d.ts +1 -0
- package/dist-server/systemever-schedule-task.js +35 -0
- package/dist-server/systemever-schedule-task.js.map +1 -0
- package/dist-server/systemever-work-order-query.d.ts +14 -0
- package/dist-server/systemever-work-order-query.js +157 -0
- package/dist-server/systemever-work-order-query.js.map +1 -0
- package/dist-server/systemever-work-order.d.ts +33 -0
- package/dist-server/systemever-work-order.js +160 -0
- package/dist-server/systemever-work-order.js.map +1 -0
- package/logs/.5e5d741d8b7784a2fbad65eedc0fd46946aaf6f2-audit.json +15 -0
- package/logs/connections-2026-10-05-11.log +0 -0
- package/package.json +5 -3
- package/server/index.ts +8 -0
- package/server/systemever-connector.ts +88 -0
- package/server/systemever-query-policy.ts +47 -0
- package/server/systemever-query-response.ts +29 -0
- package/server/systemever-query-runner.ts +79 -0
- package/server/systemever-query-task.ts +62 -0
- package/server/systemever-schedule-task.ts +37 -0
- package/server/systemever-work-order-query.ts +157 -0
- package/server/systemever-work-order.ts +184 -0
- package/test/fixtures/work-order-query-request.json +153 -0
- package/test/fixtures/work-order-query.json +239 -0
- package/test/systemever-connector.test.ts +84 -0
- package/test/systemever-query-policy.test.ts +41 -0
- package/test/systemever-query-response.test.ts +37 -0
- package/test/systemever-query-runner.test.ts +73 -0
- package/test/systemever-query-task.test.ts +106 -0
- package/test/systemever-schedule-task.test.ts +46 -0
- package/test/systemever-work-order.test.ts +101 -0
- package/tsconfig.json +2 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The least time between two queries of one screen over one connection in one domain — a person does not press
|
|
3
|
+
* 조회 more often than this, and a schedule must not either (ADR-0110, ruling 2026-10-05, condition 3).
|
|
4
|
+
*/
|
|
5
|
+
export declare const SYSTEMEVER_QUERY_FLOOR_MS = 60000;
|
|
6
|
+
/** When each (domain, connection, screen) last started a query, in this process. */
|
|
7
|
+
export declare const lastQueryStartedAt: Map<string, number>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import integration from '@things-factory/integration-base';
|
|
2
|
+
import { prepareSystemEverQuery, runSystemEverQuery } from './systemever-query-runner.js';
|
|
3
|
+
import { SystemEverQueryError } from './systemever-query-policy.js';
|
|
4
|
+
const { ConnectionManager, TaskRegistry } = integration;
|
|
5
|
+
/**
|
|
6
|
+
* The least time between two queries of one screen over one connection in one domain — a person does not press
|
|
7
|
+
* 조회 more often than this, and a schedule must not either (ADR-0110, ruling 2026-10-05, condition 3).
|
|
8
|
+
*/
|
|
9
|
+
export const SYSTEMEVER_QUERY_FLOOR_MS = 60_000;
|
|
10
|
+
/** When each (domain, connection, screen) last started a query, in this process. */
|
|
11
|
+
export const lastQueryStartedAt = new Map();
|
|
12
|
+
/** Refusals that are not ours to rename: they already say what happened and where (e.g. integration-runtime-off). */
|
|
13
|
+
function isNamedPlatformRefusal(error) {
|
|
14
|
+
const code = error?.code;
|
|
15
|
+
return typeof code === 'string' && code.startsWith('integration-');
|
|
16
|
+
}
|
|
17
|
+
async function SystemEverQuery(step, { domain }) {
|
|
18
|
+
try {
|
|
19
|
+
prepareSystemEverQuery(step.params);
|
|
20
|
+
const key = `${domain?.id}:${step.connection}:${step.params?.screen}`;
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
const last = lastQueryStartedAt.get(key);
|
|
23
|
+
if (last !== undefined && now - last < SYSTEMEVER_QUERY_FLOOR_MS) {
|
|
24
|
+
throw new SystemEverQueryError('systemever-query-too-frequent');
|
|
25
|
+
}
|
|
26
|
+
lastQueryStartedAt.set(key, now);
|
|
27
|
+
const entity = await ConnectionManager.getConnectionEntityByName(domain, step.connection);
|
|
28
|
+
if (!entity || entity.type !== 'systemever-connector' ||
|
|
29
|
+
new URL(entity.endpoint).origin !== 'https://mfg.systemevererp.com') {
|
|
30
|
+
throw new SystemEverQueryError('systemever-query-invalid-connection');
|
|
31
|
+
}
|
|
32
|
+
const instance = await ConnectionManager.getConnectionInstanceByName(domain, step.connection);
|
|
33
|
+
const connection = { ...instance, type: entity.type, name: entity.name, domain };
|
|
34
|
+
const data = await ConnectionManager.withSession(connection, async (page) => {
|
|
35
|
+
try {
|
|
36
|
+
return await runSystemEverQuery(page, step.params);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (!(error instanceof SystemEverQueryError) || error.code !== 'systemever-query-session-expired')
|
|
40
|
+
throw error;
|
|
41
|
+
const recovered = await connection.reAuthenticateSession();
|
|
42
|
+
try {
|
|
43
|
+
return await runSystemEverQuery(recovered.page, step.params);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
await connection.releasePage(recovered.page);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return { data };
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error instanceof SystemEverQueryError || isNamedPlatformRefusal(error))
|
|
54
|
+
throw error;
|
|
55
|
+
throw new SystemEverQueryError('systemever-query-connection-failed', error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
SystemEverQuery.parameterSpec = [
|
|
59
|
+
{ type: 'select', name: 'screen', label: 'screen', property: { options: [{ display: '작업지시조회', value: 'work-order' }] } },
|
|
60
|
+
{ type: 'string', name: 'dateFrom', label: 'date-from' },
|
|
61
|
+
{ type: 'string', name: 'dateTo', label: 'date-to' },
|
|
62
|
+
{ type: 'string', name: 'bizUnit', label: 'business-unit' }
|
|
63
|
+
];
|
|
64
|
+
TaskRegistry.registerTaskHandler('systemever-query', SystemEverQuery);
|
|
65
|
+
//# sourceMappingURL=systemever-query-task.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"systemever-query-task.js","sourceRoot":"","sources":["../server/systemever-query-task.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,kCAAkC,CAAA;AAC1D,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAA;AACzF,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAA;AACnE,MAAM,EAAE,iBAAiB,EAAE,YAAY,EAAE,GAAG,WAAW,CAAA;AAEvD;;;GAGG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,MAAM,CAAA;AAE/C,oFAAoF;AACpF,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,GAAG,EAAkB,CAAA;AAE3D,qHAAqH;AACrH,SAAS,sBAAsB,CAAC,KAAc;IAC5C,MAAM,IAAI,GAAI,KAAa,EAAE,IAAI,CAAA;IACjC,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,CAAA;AACpE,CAAC;AAED,KAAK,UAAU,eAAe,CAAC,IAAS,EAAE,EAAE,MAAM,EAAO;IACvD,IAAI,CAAC;QACH,sBAAsB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QACnC,MAAM,GAAG,GAAG,GAAG,MAAM,EAAE,EAAE,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,CAAA;QACrE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;QACtB,MAAM,IAAI,GAAG,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACxC,IAAI,IAAI,KAAK,SAAS,IAAI,GAAG,GAAG,IAAI,GAAG,yBAAyB,EAAE,CAAC;YACjE,MAAM,IAAI,oBAAoB,CAAC,+BAA+B,CAAC,CAAA;QACjE,CAAC;QACD,kBAAkB,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;QAChC,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,yBAAyB,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;QACzF,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,KAAK,sBAAsB;YACjD,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,KAAK,+BAA+B,EAAE,CAAC;YACxE,MAAM,IAAI,oBAAoB,CAAC,qCAAqC,CAAC,CAAA;QACvE,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,iBAAiB,CAAC,2BAA2B,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;QAC7F,MAAM,UAAU,GAAG,EAAE,GAAG,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,CAAA;QAChF,MAAM,IAAI,GAAG,MAAM,iBAAiB,CAAC,WAAW,CAAC,UAAU,EAAE,KAAK,EAAC,IAAI,EAAC,EAAE;YACxE,IAAI,CAAC;gBACH,OAAO,MAAM,kBAAkB,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;YACpD,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,CAAC,CAAC,KAAK,YAAY,oBAAoB,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,kCAAkC;oBAAE,MAAM,KAAK,CAAA;gBAC9G,MAAM,SAAS,GAAG,MAAM,UAAU,CAAC,qBAAqB,EAAE,CAAA;gBAC1D,IAAI,CAAC;oBAAC,OAAO,MAAM,kBAAkB,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;gBAAC,CAAC;wBAC5D,CAAC;oBAAC,MAAM,UAAU,CAAC,WAAW,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;gBAAC,CAAC;YAC1D,CAAC;QACH,CAAC,CAAC,CAAA;QACF,OAAO,EAAE,IAAI,EAAE,CAAA;IACjB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,oBAAoB,IAAI,sBAAsB,CAAC,KAAK,CAAC;YAAE,MAAM,KAAK,CAAA;QACvF,MAAM,IAAI,oBAAoB,CAAC,oCAAoC,EAAE,KAAK,CAAC,CAAA;IAC7E,CAAC;AACH,CAAC;AAED,eAAe,CAAC,aAAa,GAAG;IAC9B,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,EAAE,EAAE;IACxH,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,EAAE;IACxD,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE;IACpD,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,eAAe,EAAE;CAC5D,CAAA;AAED,YAAY,CAAC,mBAAmB,CAAC,kBAAkB,EAAE,eAAe,CAAC,CAAA","sourcesContent":["import integration from '@things-factory/integration-base'\nimport { prepareSystemEverQuery, runSystemEverQuery } from './systemever-query-runner.js'\nimport { SystemEverQueryError } from './systemever-query-policy.js'\nconst { ConnectionManager, TaskRegistry } = integration\n\n/**\n * The least time between two queries of one screen over one connection in one domain — a person does not press\n * 조회 more often than this, and a schedule must not either (ADR-0110, ruling 2026-10-05, condition 3).\n */\nexport const SYSTEMEVER_QUERY_FLOOR_MS = 60_000\n\n/** When each (domain, connection, screen) last started a query, in this process. */\nexport const lastQueryStartedAt = new Map<string, number>()\n\n/** Refusals that are not ours to rename: they already say what happened and where (e.g. integration-runtime-off). */\nfunction isNamedPlatformRefusal(error: unknown): boolean {\n const code = (error as any)?.code\n return typeof code === 'string' && code.startsWith('integration-')\n}\n\nasync function SystemEverQuery(step: any, { domain }: any) {\n try {\n prepareSystemEverQuery(step.params)\n const key = `${domain?.id}:${step.connection}:${step.params?.screen}`\n const now = Date.now()\n const last = lastQueryStartedAt.get(key)\n if (last !== undefined && now - last < SYSTEMEVER_QUERY_FLOOR_MS) {\n throw new SystemEverQueryError('systemever-query-too-frequent')\n }\n lastQueryStartedAt.set(key, now)\n const entity = await ConnectionManager.getConnectionEntityByName(domain, step.connection)\n if (!entity || entity.type !== 'systemever-connector' ||\n new URL(entity.endpoint).origin !== 'https://mfg.systemevererp.com') {\n throw new SystemEverQueryError('systemever-query-invalid-connection')\n }\n const instance = await ConnectionManager.getConnectionInstanceByName(domain, step.connection)\n const connection = { ...instance, type: entity.type, name: entity.name, domain }\n const data = await ConnectionManager.withSession(connection, async page => {\n try {\n return await runSystemEverQuery(page, step.params)\n } catch (error) {\n if (!(error instanceof SystemEverQueryError) || error.code !== 'systemever-query-session-expired') throw error\n const recovered = await connection.reAuthenticateSession()\n try { return await runSystemEverQuery(recovered.page, step.params) }\n finally { await connection.releasePage(recovered.page) }\n }\n })\n return { data }\n } catch (error) {\n if (error instanceof SystemEverQueryError || isNamedPlatformRefusal(error)) throw error\n throw new SystemEverQueryError('systemever-query-connection-failed', error)\n }\n}\n\nSystemEverQuery.parameterSpec = [\n { type: 'select', name: 'screen', label: 'screen', property: { options: [{ display: '작업지시조회', value: 'work-order' }] } },\n { type: 'string', name: 'dateFrom', label: 'date-from' },\n { type: 'string', name: 'dateTo', label: 'date-to' },\n { type: 'string', name: 'bizUnit', label: 'business-unit' }\n]\n\nTaskRegistry.registerTaskHandler('systemever-query', SystemEverQuery)\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import integration from '@things-factory/integration-base';
|
|
2
|
+
import utils from '@things-factory/utils';
|
|
3
|
+
import { SystemEverQueryError } from './systemever-query-policy.js';
|
|
4
|
+
import { toScheduleEntries } from './systemever-work-order.js';
|
|
5
|
+
const { TaskRegistry } = integration;
|
|
6
|
+
const { access } = utils;
|
|
7
|
+
/**
|
|
8
|
+
* **SystemEver work-order rows → contract schedule entries**, as a scenario step (ADR-0110 addition 1 amended).
|
|
9
|
+
*
|
|
10
|
+
* `systemever-query` returns the 작업지시조회 rows with SystemEver's column names; this step maps them with the same
|
|
11
|
+
* `toScheduleEntries` the adapter would use, so the plant task that comes next receives only the contract's
|
|
12
|
+
* `ProductionScheduleEntry` and knows nothing of SystemEver. Orders it cannot map are passed on as `skipped`,
|
|
13
|
+
* each with its reason — none is dropped without a word.
|
|
14
|
+
*/
|
|
15
|
+
async function SystemEverWorkOrdersToSchedule(step, { data }) {
|
|
16
|
+
const { accessor, timeZone } = step.params || {};
|
|
17
|
+
const rows = accessor ? access(accessor, data) : undefined;
|
|
18
|
+
if (!Array.isArray(rows))
|
|
19
|
+
throw new SystemEverQueryError('systemever-schedule-invalid-rows');
|
|
20
|
+
if (typeof timeZone !== 'string' || !timeZone.trim())
|
|
21
|
+
throw new SystemEverQueryError('systemever-schedule-invalid-time-zone');
|
|
22
|
+
try {
|
|
23
|
+
return { data: toScheduleEntries(rows, { timeZone }) };
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
/* toScheduleEntries throws only for the zone: missing or unknown. */
|
|
27
|
+
throw new SystemEverQueryError('systemever-schedule-invalid-time-zone', error);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
SystemEverWorkOrdersToSchedule.parameterSpec = [
|
|
31
|
+
{ type: 'scenario-step-input', name: 'accessor', label: 'accessor' },
|
|
32
|
+
{ type: 'string', name: 'timeZone', label: 'time-zone' }
|
|
33
|
+
];
|
|
34
|
+
TaskRegistry.registerTaskHandler('systemever-work-orders-to-schedule', SystemEverWorkOrdersToSchedule);
|
|
35
|
+
//# sourceMappingURL=systemever-schedule-task.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"systemever-schedule-task.js","sourceRoot":"","sources":["../server/systemever-schedule-task.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,kCAAkC,CAAA;AAC1D,OAAO,KAAK,MAAM,uBAAuB,CAAA;AAEzC,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAA;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAA;AAE9D,MAAM,EAAE,YAAY,EAAE,GAAG,WAAW,CAAA;AACpC,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,CAAA;AAExB;;;;;;;GAOG;AACH,KAAK,UAAU,8BAA8B,CAAC,IAAS,EAAE,EAAE,IAAI,EAAO;IACpE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,MAAM,IAAI,EAAE,CAAA;IAChD,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAC1D,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,oBAAoB,CAAC,kCAAkC,CAAC,CAAA;IAC5F,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE;QAAE,MAAM,IAAI,oBAAoB,CAAC,uCAAuC,CAAC,CAAA;IAE7H,IAAI,CAAC;QACH,OAAO,EAAE,IAAI,EAAE,iBAAiB,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAA;IACxD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,qEAAqE;QACrE,MAAM,IAAI,oBAAoB,CAAC,uCAAuC,EAAE,KAAK,CAAC,CAAA;IAChF,CAAC;AACH,CAAC;AAED,8BAA8B,CAAC,aAAa,GAAG;IAC7C,EAAE,IAAI,EAAE,qBAAqB,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE;IACpE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,EAAE;CACzD,CAAA;AAED,YAAY,CAAC,mBAAmB,CAAC,oCAAoC,EAAE,8BAA8B,CAAC,CAAA","sourcesContent":["import integration from '@things-factory/integration-base'\nimport utils from '@things-factory/utils'\n\nimport { SystemEverQueryError } from './systemever-query-policy.js'\nimport { toScheduleEntries } from './systemever-work-order.js'\n\nconst { TaskRegistry } = integration\nconst { access } = utils\n\n/**\n * **SystemEver work-order rows → contract schedule entries**, as a scenario step (ADR-0110 addition 1 amended).\n *\n * `systemever-query` returns the 작업지시조회 rows with SystemEver's column names; this step maps them with the same\n * `toScheduleEntries` the adapter would use, so the plant task that comes next receives only the contract's\n * `ProductionScheduleEntry` and knows nothing of SystemEver. Orders it cannot map are passed on as `skipped`,\n * each with its reason — none is dropped without a word.\n */\nasync function SystemEverWorkOrdersToSchedule(step: any, { data }: any) {\n const { accessor, timeZone } = step.params || {}\n const rows = accessor ? access(accessor, data) : undefined\n if (!Array.isArray(rows)) throw new SystemEverQueryError('systemever-schedule-invalid-rows')\n if (typeof timeZone !== 'string' || !timeZone.trim()) throw new SystemEverQueryError('systemever-schedule-invalid-time-zone')\n\n try {\n return { data: toScheduleEntries(rows, { timeZone }) }\n } catch (error) {\n /* toScheduleEntries throws only for the zone: missing or unknown. */\n throw new SystemEverQueryError('systemever-schedule-invalid-time-zone', error)\n }\n}\n\nSystemEverWorkOrdersToSchedule.parameterSpec = [\n { type: 'scenario-step-input', name: 'accessor', label: 'accessor' },\n { type: 'string', name: 'timeZone', label: 'time-zone' }\n]\n\nTaskRegistry.registerTaskHandler('systemever-work-orders-to-schedule', SystemEverWorkOrdersToSchedule)\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function workOrderQueryData(params: {
|
|
2
|
+
dateFrom: string;
|
|
3
|
+
dateTo: string;
|
|
4
|
+
bizUnit: string;
|
|
5
|
+
}): {
|
|
6
|
+
Tables: {
|
|
7
|
+
TableName: string;
|
|
8
|
+
Columns: string[];
|
|
9
|
+
ColumnsType: number[];
|
|
10
|
+
Rows: (string | number)[][];
|
|
11
|
+
}[];
|
|
12
|
+
IsSendXml: boolean;
|
|
13
|
+
DataBlock1: string;
|
|
14
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { SystemEverQueryError } from './systemever-query-policy.js';
|
|
2
|
+
// Captured from a read-only work-order query on 2026-10-05. No session values.
|
|
3
|
+
const template = {
|
|
4
|
+
"TableName": "DataBlock1",
|
|
5
|
+
"Columns": [
|
|
6
|
+
"WorkingTag",
|
|
7
|
+
"IDX_NO",
|
|
8
|
+
"DataSeq",
|
|
9
|
+
"Status",
|
|
10
|
+
"Selected",
|
|
11
|
+
"IsChangedMst",
|
|
12
|
+
"TABLE_NAME",
|
|
13
|
+
"IsSum",
|
|
14
|
+
"ItemClassSSeq",
|
|
15
|
+
"ItemClassSName",
|
|
16
|
+
"ItemClassLSeq",
|
|
17
|
+
"ItemClassLName",
|
|
18
|
+
"ItemClassMSeq",
|
|
19
|
+
"BizUnit",
|
|
20
|
+
"BizUnitName",
|
|
21
|
+
"WorkDateFr",
|
|
22
|
+
"WorkDateTo",
|
|
23
|
+
"WorkOrderNo",
|
|
24
|
+
"ProgStatus",
|
|
25
|
+
"ProgStatusName",
|
|
26
|
+
"WorkCenterSeq",
|
|
27
|
+
"WorkCenterName",
|
|
28
|
+
"DeptSeq",
|
|
29
|
+
"DeptName",
|
|
30
|
+
"ProcSeq",
|
|
31
|
+
"ProcName",
|
|
32
|
+
"WorkType",
|
|
33
|
+
"WorkTypeName",
|
|
34
|
+
"GoodItemName",
|
|
35
|
+
"GoodItemNo",
|
|
36
|
+
"GoodItemSpec",
|
|
37
|
+
"WorkOrderDateFr",
|
|
38
|
+
"WorkOrderDateTo",
|
|
39
|
+
"FileSeq",
|
|
40
|
+
"ProdPlanNo",
|
|
41
|
+
"OrderNo",
|
|
42
|
+
"CustSeq",
|
|
43
|
+
"CustName",
|
|
44
|
+
"WHSeq",
|
|
45
|
+
"WHName",
|
|
46
|
+
"PoNo"
|
|
47
|
+
],
|
|
48
|
+
"ColumnsType": [
|
|
49
|
+
0,
|
|
50
|
+
1,
|
|
51
|
+
1,
|
|
52
|
+
1,
|
|
53
|
+
1,
|
|
54
|
+
0,
|
|
55
|
+
0,
|
|
56
|
+
0,
|
|
57
|
+
0,
|
|
58
|
+
0,
|
|
59
|
+
0,
|
|
60
|
+
0,
|
|
61
|
+
0,
|
|
62
|
+
0,
|
|
63
|
+
0,
|
|
64
|
+
0,
|
|
65
|
+
0,
|
|
66
|
+
0,
|
|
67
|
+
0,
|
|
68
|
+
0,
|
|
69
|
+
0,
|
|
70
|
+
0,
|
|
71
|
+
0,
|
|
72
|
+
0,
|
|
73
|
+
0,
|
|
74
|
+
0,
|
|
75
|
+
0,
|
|
76
|
+
0,
|
|
77
|
+
0,
|
|
78
|
+
0,
|
|
79
|
+
0,
|
|
80
|
+
0,
|
|
81
|
+
0,
|
|
82
|
+
0,
|
|
83
|
+
0,
|
|
84
|
+
0,
|
|
85
|
+
0,
|
|
86
|
+
0,
|
|
87
|
+
0,
|
|
88
|
+
0,
|
|
89
|
+
0
|
|
90
|
+
],
|
|
91
|
+
"Rows": [
|
|
92
|
+
[
|
|
93
|
+
"A",
|
|
94
|
+
1,
|
|
95
|
+
1,
|
|
96
|
+
0,
|
|
97
|
+
1,
|
|
98
|
+
"0",
|
|
99
|
+
"DataBlock1",
|
|
100
|
+
"0",
|
|
101
|
+
"",
|
|
102
|
+
"",
|
|
103
|
+
"",
|
|
104
|
+
"",
|
|
105
|
+
"",
|
|
106
|
+
"1",
|
|
107
|
+
"본사",
|
|
108
|
+
"20261005",
|
|
109
|
+
"20261012",
|
|
110
|
+
"",
|
|
111
|
+
"",
|
|
112
|
+
"",
|
|
113
|
+
"",
|
|
114
|
+
"",
|
|
115
|
+
"",
|
|
116
|
+
"",
|
|
117
|
+
"",
|
|
118
|
+
"",
|
|
119
|
+
"",
|
|
120
|
+
"",
|
|
121
|
+
"",
|
|
122
|
+
"",
|
|
123
|
+
"",
|
|
124
|
+
"",
|
|
125
|
+
"",
|
|
126
|
+
0,
|
|
127
|
+
"",
|
|
128
|
+
"",
|
|
129
|
+
"",
|
|
130
|
+
"",
|
|
131
|
+
"",
|
|
132
|
+
"",
|
|
133
|
+
""
|
|
134
|
+
]
|
|
135
|
+
]
|
|
136
|
+
};
|
|
137
|
+
export function workOrderQueryData(params) {
|
|
138
|
+
for (const value of [params.dateFrom, params.dateTo]) {
|
|
139
|
+
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value) ||
|
|
140
|
+
!Number.isFinite(Date.parse(value)) || new Date(value).toISOString().slice(0, 10) !== value) {
|
|
141
|
+
throw new SystemEverQueryError('systemever-query-invalid-date');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (params.dateFrom > params.dateTo)
|
|
145
|
+
throw new SystemEverQueryError('systemever-query-invalid-date-range');
|
|
146
|
+
if (typeof params.bizUnit !== 'string' || !/^\d+$/.test(params.bizUnit)) {
|
|
147
|
+
throw new SystemEverQueryError('systemever-query-invalid-biz-unit');
|
|
148
|
+
}
|
|
149
|
+
const table = structuredClone(template);
|
|
150
|
+
const row = table.Rows[0];
|
|
151
|
+
row[table.Columns.indexOf('WorkDateFr')] = params.dateFrom.replaceAll('-', '');
|
|
152
|
+
row[table.Columns.indexOf('WorkDateTo')] = params.dateTo.replaceAll('-', '');
|
|
153
|
+
row[table.Columns.indexOf('BizUnit')] = params.bizUnit;
|
|
154
|
+
row[table.Columns.indexOf('BizUnitName')] = '';
|
|
155
|
+
return { Tables: [table], IsSendXml: true, DataBlock1: 'DataBlock1' };
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=systemever-work-order-query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"systemever-work-order-query.js","sourceRoot":"","sources":["../server/systemever-work-order-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { SystemEverQueryError } from './systemever-query-policy.js'\n\n// Captured from a read-only work-order query on 2026-10-05. No session values.\nconst template = {\n \"TableName\": \"DataBlock1\",\n \"Columns\": [\n \"WorkingTag\",\n \"IDX_NO\",\n \"DataSeq\",\n \"Status\",\n \"Selected\",\n \"IsChangedMst\",\n \"TABLE_NAME\",\n \"IsSum\",\n \"ItemClassSSeq\",\n \"ItemClassSName\",\n \"ItemClassLSeq\",\n \"ItemClassLName\",\n \"ItemClassMSeq\",\n \"BizUnit\",\n \"BizUnitName\",\n \"WorkDateFr\",\n \"WorkDateTo\",\n \"WorkOrderNo\",\n \"ProgStatus\",\n \"ProgStatusName\",\n \"WorkCenterSeq\",\n \"WorkCenterName\",\n \"DeptSeq\",\n \"DeptName\",\n \"ProcSeq\",\n \"ProcName\",\n \"WorkType\",\n \"WorkTypeName\",\n \"GoodItemName\",\n \"GoodItemNo\",\n \"GoodItemSpec\",\n \"WorkOrderDateFr\",\n \"WorkOrderDateTo\",\n \"FileSeq\",\n \"ProdPlanNo\",\n \"OrderNo\",\n \"CustSeq\",\n \"CustName\",\n \"WHSeq\",\n \"WHName\",\n \"PoNo\"\n ],\n \"ColumnsType\": [\n 0,\n 1,\n 1,\n 1,\n 1,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0\n ],\n \"Rows\": [\n [\n \"A\",\n 1,\n 1,\n 0,\n 1,\n \"0\",\n \"DataBlock1\",\n \"0\",\n \"\",\n \"\",\n \"\",\n \"\",\n \"\",\n \"1\",\n \"본사\",\n \"20261005\",\n \"20261012\",\n \"\",\n \"\",\n \"\",\n \"\",\n \"\",\n \"\",\n \"\",\n \"\",\n \"\",\n \"\",\n \"\",\n \"\",\n \"\",\n \"\",\n \"\",\n \"\",\n 0,\n \"\",\n \"\",\n \"\",\n \"\",\n \"\",\n \"\",\n \"\"\n ]\n ]\n}\n\nexport function workOrderQueryData(params: { dateFrom: string; dateTo: string; bizUnit: string }) {\n for (const value of [params.dateFrom, params.dateTo]) {\n if (typeof value !== 'string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(value) ||\n !Number.isFinite(Date.parse(value)) || new Date(value).toISOString().slice(0, 10) !== value) {\n throw new SystemEverQueryError('systemever-query-invalid-date')\n }\n }\n if (params.dateFrom > params.dateTo) throw new SystemEverQueryError('systemever-query-invalid-date-range')\n if (typeof params.bizUnit !== 'string' || !/^\\d+$/.test(params.bizUnit)) {\n throw new SystemEverQueryError('systemever-query-invalid-biz-unit')\n }\n const table = structuredClone(template)\n const row = table.Rows[0]\n row[table.Columns.indexOf('WorkDateFr')] = params.dateFrom.replaceAll('-', '')\n row[table.Columns.indexOf('WorkDateTo')] = params.dateTo.replaceAll('-', '')\n row[table.Columns.indexOf('BizUnit')] = params.bizUnit\n row[table.Columns.indexOf('BizUnitName')] = ''\n return { Tables: [table], IsSendXml: true, DataBlock1: 'DataBlock1' }\n}\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ProductionScheduleEntry, ScheduleStatus } from '@operato/ops-contract';
|
|
2
|
+
/** The columns this mapping reads — all present in the 2026-10-01 response. */
|
|
3
|
+
export declare const WORK_ORDER_COLUMNS: readonly ["WorkOrderNo", "WorkOrderSerl", "ProgStatusName", "IsStop", "GoodItemNo", "OrderQty", "ItemUnitName", "WorkDate", "WorkStartTime", "WorkEndTime", "WorkCenterName", "LastDateTime"];
|
|
4
|
+
/**
|
|
5
|
+
* SystemEver progress status → contract status. Read by name; only 완료 has been seen (code 6036004).
|
|
6
|
+
* 중단 · 미완료 are left out on purpose (see the file head).
|
|
7
|
+
*/
|
|
8
|
+
export declare const WORK_ORDER_STATUS: Readonly<Record<string, ScheduleStatus>>;
|
|
9
|
+
export type WorkOrderRow = Record<string, unknown>;
|
|
10
|
+
export interface WorkOrderSkip {
|
|
11
|
+
orderId?: string;
|
|
12
|
+
reason: string;
|
|
13
|
+
}
|
|
14
|
+
export interface WorkOrderMapping {
|
|
15
|
+
entries: ProductionScheduleEntry[];
|
|
16
|
+
skipped: WorkOrderSkip[];
|
|
17
|
+
}
|
|
18
|
+
/** A plant-local wall time as an ISO instant. Undefined when the parts are not a time. */
|
|
19
|
+
export declare function localToIso(date: string, time: string, timeZone: string): string | undefined;
|
|
20
|
+
/** The content revision — what the floor would act on, in a fixed order. */
|
|
21
|
+
export declare function contentRevision(e: Omit<ProductionScheduleEntry, 'revision' | 'recordTime' | 'orderId'>): string;
|
|
22
|
+
/**
|
|
23
|
+
* Map a 작업지시조회 response.
|
|
24
|
+
*
|
|
25
|
+
* An order that comes as more than one line (several `WorkOrderSerl`, i.e. several processes) is skipped whole:
|
|
26
|
+
* the contract entry is one order, and picking one line would drop the others. Miratek's in-house orders are one
|
|
27
|
+
* process each; if that changes, this says so instead of guessing.
|
|
28
|
+
*
|
|
29
|
+
* @throws when `timeZone` is missing or unknown — no row can be read without it.
|
|
30
|
+
*/
|
|
31
|
+
export declare function toScheduleEntries(rows: WorkOrderRow[], options: {
|
|
32
|
+
timeZone: string;
|
|
33
|
+
}): WorkOrderMapping;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **SystemEver work orders → contract schedule entries** — a pure mapping, no I/O.
|
|
3
|
+
*
|
|
4
|
+
* Both reading roads reach it (ADR-0110 addition 1, amended 2026-10-05): the adapter's own `readSchedules` when
|
|
5
|
+
* it reads `/api/WebApi` directly, and the plant task when an integration scenario brings the rows. The rows
|
|
6
|
+
* are the 작업지시조회 response as SystemEver sends them (svc 501041 / pgm 500880), column names untouched.
|
|
7
|
+
*
|
|
8
|
+
* ── What was seen (2026-10-01 response, two rows) ────────────────────────
|
|
9
|
+
* WorkDate "20251107" · WorkStartTime "0900" · WorkEndTime "1800" planned day and hours, plant-local
|
|
10
|
+
* OrderQty "200.00000" a decimal in a string
|
|
11
|
+
* ItemUnitName "EA" · GoodItemNo "F0102-H1003" unit and item
|
|
12
|
+
* ProgStatus 6036004 · ProgStatusName "완료" the only status seen
|
|
13
|
+
* WorkOrderNo "20251107-0001" · WorkOrderSerl 1 one line per order
|
|
14
|
+
* LastDateTime "2025-11-28 14:35:13" plant-local
|
|
15
|
+
*
|
|
16
|
+
* ── What it does not decide ──────────────────────────────────────────────
|
|
17
|
+
* · **A status it does not know is not mapped.** Only 완료 was seen. 작성 · 확정 · 진행중 are read by their
|
|
18
|
+
* names; 중단 · 미완료 and anything else skip the order with the reason — read as `cancelled`, an order the
|
|
19
|
+
* floor has not started would be cancelled without a word.
|
|
20
|
+
* · **The revision is the content, not the last update time** (user decision 2026-09-30): a remark edit must
|
|
21
|
+
* not look like a new revision, which holds a started order for a person.
|
|
22
|
+
* · **No zone is assumed.** The plant's zone is required; the ERP's times are local.
|
|
23
|
+
*/
|
|
24
|
+
import { createHash } from 'node:crypto';
|
|
25
|
+
/** The columns this mapping reads — all present in the 2026-10-01 response. */
|
|
26
|
+
export const WORK_ORDER_COLUMNS = [
|
|
27
|
+
'WorkOrderNo',
|
|
28
|
+
'WorkOrderSerl',
|
|
29
|
+
'ProgStatusName',
|
|
30
|
+
'IsStop',
|
|
31
|
+
'GoodItemNo',
|
|
32
|
+
'OrderQty',
|
|
33
|
+
'ItemUnitName',
|
|
34
|
+
'WorkDate',
|
|
35
|
+
'WorkStartTime',
|
|
36
|
+
'WorkEndTime',
|
|
37
|
+
'WorkCenterName',
|
|
38
|
+
'LastDateTime'
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* SystemEver progress status → contract status. Read by name; only 완료 has been seen (code 6036004).
|
|
42
|
+
* 중단 · 미완료 are left out on purpose (see the file head).
|
|
43
|
+
*/
|
|
44
|
+
export const WORK_ORDER_STATUS = {
|
|
45
|
+
작성: 'draft',
|
|
46
|
+
확정: 'released',
|
|
47
|
+
진행중: 'started',
|
|
48
|
+
완료: 'completed'
|
|
49
|
+
};
|
|
50
|
+
const text = (v) => (v == null ? '' : String(v).trim());
|
|
51
|
+
/** Offset of `timeZone` from UTC at `utcMs`, in ms. */
|
|
52
|
+
function offsetMs(utcMs, timeZone) {
|
|
53
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
54
|
+
timeZone,
|
|
55
|
+
hourCycle: 'h23',
|
|
56
|
+
year: 'numeric',
|
|
57
|
+
month: '2-digit',
|
|
58
|
+
day: '2-digit',
|
|
59
|
+
hour: '2-digit',
|
|
60
|
+
minute: '2-digit',
|
|
61
|
+
second: '2-digit'
|
|
62
|
+
}).formatToParts(new Date(utcMs));
|
|
63
|
+
const n = (type) => Number(parts.find(p => p.type === type)?.value);
|
|
64
|
+
return Date.UTC(n('year'), n('month') - 1, n('day'), n('hour'), n('minute'), n('second')) - utcMs;
|
|
65
|
+
}
|
|
66
|
+
/** A plant-local wall time as an ISO instant. Undefined when the parts are not a time. */
|
|
67
|
+
export function localToIso(date, time, timeZone) {
|
|
68
|
+
const d = /^(\d{4})-?(\d{2})-?(\d{2})$/.exec(date);
|
|
69
|
+
const t = /^(\d{2}):?(\d{2})(?::?(\d{2}))?$/.exec(time);
|
|
70
|
+
if (!d || !t)
|
|
71
|
+
return undefined;
|
|
72
|
+
const wall = Date.UTC(+d[1], +d[2] - 1, +d[3], +t[1], +t[2], +(t[3] ?? 0));
|
|
73
|
+
if (!Number.isFinite(wall))
|
|
74
|
+
return undefined;
|
|
75
|
+
/* two passes: the offset at the guessed instant, then at the corrected one (around a DST change) */
|
|
76
|
+
let at = wall - offsetMs(wall, timeZone);
|
|
77
|
+
at = wall - offsetMs(at, timeZone);
|
|
78
|
+
return new Date(at).toISOString();
|
|
79
|
+
}
|
|
80
|
+
/** The content revision — what the floor would act on, in a fixed order. */
|
|
81
|
+
export function contentRevision(e) {
|
|
82
|
+
const content = [e.materialDefinitionId, e.quantity, e.uom, e.startTime ?? '', e.endTime ?? '', e.status];
|
|
83
|
+
return createHash('sha256').update(JSON.stringify(content)).digest('hex').slice(0, 16);
|
|
84
|
+
}
|
|
85
|
+
function one(row, timeZone) {
|
|
86
|
+
const orderId = text(row.WorkOrderNo);
|
|
87
|
+
if (!orderId)
|
|
88
|
+
return { reason: 'no WorkOrderNo' };
|
|
89
|
+
if (text(row.IsStop) === '1')
|
|
90
|
+
return { orderId, reason: 'stopped in SystemEver (IsStop) — not mapped' };
|
|
91
|
+
const statusName = text(row.ProgStatusName);
|
|
92
|
+
const status = WORK_ORDER_STATUS[statusName];
|
|
93
|
+
if (!status)
|
|
94
|
+
return { orderId, reason: `unknown progress status "${statusName}" — not mapped` };
|
|
95
|
+
const materialDefinitionId = text(row.GoodItemNo);
|
|
96
|
+
if (!materialDefinitionId)
|
|
97
|
+
return { orderId, reason: 'no GoodItemNo' };
|
|
98
|
+
const quantity = Number(text(row.OrderQty));
|
|
99
|
+
if (!text(row.OrderQty) || !Number.isFinite(quantity))
|
|
100
|
+
return { orderId, reason: `OrderQty "${text(row.OrderQty)}" is not a number` };
|
|
101
|
+
const uom = text(row.ItemUnitName);
|
|
102
|
+
if (!uom)
|
|
103
|
+
return { orderId, reason: 'no ItemUnitName — a unit is not invented' };
|
|
104
|
+
const day = text(row.WorkDate);
|
|
105
|
+
const startTime = day && text(row.WorkStartTime) ? localToIso(day, text(row.WorkStartTime), timeZone) : undefined;
|
|
106
|
+
const endTime = day && text(row.WorkEndTime) ? localToIso(day, text(row.WorkEndTime), timeZone) : undefined;
|
|
107
|
+
const [lastDay, lastTime] = text(row.LastDateTime).split(/\s+/);
|
|
108
|
+
const recordTime = lastDay && lastTime ? localToIso(lastDay, lastTime, timeZone) : undefined;
|
|
109
|
+
const content = {
|
|
110
|
+
materialDefinitionId,
|
|
111
|
+
quantity,
|
|
112
|
+
uom,
|
|
113
|
+
...(startTime ? { startTime } : {}),
|
|
114
|
+
...(endTime ? { endTime } : {}),
|
|
115
|
+
status
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
orderId,
|
|
119
|
+
...content,
|
|
120
|
+
revision: contentRevision(content),
|
|
121
|
+
...(text(row.WorkCenterName) ? { workCenter: text(row.WorkCenterName) } : {}),
|
|
122
|
+
...(recordTime ? { recordTime } : {})
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Map a 작업지시조회 response.
|
|
127
|
+
*
|
|
128
|
+
* An order that comes as more than one line (several `WorkOrderSerl`, i.e. several processes) is skipped whole:
|
|
129
|
+
* the contract entry is one order, and picking one line would drop the others. Miratek's in-house orders are one
|
|
130
|
+
* process each; if that changes, this says so instead of guessing.
|
|
131
|
+
*
|
|
132
|
+
* @throws when `timeZone` is missing or unknown — no row can be read without it.
|
|
133
|
+
*/
|
|
134
|
+
export function toScheduleEntries(rows, options) {
|
|
135
|
+
const timeZone = text(options?.timeZone);
|
|
136
|
+
if (!timeZone)
|
|
137
|
+
throw new Error('timeZone is not set — SystemEver times are plant-local');
|
|
138
|
+
new Intl.DateTimeFormat('en-US', { timeZone });
|
|
139
|
+
const lines = new Map();
|
|
140
|
+
for (const row of rows) {
|
|
141
|
+
const no = text(row.WorkOrderNo);
|
|
142
|
+
if (no)
|
|
143
|
+
lines.set(no, (lines.get(no) ?? 0) + 1);
|
|
144
|
+
}
|
|
145
|
+
const result = { entries: [], skipped: [] };
|
|
146
|
+
for (const row of rows) {
|
|
147
|
+
const no = text(row.WorkOrderNo);
|
|
148
|
+
if (no && (lines.get(no) ?? 0) > 1) {
|
|
149
|
+
result.skipped.push({ orderId: no, reason: `${lines.get(no)} lines (WorkOrderSerl) for one order — not mapped` });
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const mapped = one(row, timeZone);
|
|
153
|
+
if ('reason' in mapped)
|
|
154
|
+
result.skipped.push(mapped);
|
|
155
|
+
else
|
|
156
|
+
result.entries.push(mapped);
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=systemever-work-order.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"systemever-work-order.js","sourceRoot":"","sources":["../server/systemever-work-order.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAIxC,+EAA+E;AAC/E,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,aAAa;IACb,eAAe;IACf,gBAAgB;IAChB,QAAQ;IACR,YAAY;IACZ,UAAU;IACV,cAAc;IACd,UAAU;IACV,eAAe;IACf,aAAa;IACb,gBAAgB;IAChB,cAAc;CACN,CAAA;AAEV;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAA6C;IACzE,EAAE,EAAE,OAAO;IACX,EAAE,EAAE,UAAU;IACd,GAAG,EAAE,SAAS;IACd,EAAE,EAAE,WAAW;CAChB,CAAA;AAcD,MAAM,IAAI,GAAG,CAAC,CAAU,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;AAEhE,uDAAuD;AACvD,SAAS,QAAQ,CAAC,KAAa,EAAE,QAAgB;IAC/C,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE;QAC7C,QAAQ;QACR,SAAS,EAAE,KAAK;QAChB,IAAI,EAAE,SAAS;QACf,KAAK,EAAE,SAAS;QAChB,GAAG,EAAE,SAAS;QACd,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,SAAS;QACjB,MAAM,EAAE,SAAS;KAClB,CAAC,CAAC,aAAa,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;IACjC,MAAM,CAAC,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,KAAK,CAAC,CAAA;IAC3E,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,KAAK,CAAA;AACnG,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,UAAU,CACxB,IAAY,EACZ,IAAY,EACZ,QAAgB;IAEhB,MAAM,CAAC,GAAG,6BAA6B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAClD,MAAM,CAAC,GAAG,kCAAkC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACvD,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;QAAE,OAAO,SAAS,CAAA;IAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC1E,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAA;IAC5C,oGAAoG;IACpG,IAAI,EAAE,GAAG,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;IACxC,EAAE,GAAG,IAAI,GAAG,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAA;IAClC,OAAO,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;AACnC,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,eAAe,CAAC,CAAuE;IACrG,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC,oBAAoB,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,SAAS,IAAI,EAAE,EAAE,CAAC,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,CAAA;IACzG,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;AACxF,CAAC;AAED,SAAS,GAAG,CAAC,GAAiB,EAAE,QAAgB;IAC9C,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;IACrC,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAA;IAEjD,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,GAAG;QAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,6CAA6C,EAAE,CAAA;IAEvG,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;IAC3C,MAAM,MAAM,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAA;IAC5C,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,4BAA4B,UAAU,gBAAgB,EAAE,CAAA;IAE/F,MAAM,oBAAoB,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;IACjD,IAAI,CAAC,oBAAoB;QAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,CAAA;IAEtE,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC3C,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,mBAAmB,EAAE,CAAA;IAErI,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;IAClC,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,0CAA0C,EAAE,CAAA;IAEhF,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IAC9B,MAAM,SAAS,GAAG,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACjH,MAAM,OAAO,GAAG,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAE3G,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IAC/D,MAAM,UAAU,GAAG,OAAO,IAAI,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAE5F,MAAM,OAAO,GAAG;QACd,oBAAoB;QACpB,QAAQ;QACR,GAAG;QACH,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/B,MAAM;KACP,CAAA;IAED,OAAO;QACL,OAAO;QACP,GAAG,OAAO;QACV,QAAQ,EAAE,eAAe,CAAC,OAAO,CAAC;QAClC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7E,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACtC,CAAA;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAoB,EAAE,OAA6B;IACnF,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;IACxC,IAAI,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAA;IACxF,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAA;IAE9C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAA;IACvC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;QAChC,IAAI,EAAE;YAAE,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IACjD,CAAC;IAED,MAAM,MAAM,GAAqB,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAA;IAC7D,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;QAChC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;YACnC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,mDAAmD,EAAE,CAAC,CAAA;YACjH,SAAQ;QACV,CAAC;QACD,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAA;QACjC,IAAI,QAAQ,IAAI,MAAM;YAAE,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;;YAC9C,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAClC,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["/*\n * **SystemEver work orders → contract schedule entries** — a pure mapping, no I/O.\n *\n * Both reading roads reach it (ADR-0110 addition 1, amended 2026-10-05): the adapter's own `readSchedules` when\n * it reads `/api/WebApi` directly, and the plant task when an integration scenario brings the rows. The rows\n * are the 작업지시조회 response as SystemEver sends them (svc 501041 / pgm 500880), column names untouched.\n *\n * ── What was seen (2026-10-01 response, two rows) ────────────────────────\n * WorkDate \"20251107\" · WorkStartTime \"0900\" · WorkEndTime \"1800\" planned day and hours, plant-local\n * OrderQty \"200.00000\" a decimal in a string\n * ItemUnitName \"EA\" · GoodItemNo \"F0102-H1003\" unit and item\n * ProgStatus 6036004 · ProgStatusName \"완료\" the only status seen\n * WorkOrderNo \"20251107-0001\" · WorkOrderSerl 1 one line per order\n * LastDateTime \"2025-11-28 14:35:13\" plant-local\n *\n * ── What it does not decide ──────────────────────────────────────────────\n * · **A status it does not know is not mapped.** Only 완료 was seen. 작성 · 확정 · 진행중 are read by their\n * names; 중단 · 미완료 and anything else skip the order with the reason — read as `cancelled`, an order the\n * floor has not started would be cancelled without a word.\n * · **The revision is the content, not the last update time** (user decision 2026-09-30): a remark edit must\n * not look like a new revision, which holds a started order for a person.\n * · **No zone is assumed.** The plant's zone is required; the ERP's times are local.\n */\nimport { createHash } from 'node:crypto'\n\nimport type { ProductionScheduleEntry, ScheduleStatus } from '@operato/ops-contract'\n\n/** The columns this mapping reads — all present in the 2026-10-01 response. */\nexport const WORK_ORDER_COLUMNS = [\n 'WorkOrderNo',\n 'WorkOrderSerl',\n 'ProgStatusName',\n 'IsStop',\n 'GoodItemNo',\n 'OrderQty',\n 'ItemUnitName',\n 'WorkDate',\n 'WorkStartTime',\n 'WorkEndTime',\n 'WorkCenterName',\n 'LastDateTime'\n] as const\n\n/**\n * SystemEver progress status → contract status. Read by name; only 완료 has been seen (code 6036004).\n * 중단 · 미완료 are left out on purpose (see the file head).\n */\nexport const WORK_ORDER_STATUS: Readonly<Record<string, ScheduleStatus>> = {\n 작성: 'draft',\n 확정: 'released',\n 진행중: 'started',\n 완료: 'completed'\n}\n\nexport type WorkOrderRow = Record<string, unknown>\n\nexport interface WorkOrderSkip {\n orderId?: string\n reason: string\n}\n\nexport interface WorkOrderMapping {\n entries: ProductionScheduleEntry[]\n skipped: WorkOrderSkip[]\n}\n\nconst text = (v: unknown) => (v == null ? '' : String(v).trim())\n\n/** Offset of `timeZone` from UTC at `utcMs`, in ms. */\nfunction offsetMs(utcMs: number, timeZone: string): number {\n const parts = new Intl.DateTimeFormat('en-US', {\n timeZone,\n hourCycle: 'h23',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit'\n }).formatToParts(new Date(utcMs))\n const n = (type: string) => Number(parts.find(p => p.type === type)?.value)\n return Date.UTC(n('year'), n('month') - 1, n('day'), n('hour'), n('minute'), n('second')) - utcMs\n}\n\n/** A plant-local wall time as an ISO instant. Undefined when the parts are not a time. */\nexport function localToIso(\n date: string,\n time: string,\n timeZone: string\n): string | undefined {\n const d = /^(\\d{4})-?(\\d{2})-?(\\d{2})$/.exec(date)\n const t = /^(\\d{2}):?(\\d{2})(?::?(\\d{2}))?$/.exec(time)\n if (!d || !t) return undefined\n const wall = Date.UTC(+d[1], +d[2] - 1, +d[3], +t[1], +t[2], +(t[3] ?? 0))\n if (!Number.isFinite(wall)) return undefined\n /* two passes: the offset at the guessed instant, then at the corrected one (around a DST change) */\n let at = wall - offsetMs(wall, timeZone)\n at = wall - offsetMs(at, timeZone)\n return new Date(at).toISOString()\n}\n\n/** The content revision — what the floor would act on, in a fixed order. */\nexport function contentRevision(e: Omit<ProductionScheduleEntry, 'revision' | 'recordTime' | 'orderId'>): string {\n const content = [e.materialDefinitionId, e.quantity, e.uom, e.startTime ?? '', e.endTime ?? '', e.status]\n return createHash('sha256').update(JSON.stringify(content)).digest('hex').slice(0, 16)\n}\n\nfunction one(row: WorkOrderRow, timeZone: string): ProductionScheduleEntry | WorkOrderSkip {\n const orderId = text(row.WorkOrderNo)\n if (!orderId) return { reason: 'no WorkOrderNo' }\n\n if (text(row.IsStop) === '1') return { orderId, reason: 'stopped in SystemEver (IsStop) — not mapped' }\n\n const statusName = text(row.ProgStatusName)\n const status = WORK_ORDER_STATUS[statusName]\n if (!status) return { orderId, reason: `unknown progress status \"${statusName}\" — not mapped` }\n\n const materialDefinitionId = text(row.GoodItemNo)\n if (!materialDefinitionId) return { orderId, reason: 'no GoodItemNo' }\n\n const quantity = Number(text(row.OrderQty))\n if (!text(row.OrderQty) || !Number.isFinite(quantity)) return { orderId, reason: `OrderQty \"${text(row.OrderQty)}\" is not a number` }\n\n const uom = text(row.ItemUnitName)\n if (!uom) return { orderId, reason: 'no ItemUnitName — a unit is not invented' }\n\n const day = text(row.WorkDate)\n const startTime = day && text(row.WorkStartTime) ? localToIso(day, text(row.WorkStartTime), timeZone) : undefined\n const endTime = day && text(row.WorkEndTime) ? localToIso(day, text(row.WorkEndTime), timeZone) : undefined\n\n const [lastDay, lastTime] = text(row.LastDateTime).split(/\\s+/)\n const recordTime = lastDay && lastTime ? localToIso(lastDay, lastTime, timeZone) : undefined\n\n const content = {\n materialDefinitionId,\n quantity,\n uom,\n ...(startTime ? { startTime } : {}),\n ...(endTime ? { endTime } : {}),\n status\n }\n\n return {\n orderId,\n ...content,\n revision: contentRevision(content),\n ...(text(row.WorkCenterName) ? { workCenter: text(row.WorkCenterName) } : {}),\n ...(recordTime ? { recordTime } : {})\n }\n}\n\n/**\n * Map a 작업지시조회 response.\n *\n * An order that comes as more than one line (several `WorkOrderSerl`, i.e. several processes) is skipped whole:\n * the contract entry is one order, and picking one line would drop the others. Miratek's in-house orders are one\n * process each; if that changes, this says so instead of guessing.\n *\n * @throws when `timeZone` is missing or unknown — no row can be read without it.\n */\nexport function toScheduleEntries(rows: WorkOrderRow[], options: { timeZone: string }): WorkOrderMapping {\n const timeZone = text(options?.timeZone)\n if (!timeZone) throw new Error('timeZone is not set — SystemEver times are plant-local')\n new Intl.DateTimeFormat('en-US', { timeZone })\n\n const lines = new Map<string, number>()\n for (const row of rows) {\n const no = text(row.WorkOrderNo)\n if (no) lines.set(no, (lines.get(no) ?? 0) + 1)\n }\n\n const result: WorkOrderMapping = { entries: [], skipped: [] }\n for (const row of rows) {\n const no = text(row.WorkOrderNo)\n if (no && (lines.get(no) ?? 0) > 1) {\n result.skipped.push({ orderId: no, reason: `${lines.get(no)} lines (WorkOrderSerl) for one order — not mapped` })\n continue\n }\n const mapped = one(row, timeZone)\n if ('reason' in mapped) result.skipped.push(mapped)\n else result.entries.push(mapped)\n }\n return result\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"keep": {
|
|
3
|
+
"days": true,
|
|
4
|
+
"amount": 14
|
|
5
|
+
},
|
|
6
|
+
"auditLog": "logs/.5e5d741d8b7784a2fbad65eedc0fd46946aaf6f2-audit.json",
|
|
7
|
+
"files": [
|
|
8
|
+
{
|
|
9
|
+
"date": 1791166847865,
|
|
10
|
+
"name": "logs/connections-2026-10-05-11.log",
|
|
11
|
+
"hash": "5647fcfe1bef3c9ddec598c57f6b17fdfdd80114133657ed3f064381a430548d"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"hashType": "sha256"
|
|
15
|
+
}
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/erp-systemever",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.118",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Younglimwon SystemEver adapter for the ERP (Level 4) ↔ MES (Level 3) boundary — a manual-export ERP: work report lines a person pastes into the work report sheet.",
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
"test": "node --import ./test/resolve-ts-siblings.mjs --test test/*.test.ts"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@operato/ops-contract": "^0.9.41"
|
|
25
|
+
"@operato/ops-contract": "^0.9.41",
|
|
26
|
+
"@things-factory/integration-base": "^10.1.118",
|
|
27
|
+
"@things-factory/utils": "^10.1.20"
|
|
26
28
|
},
|
|
27
|
-
"gitHead": "
|
|
29
|
+
"gitHead": "53d4c1839c41c7305c8f18021120fd3ad82871c6"
|
|
28
30
|
}
|
package/server/index.ts
CHANGED
|
@@ -1 +1,9 @@
|
|
|
1
1
|
export * from './systemever-adapter.js'
|
|
2
|
+
export * from './systemever-work-order.js'
|
|
3
|
+
export * from './systemever-query-policy.js'
|
|
4
|
+
export * from './systemever-query-response.js'
|
|
5
|
+
export * from './systemever-work-order-query.js'
|
|
6
|
+
export * from './systemever-query-runner.js'
|
|
7
|
+
import './systemever-connector.js'
|
|
8
|
+
import './systemever-query-task.js'
|
|
9
|
+
import './systemever-schedule-task.js'
|