@flowselections/floriday-verkoop-module 1.0.6 → 1.0.8
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/dist-lib/components/verkoop/AnalyticsCards.js +2 -2
- package/dist-lib/components/verkoop/CreateOrderPage.d.ts +2 -0
- package/dist-lib/components/verkoop/CreateOrderPage.d.ts.map +1 -0
- package/dist-lib/components/verkoop/CreateOrderPage.js +221 -0
- package/dist-lib/components/verkoop/DiagnoseConnectionDialog.d.ts +5 -0
- package/dist-lib/components/verkoop/DiagnoseConnectionDialog.d.ts.map +1 -0
- package/dist-lib/components/verkoop/DiagnoseConnectionDialog.js +40 -0
- package/dist-lib/components/verkoop/SalesOrderDetailPage.d.ts +2 -0
- package/dist-lib/components/verkoop/SalesOrderDetailPage.d.ts.map +1 -0
- package/dist-lib/components/verkoop/SalesOrderDetailPage.js +14 -0
- package/dist-lib/components/verkoop/SalesOrderDetailView.d.ts +6 -0
- package/dist-lib/components/verkoop/SalesOrderDetailView.d.ts.map +1 -0
- package/dist-lib/components/verkoop/SalesOrderDetailView.js +213 -0
- package/dist-lib/components/verkoop/SalesOrdersFilterPanel.d.ts +9 -0
- package/dist-lib/components/verkoop/SalesOrdersFilterPanel.d.ts.map +1 -0
- package/dist-lib/components/verkoop/SalesOrdersFilterPanel.js +62 -0
- package/dist-lib/components/verkoop/SalesOrdersFilters.d.ts +13 -0
- package/dist-lib/components/verkoop/SalesOrdersFilters.d.ts.map +1 -1
- package/dist-lib/components/verkoop/SalesOrdersFilters.js +123 -23
- package/dist-lib/components/verkoop/SalesOrdersTable.d.ts +3 -2
- package/dist-lib/components/verkoop/SalesOrdersTable.d.ts.map +1 -1
- package/dist-lib/components/verkoop/SalesOrdersTable.js +3 -3
- package/dist-lib/components/verkoop/VerkoopOrdersPage.d.ts.map +1 -1
- package/dist-lib/components/verkoop/VerkoopOrdersPage.js +124 -33
- package/dist-lib/components/verkoop/WeekCalendar.d.ts +6 -0
- package/dist-lib/components/verkoop/WeekCalendar.d.ts.map +1 -0
- package/dist-lib/components/verkoop/WeekCalendar.js +101 -0
- package/dist-lib/index.d.ts +3 -1
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +3 -1
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +5727 -653
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +5727 -653
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.js +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +5727 -653
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.js +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +5871 -653
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +18 -1
- package/dist-lib/lib/floriday-sync.d.ts +32 -0
- package/dist-lib/lib/floriday-sync.d.ts.map +1 -0
- package/dist-lib/lib/floriday-sync.js +177 -0
- package/dist-lib/lib/salesorders-mapping.d.ts +85 -0
- package/dist-lib/lib/salesorders-mapping.d.ts.map +1 -0
- package/dist-lib/lib/salesorders-mapping.js +198 -0
- package/dist-lib/lib/salesorders.functions.d.ts +19941 -0
- package/dist-lib/lib/salesorders.functions.d.ts.map +1 -0
- package/dist-lib/lib/salesorders.functions.js +216 -0
- package/dist-lib/lib/tradeitems.functions.d.ts +6641 -0
- package/dist-lib/lib/tradeitems.functions.d.ts.map +1 -0
- package/dist-lib/lib/tradeitems.functions.js +60 -0
- package/dist-lib/lib/useSalesOrdersQuery.d.ts +6 -0
- package/dist-lib/lib/useSalesOrdersQuery.d.ts.map +1 -0
- package/dist-lib/lib/useSalesOrdersQuery.js +143 -0
- package/package.json +18 -4
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
export declare const FLORIDAY_API_BASES: {
|
|
3
|
+
readonly staging: "https://api.staging.floriday.io/suppliers-api-2026v1";
|
|
4
|
+
readonly production: "https://api.floriday.io/suppliers-api-2026v1";
|
|
5
|
+
};
|
|
6
|
+
export interface FloridayConnectionRow {
|
|
7
|
+
id: string;
|
|
8
|
+
environment: string | null;
|
|
9
|
+
access_token: string | null;
|
|
10
|
+
api_key: string | null;
|
|
11
|
+
is_active: boolean | null;
|
|
12
|
+
}
|
|
13
|
+
export interface SyncOptions<TRow> {
|
|
14
|
+
connection: FloridayConnectionRow;
|
|
15
|
+
entityType: string;
|
|
16
|
+
maxEndpoint: string;
|
|
17
|
+
syncEndpointBase: string;
|
|
18
|
+
cacheTable: string;
|
|
19
|
+
conflictTarget: string;
|
|
20
|
+
mapRow: (record: any, connectionId: string) => TRow;
|
|
21
|
+
}
|
|
22
|
+
export interface SyncResult {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
entityType: string;
|
|
25
|
+
connectionId: string;
|
|
26
|
+
synced: number;
|
|
27
|
+
fromSequence: number;
|
|
28
|
+
toSequence: number;
|
|
29
|
+
}
|
|
30
|
+
export declare function syncBySequence<TRow extends Record<string, any>>(supabase: SupabaseClient, opts: SyncOptions<TRow>): Promise<SyncResult>;
|
|
31
|
+
export declare function loadActiveConnections(supabase: SupabaseClient): Promise<FloridayConnectionRow[]>;
|
|
32
|
+
//# sourceMappingURL=floriday-sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floriday-sync.d.ts","sourceRoot":"","sources":["../../src/lib/floriday-sync.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,eAAO,MAAM,kBAAkB;;;CAGrB,CAAC;AAEX,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,OAAO,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW,CAAC,IAAI;IAC/B,UAAU,EAAE,qBAAqB,CAAC;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;CACrD;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AA2HD,wBAAsB,cAAc,CAAC,IAAI,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACnE,QAAQ,EAAE,cAAc,EACxB,IAAI,EAAE,WAAW,CAAC,IAAI,CAAC,GACtB,OAAO,CAAC,UAAU,CAAC,CAsFrB;AAGD,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,cAAc,GACvB,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAOlC"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
export const FLORIDAY_API_BASES = {
|
|
2
|
+
staging: "https://api.staging.floriday.io/suppliers-api-2026v1",
|
|
3
|
+
production: "https://api.floriday.io/suppliers-api-2026v1",
|
|
4
|
+
};
|
|
5
|
+
function apiBaseFor(environment) {
|
|
6
|
+
return environment === "production" || environment === "prod"
|
|
7
|
+
? FLORIDAY_API_BASES.production
|
|
8
|
+
: FLORIDAY_API_BASES.staging;
|
|
9
|
+
}
|
|
10
|
+
// URL of the auth-module's token-refresh endpoint (shared Supabase project).
|
|
11
|
+
// On 401/403 we POST here, then re-read the freshly written token from
|
|
12
|
+
// floriday_connections. We never run our own OAuth flow or write tokens.
|
|
13
|
+
const AUTH_MODULE_REFRESH_URL = "https://project--vmicscahrnzpmhagztmx.lovable.app/api/public/cron/refresh-floriday-tokens";
|
|
14
|
+
async function reloadConnection(supabase, connectionId) {
|
|
15
|
+
const { data, error } = await supabase
|
|
16
|
+
.from("floriday_connections")
|
|
17
|
+
.select("id, environment, access_token, api_key, is_active")
|
|
18
|
+
.eq("id", connectionId)
|
|
19
|
+
.maybeSingle();
|
|
20
|
+
if (error || !data) {
|
|
21
|
+
throw new Error(`Floriday connectie ${connectionId} herladen mislukt: ${error?.message ?? "not found"}`);
|
|
22
|
+
}
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
async function triggerAuthModuleRefresh() {
|
|
26
|
+
try {
|
|
27
|
+
await fetch(AUTH_MODULE_REFRESH_URL, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "Content-Type": "application/json" },
|
|
30
|
+
body: "{}",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// best-effort; we'll re-read the token regardless and let the retry surface a clear error
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function floridayGet(supabase, conn, path) {
|
|
38
|
+
let active = conn;
|
|
39
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
40
|
+
if (!active.access_token) {
|
|
41
|
+
// No token yet — kick the auth-module once, then re-read.
|
|
42
|
+
await triggerAuthModuleRefresh();
|
|
43
|
+
active = await reloadConnection(supabase, active.id);
|
|
44
|
+
if (!active.access_token) {
|
|
45
|
+
throw new Error(`Floriday connectie ${active.id} heeft geen access_token (auth-module heeft nog niet ververst)`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const url = `${apiBaseFor(active.environment)}${path}`;
|
|
49
|
+
const res = await fetch(url, {
|
|
50
|
+
method: "GET",
|
|
51
|
+
headers: {
|
|
52
|
+
Accept: "application/json",
|
|
53
|
+
Authorization: `Bearer ${active.access_token}`,
|
|
54
|
+
...(active.api_key ? { "x-api-key": active.api_key } : {}),
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
const text = await res.text();
|
|
58
|
+
if (res.ok) {
|
|
59
|
+
return {
|
|
60
|
+
result: (text ? JSON.parse(text) : null),
|
|
61
|
+
connection: active,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// 401/403 → trigger auth-module refresh, re-read token, retry once.
|
|
65
|
+
if ((res.status === 401 || res.status === 403) && attempt === 0) {
|
|
66
|
+
await triggerAuthModuleRefresh();
|
|
67
|
+
active = await reloadConnection(supabase, active.id);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`Floriday GET ${path} faalde [${res.status}]: ${text.slice(0, 400)}`);
|
|
71
|
+
}
|
|
72
|
+
// Unreachable, but keeps TypeScript happy.
|
|
73
|
+
throw new Error(`Floriday GET ${path} faalde na retry`);
|
|
74
|
+
}
|
|
75
|
+
async function readLastSequence(supabase, connectionId, entityType) {
|
|
76
|
+
const { data } = await supabase
|
|
77
|
+
.from("floriday_sync_state")
|
|
78
|
+
.select("last_sequence_number")
|
|
79
|
+
.eq("connection_id", connectionId)
|
|
80
|
+
.eq("entity_type", entityType)
|
|
81
|
+
.maybeSingle();
|
|
82
|
+
const v = data?.last_sequence_number;
|
|
83
|
+
return typeof v === "number" ? v : Number(v ?? 0) || 0;
|
|
84
|
+
}
|
|
85
|
+
async function writeLastSequence(supabase, connectionId, entityType, lastSequenceNumber) {
|
|
86
|
+
await supabase.from("floriday_sync_state").upsert({
|
|
87
|
+
connection_id: connectionId,
|
|
88
|
+
entity_type: entityType,
|
|
89
|
+
last_sequence_number: lastSequenceNumber,
|
|
90
|
+
last_synced_at: new Date().toISOString(),
|
|
91
|
+
}, { onConflict: "connection_id,entity_type" });
|
|
92
|
+
}
|
|
93
|
+
export async function syncBySequence(supabase, opts) {
|
|
94
|
+
const { connection, entityType, maxEndpoint, syncEndpointBase, cacheTable, conflictTarget, mapRow } = opts;
|
|
95
|
+
let active = connection;
|
|
96
|
+
// Stap 1: huidige max sequence
|
|
97
|
+
const maxCall = await floridayGet(supabase, active, maxEndpoint);
|
|
98
|
+
active = maxCall.connection;
|
|
99
|
+
const maxRes = maxCall.result;
|
|
100
|
+
const maxSequence = Number((typeof maxRes === "number" ? maxRes : maxRes?.maxSequenceNumber ?? maxRes?.sequenceNumber ?? 0) ?? 0);
|
|
101
|
+
const fromSequence = await readLastSequence(supabase, active.id, entityType);
|
|
102
|
+
let cursor = fromSequence;
|
|
103
|
+
let synced = 0;
|
|
104
|
+
let safetyHops = 0;
|
|
105
|
+
const MAX_HOPS = 1000;
|
|
106
|
+
// Per Floriday-spec: lege batches zijn toegestaan (server-side filter).
|
|
107
|
+
// Doorgaan tot cursor de (eventueel opgeschoven) maxSequence bereikt.
|
|
108
|
+
let effectiveMax = maxSequence;
|
|
109
|
+
while (cursor < effectiveMax && safetyHops < MAX_HOPS) {
|
|
110
|
+
safetyHops += 1;
|
|
111
|
+
const batchCall = await floridayGet(supabase, active, `${syncEndpointBase}/${cursor}`);
|
|
112
|
+
active = batchCall.connection;
|
|
113
|
+
const batch = batchCall.result;
|
|
114
|
+
// Response-shape soepel parsen.
|
|
115
|
+
let list = [];
|
|
116
|
+
let respSeq = null;
|
|
117
|
+
let respMaxSeq = null;
|
|
118
|
+
if (Array.isArray(batch)) {
|
|
119
|
+
list = batch;
|
|
120
|
+
}
|
|
121
|
+
else if (batch && typeof batch === "object") {
|
|
122
|
+
list = (batch.results ?? batch.items ?? batch.salesOrders ?? batch.data ?? []);
|
|
123
|
+
const s = Number(batch.sequenceNumber);
|
|
124
|
+
const m = Number(batch.maximumSequenceNumber);
|
|
125
|
+
if (Number.isFinite(s))
|
|
126
|
+
respSeq = s;
|
|
127
|
+
if (Number.isFinite(m))
|
|
128
|
+
respMaxSeq = m;
|
|
129
|
+
}
|
|
130
|
+
if (list.length > 0) {
|
|
131
|
+
const rows = list.map((r) => mapRow(r, active.id));
|
|
132
|
+
const { error: upErr } = await supabase.from(cacheTable).upsert(rows, { onConflict: conflictTarget });
|
|
133
|
+
if (upErr)
|
|
134
|
+
throw new Error(`Upsert ${cacheTable} faalde: ${upErr.message}`);
|
|
135
|
+
synced += list.length;
|
|
136
|
+
}
|
|
137
|
+
const seqs = list
|
|
138
|
+
.map((r) => Number(r?.sequenceNumber ?? 0))
|
|
139
|
+
.filter((n) => Number.isFinite(n));
|
|
140
|
+
const highestInList = seqs.length ? Math.max(...seqs) : -Infinity;
|
|
141
|
+
let nextCursor = cursor;
|
|
142
|
+
if (highestInList > cursor)
|
|
143
|
+
nextCursor = highestInList;
|
|
144
|
+
else if (respSeq !== null && respSeq > cursor)
|
|
145
|
+
nextCursor = respSeq;
|
|
146
|
+
else if (list.length === 0 && respMaxSeq !== null && respMaxSeq > cursor) {
|
|
147
|
+
nextCursor = respMaxSeq;
|
|
148
|
+
}
|
|
149
|
+
else if (list.length === 0) {
|
|
150
|
+
// Geen vooruitgang: voorkom oneindige loop.
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
if (nextCursor <= cursor)
|
|
154
|
+
break;
|
|
155
|
+
cursor = nextCursor;
|
|
156
|
+
await writeLastSequence(supabase, active.id, entityType, cursor);
|
|
157
|
+
if (respMaxSeq !== null && respMaxSeq > effectiveMax)
|
|
158
|
+
effectiveMax = respMaxSeq;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
ok: true,
|
|
162
|
+
entityType,
|
|
163
|
+
connectionId: active.id,
|
|
164
|
+
synced,
|
|
165
|
+
fromSequence,
|
|
166
|
+
toSequence: cursor,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
export async function loadActiveConnections(supabase) {
|
|
170
|
+
const { data, error } = await supabase
|
|
171
|
+
.from("floriday_connections")
|
|
172
|
+
.select("id, environment, access_token, api_key, is_active")
|
|
173
|
+
.eq("is_active", true);
|
|
174
|
+
if (error)
|
|
175
|
+
throw new Error(`floriday_connections lezen faalde: ${error.message}`);
|
|
176
|
+
return (data ?? []);
|
|
177
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export interface SalesOrderDTO {
|
|
2
|
+
id: string;
|
|
3
|
+
orderNumber: string;
|
|
4
|
+
productName: string;
|
|
5
|
+
sku: string;
|
|
6
|
+
imageUrl: string | null;
|
|
7
|
+
orderedAt: string;
|
|
8
|
+
deliveryAt: string;
|
|
9
|
+
quantity: {
|
|
10
|
+
multiplier: number;
|
|
11
|
+
count: number;
|
|
12
|
+
unitPrice: number;
|
|
13
|
+
};
|
|
14
|
+
revenue: number;
|
|
15
|
+
fustCount: number;
|
|
16
|
+
isLocal?: boolean;
|
|
17
|
+
customer: {
|
|
18
|
+
name: string;
|
|
19
|
+
postalCode: string;
|
|
20
|
+
city: string;
|
|
21
|
+
logoUrl: string | null;
|
|
22
|
+
};
|
|
23
|
+
status: {
|
|
24
|
+
isCancelled: boolean;
|
|
25
|
+
isShipped: boolean;
|
|
26
|
+
isInvoiced: boolean;
|
|
27
|
+
isConfirmed: boolean;
|
|
28
|
+
};
|
|
29
|
+
raw: {
|
|
30
|
+
order: any;
|
|
31
|
+
customer: any | null;
|
|
32
|
+
tradeItem: any | null;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export interface CustomerLookupEntry {
|
|
36
|
+
name: string;
|
|
37
|
+
postalCode: string;
|
|
38
|
+
city: string;
|
|
39
|
+
logoUrl: string | null;
|
|
40
|
+
raw?: any;
|
|
41
|
+
}
|
|
42
|
+
export type CustomerLookup = Map<string, CustomerLookupEntry>;
|
|
43
|
+
export interface TradeItemLookupEntry {
|
|
44
|
+
name: string;
|
|
45
|
+
imageUrl: string | null;
|
|
46
|
+
sku: string | null;
|
|
47
|
+
raw?: any;
|
|
48
|
+
}
|
|
49
|
+
export type TradeItemLookup = Map<string, TradeItemLookupEntry>;
|
|
50
|
+
export declare function mapJsonbToSalesOrderDTO(o: any, customerLookup?: CustomerLookup, tradeItemLookup?: TradeItemLookup): SalesOrderDTO;
|
|
51
|
+
export interface SalesOrdersTotals {
|
|
52
|
+
revenue: number;
|
|
53
|
+
avgPrice: number;
|
|
54
|
+
fustCount: number;
|
|
55
|
+
}
|
|
56
|
+
export declare function computeTotals(orders: SalesOrderDTO[]): SalesOrdersTotals;
|
|
57
|
+
export interface SalesOrderFilters {
|
|
58
|
+
from?: string;
|
|
59
|
+
to?: string;
|
|
60
|
+
search?: string;
|
|
61
|
+
status?: string;
|
|
62
|
+
week?: number;
|
|
63
|
+
year?: number;
|
|
64
|
+
month?: number;
|
|
65
|
+
monthYear?: number;
|
|
66
|
+
day?: string;
|
|
67
|
+
customFrom?: string;
|
|
68
|
+
customTo?: string;
|
|
69
|
+
dateField?: "ordered" | "delivery";
|
|
70
|
+
period?: "week" | "month" | "day" | "last7" | "last30" | "custom";
|
|
71
|
+
}
|
|
72
|
+
export declare function isoWeekRange(week: number, year: number): {
|
|
73
|
+
start: Date;
|
|
74
|
+
end: Date;
|
|
75
|
+
};
|
|
76
|
+
export declare function monthRange(month: number, year: number): {
|
|
77
|
+
start: Date;
|
|
78
|
+
end: Date;
|
|
79
|
+
};
|
|
80
|
+
export declare function applyFilters(orders: SalesOrderDTO[], f: SalesOrderFilters): SalesOrderDTO[];
|
|
81
|
+
export declare function ordersToCsv(orders: SalesOrderDTO[]): {
|
|
82
|
+
csv: string;
|
|
83
|
+
filename: string;
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=salesorders-mapping.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"salesorders-mapping.d.ts","sourceRoot":"","sources":["../../src/lib/salesorders-mapping.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
function toNum(v, d = 0) {
|
|
2
|
+
if (typeof v === "number")
|
|
3
|
+
return v;
|
|
4
|
+
if (typeof v === "string") {
|
|
5
|
+
const n = parseFloat(v);
|
|
6
|
+
return Number.isFinite(n) ? n : d;
|
|
7
|
+
}
|
|
8
|
+
return d;
|
|
9
|
+
}
|
|
10
|
+
export function mapJsonbToSalesOrderDTO(o, customerLookup, tradeItemLookup) {
|
|
11
|
+
// Floriday suppliers-api-2026v1 levert een vlak sales-order object
|
|
12
|
+
// (geen salesOrderLines-array). Velden staan direct op het object.
|
|
13
|
+
const numberOfPieces = toNum(o?.numberOfPieces, 0);
|
|
14
|
+
const piecesPerPackage = Math.max(1, toNum(o?.packingConfiguration?.piecesPerPackage, 1));
|
|
15
|
+
const numberOfPackages = piecesPerPackage > 0 ? Math.ceil(numberOfPieces / piecesPerPackage) : 0;
|
|
16
|
+
const unitPrice = toNum(o?.pricePerPiece?.value);
|
|
17
|
+
const orderAmount = toNum(o?.calculatedFields?.orderAmount?.value);
|
|
18
|
+
const revenue = orderAmount > 0 ? orderAmount : unitPrice * numberOfPieces;
|
|
19
|
+
const orderedAt = o?.orderDateTime ?? o?.creationDateTime ?? "";
|
|
20
|
+
const deliveryAt = o?.delivery?.latestDeliveryDateTime ?? "";
|
|
21
|
+
const customerOrgId = o?.customerOrganizationId ?? "";
|
|
22
|
+
const found = customerOrgId ? customerLookup?.get(customerOrgId) : undefined;
|
|
23
|
+
const status = String(o?.status ?? "").toUpperCase();
|
|
24
|
+
const isCancelled = status === "CANCELLED" || !!o?.automaticallyCancelledOn;
|
|
25
|
+
const isShipped = status === "SHIPPED" || status === "DELIVERED";
|
|
26
|
+
const isInvoiced = o?.isPaid === true;
|
|
27
|
+
const isConfirmed = status === "COMMITTED" || status === "CONFIRMED";
|
|
28
|
+
const orderNumber = (typeof o?.customerOrderId === "string" && o.customerOrderId.trim().length > 0
|
|
29
|
+
? o.customerOrderId
|
|
30
|
+
: null) ??
|
|
31
|
+
(o?.salesChannelOrderId ? String(o.salesChannelOrderId) : null) ??
|
|
32
|
+
String(o?.salesOrderId ?? "").slice(0, 16);
|
|
33
|
+
const tradeItemId = String(o?.tradeItemId ?? "");
|
|
34
|
+
const tradeItem = tradeItemId ? tradeItemLookup?.get(tradeItemId) : undefined;
|
|
35
|
+
return {
|
|
36
|
+
id: String(o?.salesOrderId ?? ""),
|
|
37
|
+
orderNumber: String(orderNumber ?? ""),
|
|
38
|
+
productName: tradeItem?.name ?? "Onbekend artikel",
|
|
39
|
+
sku: tradeItem?.sku ?? tradeItemId,
|
|
40
|
+
imageUrl: tradeItem?.imageUrl ?? null,
|
|
41
|
+
orderedAt,
|
|
42
|
+
deliveryAt,
|
|
43
|
+
quantity: { multiplier: numberOfPackages, count: piecesPerPackage, unitPrice },
|
|
44
|
+
revenue,
|
|
45
|
+
fustCount: numberOfPackages,
|
|
46
|
+
customer: {
|
|
47
|
+
name: found?.name ?? "Onbekende klant",
|
|
48
|
+
postalCode: found?.postalCode ?? "",
|
|
49
|
+
city: found?.city ?? "",
|
|
50
|
+
logoUrl: found?.logoUrl ?? null,
|
|
51
|
+
},
|
|
52
|
+
status: { isCancelled, isShipped, isInvoiced, isConfirmed },
|
|
53
|
+
raw: {
|
|
54
|
+
order: o ?? null,
|
|
55
|
+
customer: found?.raw ?? null,
|
|
56
|
+
tradeItem: tradeItem?.raw ?? null,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function computeTotals(orders) {
|
|
61
|
+
const revenue = orders.reduce((s, o) => s + o.revenue, 0);
|
|
62
|
+
const fustCount = orders.reduce((s, o) => s + o.fustCount, 0);
|
|
63
|
+
const totalPieces = orders.reduce((s, o) => s + o.quantity.multiplier * o.quantity.count, 0);
|
|
64
|
+
const avgPrice = totalPieces > 0 ? revenue / totalPieces : 0;
|
|
65
|
+
return { revenue, avgPrice, fustCount };
|
|
66
|
+
}
|
|
67
|
+
// ISO-8601 weekbereik (maandag 00:00 t/m maandag erop 00:00, UTC).
|
|
68
|
+
export function isoWeekRange(week, year) {
|
|
69
|
+
// 4 januari valt altijd in week 1 (ISO).
|
|
70
|
+
const jan4 = new Date(Date.UTC(year, 0, 4));
|
|
71
|
+
const jan4Dow = (jan4.getUTCDay() + 6) % 7; // ma=0..zo=6
|
|
72
|
+
const week1Monday = new Date(jan4);
|
|
73
|
+
week1Monday.setUTCDate(jan4.getUTCDate() - jan4Dow);
|
|
74
|
+
const start = new Date(week1Monday);
|
|
75
|
+
start.setUTCDate(week1Monday.getUTCDate() + (week - 1) * 7);
|
|
76
|
+
const end = new Date(start);
|
|
77
|
+
end.setUTCDate(start.getUTCDate() + 7);
|
|
78
|
+
return { start, end };
|
|
79
|
+
}
|
|
80
|
+
export function monthRange(month, year) {
|
|
81
|
+
const start = new Date(Date.UTC(year, month, 1));
|
|
82
|
+
const end = new Date(Date.UTC(year, month + 1, 1));
|
|
83
|
+
return { start, end };
|
|
84
|
+
}
|
|
85
|
+
function ymdToUtc(ymd) {
|
|
86
|
+
const [y, m, d] = ymd.split("-").map(Number);
|
|
87
|
+
return new Date(Date.UTC(y, (m ?? 1) - 1, d ?? 1));
|
|
88
|
+
}
|
|
89
|
+
function startOfTodayUtc() {
|
|
90
|
+
const now = new Date();
|
|
91
|
+
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
|
92
|
+
}
|
|
93
|
+
export function applyFilters(orders, f) {
|
|
94
|
+
const dateField = f.dateField ?? "ordered";
|
|
95
|
+
let rangeStartIso = null;
|
|
96
|
+
let rangeEndIso = null;
|
|
97
|
+
if (f.period === "month" && f.month !== undefined && f.monthYear !== undefined) {
|
|
98
|
+
const { start, end } = monthRange(f.month, f.monthYear);
|
|
99
|
+
rangeStartIso = start.toISOString();
|
|
100
|
+
rangeEndIso = end.toISOString();
|
|
101
|
+
}
|
|
102
|
+
else if (f.period === "day" && f.day) {
|
|
103
|
+
const start = ymdToUtc(f.day);
|
|
104
|
+
const end = new Date(start);
|
|
105
|
+
end.setUTCDate(start.getUTCDate() + 1);
|
|
106
|
+
rangeStartIso = start.toISOString();
|
|
107
|
+
rangeEndIso = end.toISOString();
|
|
108
|
+
}
|
|
109
|
+
else if (f.period === "last7" || f.period === "last30") {
|
|
110
|
+
const days = f.period === "last7" ? 7 : 30;
|
|
111
|
+
const end = startOfTodayUtc();
|
|
112
|
+
end.setUTCDate(end.getUTCDate() + 1);
|
|
113
|
+
const start = new Date(end);
|
|
114
|
+
start.setUTCDate(end.getUTCDate() - days);
|
|
115
|
+
rangeStartIso = start.toISOString();
|
|
116
|
+
rangeEndIso = end.toISOString();
|
|
117
|
+
}
|
|
118
|
+
else if (f.period === "custom" && f.customFrom && f.customTo) {
|
|
119
|
+
const start = ymdToUtc(f.customFrom);
|
|
120
|
+
const end = ymdToUtc(f.customTo);
|
|
121
|
+
end.setUTCDate(end.getUTCDate() + 1);
|
|
122
|
+
rangeStartIso = start.toISOString();
|
|
123
|
+
rangeEndIso = end.toISOString();
|
|
124
|
+
}
|
|
125
|
+
else if (f.week && f.year) {
|
|
126
|
+
const { start, end } = isoWeekRange(f.week, f.year);
|
|
127
|
+
rangeStartIso = start.toISOString();
|
|
128
|
+
rangeEndIso = end.toISOString();
|
|
129
|
+
}
|
|
130
|
+
return orders.filter((o) => {
|
|
131
|
+
const refDate = dateField === "delivery" ? o.deliveryAt : o.orderedAt;
|
|
132
|
+
if (rangeStartIso && rangeEndIso) {
|
|
133
|
+
if (!refDate)
|
|
134
|
+
return false;
|
|
135
|
+
if (refDate < rangeStartIso || refDate >= rangeEndIso)
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
if (f.from && refDate < f.from)
|
|
139
|
+
return false;
|
|
140
|
+
if (f.to && refDate > f.to)
|
|
141
|
+
return false;
|
|
142
|
+
if (f.search) {
|
|
143
|
+
const q = f.search.toLowerCase();
|
|
144
|
+
const hay = `${o.productName} ${o.sku} ${o.orderNumber} ${o.customer.name}`.toLowerCase();
|
|
145
|
+
if (!hay.includes(q))
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
if (f.status === "cancelled" && !o.status.isCancelled)
|
|
149
|
+
return false;
|
|
150
|
+
if (f.status === "shipped" && !o.status.isShipped)
|
|
151
|
+
return false;
|
|
152
|
+
if (f.status === "invoiced" && !o.status.isInvoiced)
|
|
153
|
+
return false;
|
|
154
|
+
if (f.status === "open" && (o.status.isCancelled || o.status.isShipped))
|
|
155
|
+
return false;
|
|
156
|
+
return true;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
export function ordersToCsv(orders) {
|
|
160
|
+
const header = [
|
|
161
|
+
"Ordernummer",
|
|
162
|
+
"Artikel",
|
|
163
|
+
"SKU",
|
|
164
|
+
"Bestelmoment",
|
|
165
|
+
"Aantal",
|
|
166
|
+
"Stuksprijs",
|
|
167
|
+
"Omzet",
|
|
168
|
+
"Klant",
|
|
169
|
+
"Postcode",
|
|
170
|
+
"Plaats",
|
|
171
|
+
"Levering",
|
|
172
|
+
"Status",
|
|
173
|
+
];
|
|
174
|
+
const rows = orders.map((o) => [
|
|
175
|
+
o.orderNumber,
|
|
176
|
+
o.productName,
|
|
177
|
+
o.sku,
|
|
178
|
+
o.orderedAt,
|
|
179
|
+
`${o.quantity.multiplier}x${o.quantity.count}`,
|
|
180
|
+
o.quantity.unitPrice.toFixed(2),
|
|
181
|
+
o.revenue.toFixed(2),
|
|
182
|
+
o.customer.name,
|
|
183
|
+
o.customer.postalCode,
|
|
184
|
+
o.customer.city,
|
|
185
|
+
o.deliveryAt,
|
|
186
|
+
o.status.isCancelled
|
|
187
|
+
? "Geannuleerd"
|
|
188
|
+
: o.status.isShipped
|
|
189
|
+
? "Verzonden"
|
|
190
|
+
: o.status.isInvoiced
|
|
191
|
+
? "Gefactureerd"
|
|
192
|
+
: "Open",
|
|
193
|
+
]);
|
|
194
|
+
const escape = (v) => `"${String(v).replace(/"/g, '""')}"`;
|
|
195
|
+
const csv = [header, ...rows].map((r) => r.map(escape).join(";")).join("\n");
|
|
196
|
+
const filename = `verkooporders-${new Date().toISOString().slice(0, 10)}.csv`;
|
|
197
|
+
return { csv, filename };
|
|
198
|
+
}
|