react-os-shell 3.7.0 → 3.8.0

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.
Files changed (31) hide show
  1. package/dist/{Browser-IYC3LJ5P.js → Browser-HVG4RSGD.js} +4 -4
  2. package/dist/{Browser-IYC3LJ5P.js.map → Browser-HVG4RSGD.js.map} +1 -1
  3. package/dist/{Documents-YK7FB475.js → Documents-VOZ2WASX.js} +3 -3
  4. package/dist/{Documents-YK7FB475.js.map → Documents-VOZ2WASX.js.map} +1 -1
  5. package/dist/Files-7BZV5HOG.js +12 -0
  6. package/dist/{Files-FKLB5NV2.js.map → Files-7BZV5HOG.js.map} +1 -1
  7. package/dist/{Notepad-TXUDJ4TG.js → Notepad-SJDAS6CF.js} +3 -3
  8. package/dist/{Notepad-TXUDJ4TG.js.map → Notepad-SJDAS6CF.js.map} +1 -1
  9. package/dist/Preview-W6XLBSUR.js +8 -0
  10. package/dist/{Preview-WIV5TUJU.js.map → Preview-W6XLBSUR.js.map} +1 -1
  11. package/dist/{Spreadsheet-MUIPMKMH.js → Spreadsheet-KKWKUZVP.js} +4 -4
  12. package/dist/{Spreadsheet-MUIPMKMH.js.map → Spreadsheet-KKWKUZVP.js.map} +1 -1
  13. package/dist/apps/index.js +12 -12
  14. package/dist/{chunk-N3UIMOYF.js → chunk-HWHRQMYQ.js} +3 -3
  15. package/dist/{chunk-N3UIMOYF.js.map → chunk-HWHRQMYQ.js.map} +1 -1
  16. package/dist/{chunk-AUQ74UJM.js → chunk-L7UUXITN.js} +3 -3
  17. package/dist/{chunk-AUQ74UJM.js.map → chunk-L7UUXITN.js.map} +1 -1
  18. package/dist/{chunk-RTT4QFJ7.js → chunk-LIBK24SJ.js} +4 -4
  19. package/dist/{chunk-RTT4QFJ7.js.map → chunk-LIBK24SJ.js.map} +1 -1
  20. package/dist/{chunk-ACJVFI6M.js → chunk-TU7WAYDY.js} +3 -3
  21. package/dist/{chunk-ACJVFI6M.js.map → chunk-TU7WAYDY.js.map} +1 -1
  22. package/dist/{chunk-6FKV2CQK.js → chunk-VNDRNSOU.js} +3 -3
  23. package/dist/{chunk-6FKV2CQK.js.map → chunk-VNDRNSOU.js.map} +1 -1
  24. package/dist/{chunk-K62Q5WNU.js → chunk-X33RYNHS.js} +4 -4
  25. package/dist/{chunk-K62Q5WNU.js.map → chunk-X33RYNHS.js.map} +1 -1
  26. package/dist/index.d.ts +338 -1
  27. package/dist/index.js +1370 -8
  28. package/dist/index.js.map +1 -1
  29. package/package.json +1 -1
  30. package/dist/Files-FKLB5NV2.js +0 -12
  31. package/dist/Preview-WIV5TUJU.js +0 -8
package/dist/index.js CHANGED
@@ -1,19 +1,21 @@
1
1
  import { subscribePomo, getPomoSnapshot } from './chunk-L7B66ALK.js';
2
2
  export { setShellTodoProvider } from './chunk-L7B66ALK.js';
3
- import { PREVIEW_OPENED_EVENT, publishDesktopFolders, requestFilesTrashView, FolderGlyph, openPreviewFile, requestFilesDesktopFolderView, FileIconTile, hashGradient } from './chunk-K62Q5WNU.js';
4
- export { Breadcrumbs } from './chunk-K62Q5WNU.js';
3
+ import { PREVIEW_OPENED_EVENT, publishDesktopFolders, requestFilesTrashView, FolderGlyph, openPreviewFile, requestFilesDesktopFolderView, FileIconTile, hashGradient } from './chunk-X33RYNHS.js';
4
+ export { Breadcrumbs } from './chunk-X33RYNHS.js';
5
5
  import { SidebarLayout } from './chunk-VGTEM5RZ.js';
6
6
  export { SidebarLayout } from './chunk-VGTEM5RZ.js';
7
7
  import { playNotification, playStartup, soundsEnabled, getSoundConfig, SOUND_PACK_KEYS, SOUND_PACKS, SOUND_TYPES, SOUND_TYPE_LABELS, setSoundForType, previewSound, setAllSounds, playLogout } from './chunk-D7PYW2QS.js';
8
- export { ImageAnnotator_default as ImageAnnotator } from './chunk-AUQ74UJM.js';
8
+ import { setPdfPreview } from './chunk-L7UUXITN.js';
9
+ export { ImageAnnotator_default as ImageAnnotator } from './chunk-L7UUXITN.js';
9
10
  import { toast_default } from './chunk-VENYVK3L.js';
10
11
  export { toast_default as toast } from './chunk-VENYVK3L.js';
11
- export { EditableGrid } from './chunk-N3UIMOYF.js';
12
+ import { EditableGrid } from './chunk-HWHRQMYQ.js';
13
+ export { EditableGrid } from './chunk-HWHRQMYQ.js';
12
14
  import { useAuth, useShellAuth } from './chunk-ADJ3CERD.js';
13
15
  export { ShellAuthProvider, setShellAuthBridge, useShellAuth } from './chunk-ADJ3CERD.js';
14
- import { APP_VERSION } from './chunk-6FKV2CQK.js';
15
- export { VERSION } from './chunk-6FKV2CQK.js';
16
- import { useWindowManager, PopupMenu, PopupMenuLabel, PopupMenuDivider, PopupMenuItem, WINDOW_REGISTRY, isPageEntry, Modal, useShellPrefs, useIsMobile, ModalActions, useModalActive, client_default, LoadingSpinner, setWindowPosition, ThumbCard, activateModal } from './chunk-S22GZ4OQ.js';
16
+ import { APP_VERSION, VERSION } from './chunk-VNDRNSOU.js';
17
+ export { VERSION } from './chunk-VNDRNSOU.js';
18
+ import { useWindowManager, PopupMenu, PopupMenuLabel, PopupMenuDivider, PopupMenuItem, WINDOW_REGISTRY, isPageEntry, Modal, useShellPrefs, useIsMobile, ModalActions, useModalActive, client_default, LoadingSpinner, CancelButton, setWindowPosition, ThumbCard, activateModal } from './chunk-S22GZ4OQ.js';
17
19
  export { CancelButton, CopyButton, DocFavStar, Modal, ModalActions, PopupMenu, PopupMenuDivider, PopupMenuItem, PopupMenuLabel, ShellPrefsProvider, WindowCrashedFallback, WindowErrorBoundary, WindowManagerProvider, WindowTitle, commitExposeHighlight, exitExposeMode, getActiveWindowRoute, getExposeHighlight, getWindowPosition, isEntityEntry, isPageEntry, registerModalEscapeInterceptor, setExposeHighlight, setShellApiClient, setShellWindowRegistry, setWindowDefaultPosition, setWindowPosition, subscribeExposeHighlight, toggleExposeMode, useLocalStoragePrefs, useModalActive, useShellPrefs, useWidgetSettings, useWindowManager, useWindowMenuItem, useWindowTitle } from './chunk-S22GZ4OQ.js';
18
20
  export { ConfirmProvider, confirm, confirmDestructive, prompt } from './chunk-UBN4IUDE.js';
19
21
  import { glassStyle, startMenuCategories, navSections, isSection, GLASS_INPUT_BG, navIcons, sectionIcons } from './chunk-ZF6AYO4G.js';
@@ -6841,6 +6843,1366 @@ function SidebarNavItem({ label, count, active, onClick }) {
6841
6843
  }
6842
6844
  );
6843
6845
  }
6846
+
6847
+ // src/utils/mergeBulkItems.ts
6848
+ function findDuplicateKeys(rows, keyRowKey) {
6849
+ const groups = /* @__PURE__ */ new Map();
6850
+ rows.forEach((r, i) => {
6851
+ const raw = r[keyRowKey];
6852
+ const trimmed = typeof raw === "string" ? raw.trim() : "";
6853
+ if (!trimmed) return;
6854
+ const dedupeKey = trimmed.toUpperCase();
6855
+ const existing = groups.get(dedupeKey);
6856
+ if (existing) existing.rowIndices.push(i);
6857
+ else groups.set(dedupeKey, { key: trimmed, rowIndices: [i] });
6858
+ });
6859
+ const out = [];
6860
+ for (const g of groups.values()) {
6861
+ if (g.rowIndices.length >= 2) out.push(g);
6862
+ }
6863
+ return out;
6864
+ }
6865
+ function pickFilled(row, fieldMap) {
6866
+ const out = {};
6867
+ for (const [rowKey, itemField] of Object.entries(fieldMap)) {
6868
+ const v = row[rowKey];
6869
+ const trimmed = typeof v === "string" ? v.trim() : "";
6870
+ if (trimmed !== "") out[itemField] = trimmed;
6871
+ }
6872
+ return out;
6873
+ }
6874
+ function mergeBulkItems(options) {
6875
+ const { rows, existing, fieldMap, newItem, keyField = "part_number" } = options;
6876
+ const keyEntry = Object.entries(fieldMap).find(([, v]) => v === keyField);
6877
+ if (!keyEntry) {
6878
+ throw new Error(`mergeBulkItems: fieldMap must map a row column to "${keyField}"`);
6879
+ }
6880
+ const keyRowKey = keyEntry[0];
6881
+ const validRows = rows.filter((r) => (r[keyRowKey] ?? "").trim());
6882
+ if (validRows.length === 0) return { merged: existing, importedCount: 0 };
6883
+ const existingMap = /* @__PURE__ */ new Map();
6884
+ existing.forEach((item, i) => {
6885
+ const k = item[keyField];
6886
+ if (typeof k === "string" && k.trim()) existingMap.set(k.toUpperCase(), i);
6887
+ });
6888
+ const result = [...existing];
6889
+ const added = [];
6890
+ for (const r of validRows) {
6891
+ const filled = pickFilled(r, fieldMap);
6892
+ const k = filled[keyField];
6893
+ if (!k) continue;
6894
+ const idx = existingMap.get(k.toUpperCase());
6895
+ if (idx !== void 0) {
6896
+ result[idx] = { ...result[idx], ...filled };
6897
+ } else {
6898
+ added.push({ ...newItem(filled), _isNew: true });
6899
+ }
6900
+ }
6901
+ return { merged: [...result, ...added], importedCount: validRows.length };
6902
+ }
6903
+ function cleanNumber(s) {
6904
+ if (!s) return "";
6905
+ return s.replace(/[$¥¥€£,\s]/g, "");
6906
+ }
6907
+ function parseCSVLine(line) {
6908
+ if (line.includes(" ")) return line.split(" ").map((s) => s.trim());
6909
+ const parts = [];
6910
+ let current = "";
6911
+ let inQuotes = false;
6912
+ for (const ch of line) {
6913
+ if (ch === '"') {
6914
+ inQuotes = !inQuotes;
6915
+ continue;
6916
+ }
6917
+ if (ch === "," && !inQuotes) {
6918
+ parts.push(current.trim());
6919
+ current = "";
6920
+ continue;
6921
+ }
6922
+ current += ch;
6923
+ }
6924
+ parts.push(current.trim());
6925
+ return parts;
6926
+ }
6927
+ function colKind(col, index) {
6928
+ if (index === 0) return "key";
6929
+ return col.kind ?? "text";
6930
+ }
6931
+ function detectColumnType(values) {
6932
+ const nonEmpty = values.filter((v) => v.trim());
6933
+ if (nonEmpty.length === 0) return "skip";
6934
+ const allIntOrEmpty = values.every((v) => !v.trim() || /^\d+$/.test(v.trim()));
6935
+ if (allIntOrEmpty) {
6936
+ const nums = nonEmpty.map((v) => parseInt(v));
6937
+ const isSequential = nums.length > 2 && nums.every((n, i) => i === 0 || n >= nums[i - 1]);
6938
+ if (isSequential) return "skip";
6939
+ }
6940
+ const hasLetters = nonEmpty.some((v) => /[a-zA-Z]/.test(v));
6941
+ const hasDollar = nonEmpty.some((v) => /\$/.test(v));
6942
+ const hasDecimals = nonEmpty.filter((v) => /\.\d{2}$/.test(cleanNumber(v))).length > nonEmpty.length * 0.5;
6943
+ const allWholeNumbers = nonEmpty.every((v) => /^\d+$/.test(cleanNumber(v)));
6944
+ if (hasLetters && !hasDollar) return "key";
6945
+ if (hasDollar) return "price";
6946
+ if (hasDecimals) return "price";
6947
+ if (allWholeNumbers) {
6948
+ const avg = nonEmpty.reduce((s, v) => s + parseInt(cleanNumber(v)), 0) / nonEmpty.length;
6949
+ if (avg > 10) return "qty";
6950
+ return "skip";
6951
+ }
6952
+ return "key";
6953
+ }
6954
+ function autoMapColumns(csvData, targetColumns) {
6955
+ if (csvData.length === 0 || csvData[0].length === 0) return null;
6956
+ const csvColCount = csvData[0].length;
6957
+ const targetKinds = targetColumns.map((c, i) => colKind(c, i));
6958
+ const csvColTypes = [];
6959
+ for (let c = 0; c < csvColCount; c++) {
6960
+ const values = csvData.map((row) => row[c] || "");
6961
+ csvColTypes.push(detectColumnType(values));
6962
+ }
6963
+ const mapping = new Array(targetColumns.length).fill(-1);
6964
+ const used = /* @__PURE__ */ new Set();
6965
+ for (let t = 0; t < targetColumns.length; t++) {
6966
+ const kind = targetKinds[t];
6967
+ if (kind === "text") continue;
6968
+ for (let c = 0; c < csvColCount; c++) {
6969
+ if (used.has(c)) continue;
6970
+ if (csvColTypes[c] === kind) {
6971
+ mapping[t] = c;
6972
+ used.add(c);
6973
+ break;
6974
+ }
6975
+ }
6976
+ }
6977
+ const priceTargets = targetKinds.map((k, i) => ({ i, k })).filter((x) => x.k === "price");
6978
+ const priceCsvCols = csvColTypes.map((t, i) => ({ i, type: t })).filter((x) => x.type === "price" && !used.has(x.i));
6979
+ let pi = 0;
6980
+ for (const pt of priceTargets) {
6981
+ if (mapping[pt.i] === -1 && pi < priceCsvCols.length) {
6982
+ mapping[pt.i] = priceCsvCols[pi].i;
6983
+ used.add(priceCsvCols[pi].i);
6984
+ pi++;
6985
+ }
6986
+ }
6987
+ if (mapping[0] === -1) return null;
6988
+ return mapping;
6989
+ }
6990
+ function ColumnMapper({ csvPreview, targetColumns, onConfirm, onCancel }) {
6991
+ const autoMapping = autoMapColumns(csvPreview, targetColumns);
6992
+ const [mapping, setMapping] = useState(autoMapping || new Array(targetColumns.length).fill(-1));
6993
+ const csvColCount = csvPreview[0]?.length || 0;
6994
+ const keyMapped = mapping[0] >= 0;
6995
+ return /* @__PURE__ */ jsxs("div", { className: "border border-blue-200 rounded-lg bg-blue-50 p-4 mb-3", children: [
6996
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-gray-800 mb-3", children: "Map CSV columns to fields" }),
6997
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-x-6 gap-y-2 mb-4", children: targetColumns.map((col, ti) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
6998
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-gray-700 w-28 shrink-0", children: col.title }),
6999
+ /* @__PURE__ */ jsxs(
7000
+ "select",
7001
+ {
7002
+ value: mapping[ti],
7003
+ onChange: (e) => setMapping((prev) => {
7004
+ const next = [...prev];
7005
+ next[ti] = parseInt(e.target.value);
7006
+ return next;
7007
+ }),
7008
+ className: "flex-1 border border-gray-300 rounded px-2 py-1 text-sm focus:border-blue-500 focus:ring-blue-500",
7009
+ children: [
7010
+ /* @__PURE__ */ jsx("option", { value: -1, children: "\u2014 Skip \u2014" }),
7011
+ Array.from({ length: csvColCount }, (_, ci) => /* @__PURE__ */ jsxs("option", { value: ci, children: [
7012
+ "Column ",
7013
+ ci + 1,
7014
+ " (e.g. ",
7015
+ csvPreview[0]?.[ci] || "",
7016
+ ")"
7017
+ ] }, ci))
7018
+ ]
7019
+ }
7020
+ )
7021
+ ] }, col.key)) }),
7022
+ /* @__PURE__ */ jsx("div", { className: "border border-gray-200 rounded overflow-hidden mb-3", children: /* @__PURE__ */ jsxs("table", { className: "min-w-full text-xs", children: [
7023
+ /* @__PURE__ */ jsx("thead", { className: "bg-gray-100", children: /* @__PURE__ */ jsx("tr", { children: targetColumns.map((col) => /* @__PURE__ */ jsx("th", { className: "px-2 py-1 text-left text-gray-500 font-medium", children: col.title }, col.key)) }) }),
7024
+ /* @__PURE__ */ jsx("tbody", { className: "divide-y divide-gray-100", children: csvPreview.slice(0, 3).map((row, ri) => /* @__PURE__ */ jsx("tr", { children: targetColumns.map((col, ti) => /* @__PURE__ */ jsx("td", { className: "px-2 py-1 text-gray-700 font-mono", children: mapping[ti] >= 0 ? row[mapping[ti]] || "" : /* @__PURE__ */ jsx("span", { className: "text-gray-300", children: "\u2014" }) }, col.key)) }, ri)) })
7025
+ ] }) }),
7026
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
7027
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-gray-500", children: [
7028
+ csvPreview.length,
7029
+ " rows detected"
7030
+ ] }),
7031
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
7032
+ /* @__PURE__ */ jsx(CancelButton, { onClick: onCancel, className: "text-sm text-gray-500 hover:text-gray-700", children: "Cancel" }),
7033
+ /* @__PURE__ */ jsx(
7034
+ "button",
7035
+ {
7036
+ type: "button",
7037
+ onClick: () => onConfirm(mapping),
7038
+ disabled: !keyMapped,
7039
+ className: "bg-blue-600 text-white px-3 py-1.5 text-sm font-medium rounded-lg hover:bg-blue-700 disabled:opacity-50",
7040
+ children: "Confirm Mapping"
7041
+ }
7042
+ )
7043
+ ] })
7044
+ ] })
7045
+ ] });
7046
+ }
7047
+ function DuplicateReview({
7048
+ rows,
7049
+ groups,
7050
+ columns,
7051
+ resolutions,
7052
+ onResolutionChange,
7053
+ onConfirm,
7054
+ onCancel
7055
+ }) {
7056
+ const totalKept = groups.reduce((acc, g) => {
7057
+ const r = resolutions[g.key.toUpperCase()];
7058
+ if (r === "skip") return acc;
7059
+ return acc + 1;
7060
+ }, 0);
7061
+ const totalDropped = groups.reduce((acc, g) => {
7062
+ const r = resolutions[g.key.toUpperCase()];
7063
+ if (r === "skip") return acc + g.rowIndices.length;
7064
+ return acc + (g.rowIndices.length - 1);
7065
+ }, 0);
7066
+ return /* @__PURE__ */ jsxs("div", { className: "border border-amber-200 rounded-lg bg-amber-50 p-4 mb-3", children: [
7067
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-gray-800 mb-1", children: "Duplicate entries detected" }),
7068
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-600 mb-3", children: [
7069
+ groups.length,
7070
+ " ",
7071
+ groups.length === 1 ? "value appears" : "values appear",
7072
+ " more than once in your import. Choose how to handle each before proceeding."
7073
+ ] }),
7074
+ /* @__PURE__ */ jsx("div", { className: "space-y-3 mb-3 max-h-80 overflow-y-auto", children: groups.map((g) => {
7075
+ const dupKey = g.key.toUpperCase();
7076
+ const choice = resolutions[dupKey] || "first";
7077
+ return /* @__PURE__ */ jsxs("div", { className: "border border-amber-200 rounded bg-white p-3", children: [
7078
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
7079
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-mono font-medium text-gray-800", children: g.key }),
7080
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-gray-500", children: [
7081
+ g.rowIndices.length,
7082
+ " occurrences"
7083
+ ] })
7084
+ ] }),
7085
+ /* @__PURE__ */ jsx("div", { className: "border border-gray-200 rounded overflow-hidden mb-2", children: /* @__PURE__ */ jsxs("table", { className: "min-w-full text-xs", children: [
7086
+ /* @__PURE__ */ jsx("thead", { className: "bg-gray-100", children: /* @__PURE__ */ jsx("tr", { children: columns.map((col) => /* @__PURE__ */ jsx("th", { className: "px-2 py-1 text-left text-gray-500 font-medium", children: col.title.replace(" *", "") }, col.key)) }) }),
7087
+ /* @__PURE__ */ jsx("tbody", { className: "divide-y divide-gray-100", children: g.rowIndices.map((ri, occ) => /* @__PURE__ */ jsx("tr", { className: choice === "skip" ? "opacity-40" : choice === "first" && occ !== 0 || choice === "last" && occ !== g.rowIndices.length - 1 ? "opacity-40 line-through" : "", children: columns.map((col) => /* @__PURE__ */ jsx("td", { className: "px-2 py-1 text-gray-700 font-mono", children: rows[ri]?.[col.key] || /* @__PURE__ */ jsx("span", { className: "text-gray-300", children: "\u2014" }) }, col.key)) }, ri)) })
7088
+ ] }) }),
7089
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-1 text-xs", children: ["first", "last", "skip"].map((opt) => /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1 cursor-pointer", children: [
7090
+ /* @__PURE__ */ jsx(
7091
+ "input",
7092
+ {
7093
+ type: "radio",
7094
+ name: `dup-${dupKey}`,
7095
+ value: opt,
7096
+ checked: choice === opt,
7097
+ onChange: () => onResolutionChange(dupKey, opt),
7098
+ className: "h-3 w-3 text-amber-600 focus:ring-amber-500"
7099
+ }
7100
+ ),
7101
+ /* @__PURE__ */ jsx("span", { className: "text-gray-700", children: opt === "first" ? "Keep first" : opt === "last" ? "Keep last" : "Skip all" })
7102
+ ] }, opt)) })
7103
+ ] }, dupKey);
7104
+ }) }),
7105
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
7106
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-gray-600", children: [
7107
+ totalKept,
7108
+ " kept \xB7 ",
7109
+ totalDropped,
7110
+ " dropped"
7111
+ ] }),
7112
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
7113
+ /* @__PURE__ */ jsx(CancelButton, { onClick: onCancel, className: "text-sm text-gray-500 hover:text-gray-700", children: "Cancel" }),
7114
+ /* @__PURE__ */ jsx(
7115
+ "button",
7116
+ {
7117
+ type: "button",
7118
+ onClick: onConfirm,
7119
+ className: "bg-blue-600 text-white px-3 py-1.5 text-sm font-medium rounded-lg hover:bg-blue-700",
7120
+ children: "Proceed"
7121
+ }
7122
+ )
7123
+ ] })
7124
+ ] })
7125
+ ] });
7126
+ }
7127
+ function mergeDuplicateRows(rows, groups, columns) {
7128
+ const keyCol = columns[0].key;
7129
+ const firstOf = /* @__PURE__ */ new Map();
7130
+ const dropped = /* @__PURE__ */ new Set();
7131
+ for (const g of groups) {
7132
+ firstOf.set(g.rowIndices[0], g);
7133
+ g.rowIndices.slice(1).forEach((i) => dropped.add(i));
7134
+ }
7135
+ const out = [];
7136
+ rows.forEach((row, i) => {
7137
+ const g = firstOf.get(i);
7138
+ if (g) {
7139
+ const merged = {};
7140
+ for (const col of columns) {
7141
+ if (col.key === keyCol) {
7142
+ merged[col.key] = g.key;
7143
+ continue;
7144
+ }
7145
+ const nums = g.rowIndices.map((ri) => (rows[ri]?.[col.key] ?? "").trim()).filter((v) => v !== "" && Number.isFinite(Number(v))).map(Number);
7146
+ merged[col.key] = nums.length > 0 ? String(nums.reduce((s, n) => s + n, 0)) : "";
7147
+ }
7148
+ out.push(merged);
7149
+ } else if (!dropped.has(i)) {
7150
+ out.push(row);
7151
+ }
7152
+ });
7153
+ return out;
7154
+ }
7155
+ function MergeReview({
7156
+ rows,
7157
+ groups,
7158
+ columns,
7159
+ onConfirm,
7160
+ onCancel
7161
+ }) {
7162
+ const keyCol = columns[0].key;
7163
+ const dupRowCount = groups.reduce((acc, g) => acc + g.rowIndices.length, 0);
7164
+ return /* @__PURE__ */ jsxs("div", { className: "border border-amber-200 rounded-lg bg-amber-50 p-4 mb-3", children: [
7165
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-gray-800 mb-1", children: "Duplicate entries detected" }),
7166
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-600 mb-3", children: [
7167
+ groups.length,
7168
+ " ",
7169
+ groups.length === 1 ? "value appeared" : "values appeared",
7170
+ " more than once. Each is merged into a single line with its quantity summed \u2014 review before proceeding."
7171
+ ] }),
7172
+ /* @__PURE__ */ jsx("div", { className: "border border-gray-200 rounded overflow-hidden mb-3 bg-white max-h-80 overflow-y-auto", children: /* @__PURE__ */ jsxs("table", { className: "min-w-full text-xs", children: [
7173
+ /* @__PURE__ */ jsx("thead", { className: "bg-gray-100", children: /* @__PURE__ */ jsxs("tr", { children: [
7174
+ columns.map((col) => /* @__PURE__ */ jsx("th", { className: "px-2 py-1 text-left text-gray-500 font-medium", children: col.title.replace(" *", "") }, col.key)),
7175
+ /* @__PURE__ */ jsx("th", { className: "px-2 py-1 text-right text-gray-500 font-medium", children: "Merged from" })
7176
+ ] }) }),
7177
+ /* @__PURE__ */ jsx("tbody", { className: "divide-y divide-gray-100", children: groups.map((g) => /* @__PURE__ */ jsxs("tr", { children: [
7178
+ columns.map((col) => {
7179
+ if (col.key === keyCol) {
7180
+ return /* @__PURE__ */ jsx("td", { className: "px-2 py-1 font-mono font-medium text-gray-800", children: g.key }, col.key);
7181
+ }
7182
+ const nums = g.rowIndices.map((ri) => (rows[ri]?.[col.key] ?? "").trim()).filter((v) => v !== "" && Number.isFinite(Number(v))).map(Number);
7183
+ if (nums.length === 0) {
7184
+ return /* @__PURE__ */ jsx("td", { className: "px-2 py-1 text-gray-300", children: "\u2014" }, col.key);
7185
+ }
7186
+ const sum = nums.reduce((s, n) => s + n, 0);
7187
+ return /* @__PURE__ */ jsxs("td", { className: "px-2 py-1 font-mono text-gray-700", children: [
7188
+ nums.length > 1 && /* @__PURE__ */ jsxs("span", { className: "text-gray-400", children: [
7189
+ nums.join(" + "),
7190
+ " = "
7191
+ ] }),
7192
+ /* @__PURE__ */ jsx("span", { className: "font-semibold text-gray-800", children: sum.toLocaleString() })
7193
+ ] }, col.key);
7194
+ }),
7195
+ /* @__PURE__ */ jsxs("td", { className: "px-2 py-1 text-right text-gray-500", children: [
7196
+ g.rowIndices.length,
7197
+ " rows"
7198
+ ] })
7199
+ ] }, g.key.toUpperCase())) })
7200
+ ] }) }),
7201
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
7202
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-gray-600", children: [
7203
+ dupRowCount,
7204
+ " duplicate rows \u2192 ",
7205
+ groups.length,
7206
+ " merged line",
7207
+ groups.length === 1 ? "" : "s"
7208
+ ] }),
7209
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
7210
+ /* @__PURE__ */ jsx(CancelButton, { onClick: onCancel, className: "text-sm text-gray-500 hover:text-gray-700", children: "Cancel" }),
7211
+ /* @__PURE__ */ jsx(
7212
+ "button",
7213
+ {
7214
+ type: "button",
7215
+ onClick: onConfirm,
7216
+ className: "bg-blue-600 text-white px-3 py-1.5 text-sm font-medium rounded-lg hover:bg-blue-700",
7217
+ children: "Proceed"
7218
+ }
7219
+ )
7220
+ ] })
7221
+ ] })
7222
+ ] });
7223
+ }
7224
+ function BulkImportGrid({ columns, onImport, description, mergeDuplicates = false }) {
7225
+ const fileRef = useRef(null);
7226
+ const [gridData, setGridData] = useState(
7227
+ () => Array.from({ length: 15 }, () => Array(columns.length).fill(""))
7228
+ );
7229
+ const [importing, setImporting] = useState(false);
7230
+ const [error, setError] = useState("");
7231
+ const [pendingCSV, setPendingCSV] = useState(null);
7232
+ const [dupReview, setDupReview] = useState(null);
7233
+ const [mergeReview, setMergeReview] = useState(null);
7234
+ const colCount = columns.length;
7235
+ const requiredLabel = columns[0]?.title?.replace(" *", "") || "value";
7236
+ const filledRows = gridData.filter((row) => row[0]?.trim());
7237
+ const filledCount = filledRows.length;
7238
+ const totals = {};
7239
+ for (let ci = 1; ci < columns.length; ci++) {
7240
+ if (colKind(columns[ci], ci) === "text") continue;
7241
+ let sum = 0;
7242
+ for (const row of filledRows) {
7243
+ const v = parseFloat(cleanNumber(row[ci] || ""));
7244
+ if (!isNaN(v)) sum += v;
7245
+ }
7246
+ if (sum > 0) totals[columns[ci].key] = sum;
7247
+ }
7248
+ const gridColumns = columns.map((c) => ({
7249
+ key: c.key,
7250
+ title: c.title,
7251
+ width: c.width
7252
+ }));
7253
+ const loadDataIntoGrid = useCallback((data) => {
7254
+ const cleaned = data.map((row) => {
7255
+ const r = [...row];
7256
+ while (r.length < colCount) r.push("");
7257
+ return r.slice(0, colCount).map((cell, i) => i === 0 ? cell : cleanNumber(cell));
7258
+ });
7259
+ while (cleaned.length < 15) cleaned.push(Array(colCount).fill(""));
7260
+ setGridData(cleaned);
7261
+ }, [colCount]);
7262
+ const handleCSV = (e) => {
7263
+ const file = e.target.files?.[0];
7264
+ if (!file) return;
7265
+ const reader = new FileReader();
7266
+ reader.onload = (ev) => {
7267
+ const text = ev.target?.result || "";
7268
+ const parsed = text.split("\n").map((l) => l.trim()).filter(Boolean).map(parseCSVLine);
7269
+ if (parsed.length === 0) return;
7270
+ const mapping = autoMapColumns(parsed, columns);
7271
+ if (mapping) {
7272
+ const mapped = parsed.map((row) => columns.map((_, ti) => mapping[ti] >= 0 ? row[mapping[ti]] || "" : ""));
7273
+ loadDataIntoGrid(mapped);
7274
+ } else {
7275
+ setPendingCSV(parsed);
7276
+ }
7277
+ };
7278
+ reader.readAsText(file);
7279
+ if (fileRef.current) fileRef.current.value = "";
7280
+ };
7281
+ const handleMappingConfirm = (mapping) => {
7282
+ if (!pendingCSV) return;
7283
+ const mapped = pendingCSV.map((row) => columns.map((_, ti) => mapping[ti] >= 0 ? row[mapping[ti]] || "" : ""));
7284
+ loadDataIntoGrid(mapped);
7285
+ setPendingCSV(null);
7286
+ };
7287
+ const runImport = async (rows) => {
7288
+ setImporting(true);
7289
+ try {
7290
+ await onImport(rows);
7291
+ } catch {
7292
+ setError("Import failed.");
7293
+ }
7294
+ setImporting(false);
7295
+ };
7296
+ const handleImport = async () => {
7297
+ const rows = gridData.filter((row) => row[0]?.trim()).map((row) => {
7298
+ const obj = {};
7299
+ columns.forEach((col, i) => {
7300
+ const val = row[i]?.trim() || "";
7301
+ obj[col.key] = i === 0 ? val : cleanNumber(val);
7302
+ });
7303
+ return obj;
7304
+ });
7305
+ if (rows.length === 0) {
7306
+ setError(`Enter at least one ${requiredLabel.toLowerCase()}.`);
7307
+ return;
7308
+ }
7309
+ setError("");
7310
+ const keyCol = columns[0].key;
7311
+ const groups = findDuplicateKeys(rows, keyCol);
7312
+ if (groups.length > 0) {
7313
+ if (mergeDuplicates) {
7314
+ setMergeReview({ rows, groups });
7315
+ } else {
7316
+ const resolutions = {};
7317
+ for (const g of groups) resolutions[g.key.toUpperCase()] = "first";
7318
+ setDupReview({ rows, groups, resolutions });
7319
+ }
7320
+ return;
7321
+ }
7322
+ await runImport(rows);
7323
+ };
7324
+ const handleMergeConfirm = async () => {
7325
+ if (!mergeReview) return;
7326
+ const merged = mergeDuplicateRows(mergeReview.rows, mergeReview.groups, columns);
7327
+ setMergeReview(null);
7328
+ await runImport(merged);
7329
+ };
7330
+ const handleDupConfirm = async () => {
7331
+ if (!dupReview) return;
7332
+ const { rows, groups, resolutions } = dupReview;
7333
+ const drop = /* @__PURE__ */ new Set();
7334
+ for (const g of groups) {
7335
+ const choice = resolutions[g.key.toUpperCase()] || "first";
7336
+ const idx = g.rowIndices;
7337
+ if (choice === "first") {
7338
+ for (let i = 1; i < idx.length; i++) drop.add(idx[i]);
7339
+ } else if (choice === "last") {
7340
+ for (let i = 0; i < idx.length - 1; i++) drop.add(idx[i]);
7341
+ } else {
7342
+ for (const j of idx) drop.add(j);
7343
+ }
7344
+ }
7345
+ const filtered = rows.filter((_, i) => !drop.has(i));
7346
+ setDupReview(null);
7347
+ if (filtered.length === 0) {
7348
+ setError("All rows skipped.");
7349
+ return;
7350
+ }
7351
+ await runImport(filtered);
7352
+ };
7353
+ const handleClear = () => {
7354
+ setGridData(Array.from({ length: 15 }, () => Array(colCount).fill("")));
7355
+ };
7356
+ const descText = description || `Paste from spreadsheets, type directly, or upload a CSV. Rows don't need to be in order \u2014 they'll be matched. Values not in the list will be added to the end. Fields left blank will not be affected.`;
7357
+ return /* @__PURE__ */ jsxs("div", { className: "border border-gray-200 rounded-lg bg-gray-50 mb-3 overflow-hidden", children: [
7358
+ /* @__PURE__ */ jsxs("div", { className: "px-4 pt-3 pb-2 flex items-center justify-between", children: [
7359
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-500", children: [
7360
+ descText,
7361
+ /* @__PURE__ */ jsxs("span", { className: "text-gray-400 ml-1", children: [
7362
+ "(",
7363
+ filledCount,
7364
+ " lines",
7365
+ Object.keys(totals).length > 0 && " \xB7 ",
7366
+ Object.entries(totals).map(([k, v]) => {
7367
+ const col = columns.find((c) => c.key === k);
7368
+ const label = col?.title?.replace(" *", "") || k;
7369
+ const isPrice = col ? colKind(col, columns.indexOf(col)) === "price" : false;
7370
+ return `${label}: ${isPrice ? v.toLocaleString(void 0, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : v.toLocaleString()}`;
7371
+ }).join(" \xB7 "),
7372
+ ")"
7373
+ ] })
7374
+ ] }),
7375
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
7376
+ filledCount > 0 && /* @__PURE__ */ jsx("button", { type: "button", onClick: handleClear, className: "text-gray-400 hover:text-gray-600 text-xs", children: "Clear" }),
7377
+ /* @__PURE__ */ jsx("input", { ref: fileRef, type: "file", accept: ".csv,.tsv,.txt", onChange: handleCSV, className: "hidden" }),
7378
+ /* @__PURE__ */ jsx(
7379
+ "button",
7380
+ {
7381
+ type: "button",
7382
+ onClick: () => fileRef.current?.click(),
7383
+ className: "text-blue-600 border border-blue-200 bg-white px-3 py-1.5 text-sm font-medium rounded-lg hover:bg-blue-50 hover:border-blue-300",
7384
+ children: "Upload CSV"
7385
+ }
7386
+ ),
7387
+ /* @__PURE__ */ jsx(
7388
+ "button",
7389
+ {
7390
+ type: "button",
7391
+ onClick: handleImport,
7392
+ disabled: importing || filledCount === 0,
7393
+ className: "text-green-600 border border-green-200 bg-white px-3 py-1.5 text-sm font-medium rounded-lg hover:bg-green-50 hover:border-green-300 disabled:opacity-50",
7394
+ children: importing ? "Importing..." : `Import ${filledCount} Lines`
7395
+ }
7396
+ )
7397
+ ] })
7398
+ ] }),
7399
+ pendingCSV && /* @__PURE__ */ jsx("div", { className: "px-4 pb-2", children: /* @__PURE__ */ jsx(
7400
+ ColumnMapper,
7401
+ {
7402
+ csvPreview: pendingCSV,
7403
+ targetColumns: columns,
7404
+ onConfirm: handleMappingConfirm,
7405
+ onCancel: () => setPendingCSV(null)
7406
+ }
7407
+ ) }),
7408
+ dupReview && /* @__PURE__ */ jsx("div", { className: "px-4 pb-2", children: /* @__PURE__ */ jsx(
7409
+ DuplicateReview,
7410
+ {
7411
+ rows: dupReview.rows,
7412
+ groups: dupReview.groups,
7413
+ columns,
7414
+ resolutions: dupReview.resolutions,
7415
+ onResolutionChange: (dupKey, choice) => setDupReview((prev) => prev ? { ...prev, resolutions: { ...prev.resolutions, [dupKey]: choice } } : prev),
7416
+ onConfirm: handleDupConfirm,
7417
+ onCancel: () => setDupReview(null)
7418
+ }
7419
+ ) }),
7420
+ mergeReview && /* @__PURE__ */ jsx("div", { className: "px-4 pb-2", children: /* @__PURE__ */ jsx(
7421
+ MergeReview,
7422
+ {
7423
+ rows: mergeReview.rows,
7424
+ groups: mergeReview.groups,
7425
+ columns,
7426
+ onConfirm: handleMergeConfirm,
7427
+ onCancel: () => setMergeReview(null)
7428
+ }
7429
+ ) }),
7430
+ /* @__PURE__ */ jsx(EditableGrid, { columns: gridColumns, data: gridData, onChange: setGridData }),
7431
+ error && /* @__PURE__ */ jsx("p", { className: "text-xs text-red-600 px-4 py-1", children: error })
7432
+ ] });
7433
+ }
7434
+ var C20 = 33;
7435
+ var C40 = 67;
7436
+ var toInt = (v) => typeof v === "number" ? v : parseInt(String(v ?? "")) || 0;
7437
+ function ContainerFillChart({
7438
+ items,
7439
+ getVolume,
7440
+ qtyField = "instruction",
7441
+ showNewIndicator = false,
7442
+ getInstructionQty = (item) => item.quantity,
7443
+ getActualQty = (item) => item.actual_qty,
7444
+ isFilled,
7445
+ isNew = (item) => Boolean(item._isNew),
7446
+ className,
7447
+ style
7448
+ }) {
7449
+ const filledLines = isFilled ? items.filter(isFilled) : items;
7450
+ const qtyInstr = (l) => toInt(getInstructionQty(l));
7451
+ const qtyActual = (l) => toInt(getActualQty(l));
7452
+ const hasActual = filledLines.some((l) => getActualQty(l) != null && qtyActual(l) > 0);
7453
+ const qtyOf = (l) => {
7454
+ if (hasActual) return qtyInstr(l);
7455
+ return qtyField === "actual" ? getActualQty(l) ?? getInstructionQty(l) ?? 0 : getInstructionQty(l) ?? 0;
7456
+ };
7457
+ const totalQty = filledLines.reduce((s, l) => s + toInt(qtyOf(l)), 0);
7458
+ const newCount = filledLines.filter((l) => isNew(l)).length;
7459
+ const newQty = filledLines.filter((l) => isNew(l)).reduce((s, l) => s + toInt(qtyOf(l)), 0);
7460
+ const totalQtyInstr = filledLines.reduce((s, l) => s + qtyInstr(l), 0);
7461
+ const totalQtyActual = filledLines.reduce((s, l) => s + qtyActual(l), 0);
7462
+ const totalVolumeInstr = filledLines.reduce((s, l) => s + getVolume(l) * qtyInstr(l), 0);
7463
+ const totalVolumeActual = filledLines.reduce((s, l) => s + getVolume(l) * qtyActual(l), 0);
7464
+ const totalVolume = hasActual ? Math.max(totalVolumeInstr, totalVolumeActual) : qtyField === "actual" ? totalVolumeActual || totalVolumeInstr : totalVolumeInstr;
7465
+ const useType = totalVolume <= C20 * 1.05 ? "20ft" : "40ft";
7466
+ const containerCap = useType === "20ft" ? C20 : C40;
7467
+ const totalContainers = totalVolume > 0 ? Math.max(1, Math.ceil(totalVolume / containerCap)) : 0;
7468
+ const totalCap = totalContainers * containerCap;
7469
+ const isOptimal = totalVolume > 0 && totalVolume >= totalCap * 0.95 && totalVolume <= totalCap * 1.1;
7470
+ if (filledLines.length === 0) return null;
7471
+ const fillFor = (volume, idx) => containerCap > 0 ? Math.max(0, Math.min((volume - idx * containerCap) / containerCap * 100, 100)) : 0;
7472
+ return /* @__PURE__ */ jsxs("div", { className: `mt-2 px-1 space-y-2 ${className ?? ""}`.trim(), style, children: [
7473
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs text-gray-500", children: [
7474
+ /* @__PURE__ */ jsxs("span", { children: [
7475
+ filledLines.length,
7476
+ " items \xB7 ",
7477
+ totalQty,
7478
+ " pcs",
7479
+ hasActual && totalQtyInstr !== totalQtyActual && /* @__PURE__ */ jsxs("span", { className: "ml-1 text-gray-400", children: [
7480
+ "(",
7481
+ totalQtyInstr,
7482
+ " instr / ",
7483
+ totalQtyActual,
7484
+ " loaded)"
7485
+ ] })
7486
+ ] }),
7487
+ showNewIndicator && newCount > 0 && /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 text-green-600", children: [
7488
+ /* @__PURE__ */ jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-green-500" }),
7489
+ newCount,
7490
+ " new (",
7491
+ newQty,
7492
+ " pcs)"
7493
+ ] })
7494
+ ] }),
7495
+ totalVolume > 0 ? /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
7496
+ Array.from({ length: totalContainers }).map((_, i) => {
7497
+ const instrPct = fillFor(totalVolumeInstr, i);
7498
+ const actualPct = fillFor(totalVolumeActual, i);
7499
+ const dual = hasActual;
7500
+ const longerPct = Math.max(instrPct, actualPct);
7501
+ const shorterPct = Math.min(instrPct, actualPct);
7502
+ const longerIsInstr = instrPct >= actualPct;
7503
+ const longerColor = dual ? longerIsInstr ? "bg-blue-500" : "bg-green-500" : "bg-blue-500";
7504
+ const shorterColor = dual ? longerIsInstr ? "bg-green-500" : "bg-blue-500" : "";
7505
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
7506
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-gray-400 w-8 shrink-0", children: useType }),
7507
+ /* @__PURE__ */ jsxs("div", { className: "relative flex-1 h-4 bg-gray-100 rounded-full overflow-hidden border border-gray-200", children: [
7508
+ /* @__PURE__ */ jsx("div", { className: `absolute inset-y-0 left-0 ${longerColor} transition-all`, style: { width: `${longerPct}%` } }),
7509
+ dual && shorterPct > 0 && /* @__PURE__ */ jsx("div", { className: `absolute inset-y-0 left-0 ${shorterColor} transition-all`, style: { width: `${shorterPct}%` } })
7510
+ ] }),
7511
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-gray-500 w-16 text-right", children: dual ? `${Math.min(totalVolumeActual - i * containerCap, containerCap).toFixed(1)} / ${containerCap} m\xB3` : `${Math.min(totalVolumeInstr - i * containerCap, containerCap).toFixed(1)} / ${containerCap} m\xB3` })
7512
+ ] }, `bar-${i}`);
7513
+ }),
7514
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs pt-0.5", children: [
7515
+ /* @__PURE__ */ jsxs("span", { className: "text-gray-500 inline-flex items-center gap-1", children: [
7516
+ "Total:",
7517
+ hasActual ? /* @__PURE__ */ jsxs(Fragment, { children: [
7518
+ /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 text-blue-700", children: [
7519
+ /* @__PURE__ */ jsx("span", { className: "inline-block w-2 h-2 rounded-full bg-blue-500" }),
7520
+ totalVolumeInstr.toFixed(2),
7521
+ " m\xB3 instruction"
7522
+ ] }),
7523
+ /* @__PURE__ */ jsx("span", { className: "text-gray-400", children: "/" }),
7524
+ /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 text-green-700", children: [
7525
+ /* @__PURE__ */ jsx("span", { className: "inline-block w-2 h-2 rounded-full bg-green-500" }),
7526
+ totalVolumeActual.toFixed(2),
7527
+ " m\xB3 loaded"
7528
+ ] })
7529
+ ] }) : /* @__PURE__ */ jsxs("b", { className: "text-gray-700", children: [
7530
+ totalVolume.toFixed(2),
7531
+ " m\xB3"
7532
+ ] })
7533
+ ] }),
7534
+ /* @__PURE__ */ jsxs("span", { className: `font-medium ${isOptimal ? "text-green-600" : "text-gray-500"}`, children: [
7535
+ isOptimal && "\u2713 ",
7536
+ totalContainers,
7537
+ " \xD7 ",
7538
+ useType
7539
+ ] })
7540
+ ] })
7541
+ ] }) : /* @__PURE__ */ jsx("div", { className: "text-xs text-gray-400", children: "Volume: N/A (no volume data for these part numbers)" })
7542
+ ] });
7543
+ }
7544
+ var DEFAULT_POLL_MS = 15e3;
7545
+ var DEFAULT_REQUEST_TIMEOUT_MS = 5e3;
7546
+ function timeAgo3(date) {
7547
+ if (!date) return "\u2014";
7548
+ const diff = Math.max(0, Date.now() - date.getTime());
7549
+ if (diff < 1500) return "just now";
7550
+ if (diff < 6e4) return `${Math.round(diff / 1e3)}s ago`;
7551
+ if (diff < 36e5) return `${Math.round(diff / 6e4)}m ago`;
7552
+ return `${Math.round(diff / 36e5)}h ago`;
7553
+ }
7554
+ function ServerStatusIndicator({
7555
+ healthCheck,
7556
+ healthUrl = "/api/health/",
7557
+ pollMs = DEFAULT_POLL_MS,
7558
+ requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
7559
+ user,
7560
+ version = VERSION
7561
+ }) {
7562
+ const [status, setStatus] = useState("checking");
7563
+ const [lastOkAt, setLastOkAt] = useState(null);
7564
+ const [lastFailAt, setLastFailAt] = useState(null);
7565
+ const [lastLatencyMs, setLastLatencyMs] = useState(null);
7566
+ const [browserOnline, setBrowserOnline] = useState(navigator.onLine);
7567
+ const [open, setOpen] = useState(false);
7568
+ const [popPos, setPopPos] = useState({ left: 0, top: 0 });
7569
+ const [, setTick] = useState(0);
7570
+ const cancelRef = useRef(false);
7571
+ const buttonRef = useRef(null);
7572
+ const popRef = useRef(null);
7573
+ const healthCheckRef = useRef(healthCheck);
7574
+ const healthUrlRef = useRef(healthUrl);
7575
+ const requestTimeoutRef = useRef(requestTimeoutMs);
7576
+ healthCheckRef.current = healthCheck;
7577
+ healthUrlRef.current = healthUrl;
7578
+ requestTimeoutRef.current = requestTimeoutMs;
7579
+ const ping = useCallback(async () => {
7580
+ const t0 = performance.now();
7581
+ try {
7582
+ let result;
7583
+ if (healthCheckRef.current) {
7584
+ result = await healthCheckRef.current();
7585
+ } else {
7586
+ const ctrl = new AbortController();
7587
+ const timer = setTimeout(() => ctrl.abort(), requestTimeoutRef.current);
7588
+ try {
7589
+ const res = await fetch(healthUrlRef.current, {
7590
+ method: "GET",
7591
+ cache: "no-store",
7592
+ signal: ctrl.signal
7593
+ });
7594
+ result = { ok: res.ok, latencyMs: Math.round(performance.now() - t0) };
7595
+ } finally {
7596
+ clearTimeout(timer);
7597
+ }
7598
+ }
7599
+ if (cancelRef.current) return;
7600
+ if (result.ok) {
7601
+ setStatus("connected");
7602
+ setLastOkAt(/* @__PURE__ */ new Date());
7603
+ setLastLatencyMs(result.latencyMs ?? Math.round(performance.now() - t0));
7604
+ } else {
7605
+ setStatus("disconnected");
7606
+ setLastFailAt(/* @__PURE__ */ new Date());
7607
+ }
7608
+ } catch {
7609
+ if (!cancelRef.current) {
7610
+ setStatus("disconnected");
7611
+ setLastFailAt(/* @__PURE__ */ new Date());
7612
+ }
7613
+ }
7614
+ }, []);
7615
+ useEffect(() => {
7616
+ cancelRef.current = false;
7617
+ ping();
7618
+ const id = setInterval(ping, pollMs);
7619
+ return () => {
7620
+ cancelRef.current = true;
7621
+ clearInterval(id);
7622
+ };
7623
+ }, [ping, pollMs]);
7624
+ useEffect(() => {
7625
+ const onOnline = () => setBrowserOnline(true);
7626
+ const onOffline = () => setBrowserOnline(false);
7627
+ window.addEventListener("online", onOnline);
7628
+ window.addEventListener("offline", onOffline);
7629
+ return () => {
7630
+ window.removeEventListener("online", onOnline);
7631
+ window.removeEventListener("offline", onOffline);
7632
+ };
7633
+ }, []);
7634
+ useEffect(() => {
7635
+ if (!open) return;
7636
+ const id = setInterval(() => setTick((t) => t + 1), 1e3);
7637
+ return () => clearInterval(id);
7638
+ }, [open]);
7639
+ const placePopover = useCallback(() => {
7640
+ if (!buttonRef.current) return;
7641
+ const r = buttonRef.current.getBoundingClientRect();
7642
+ const popW = 320;
7643
+ const popH = popRef.current?.offsetHeight || 320;
7644
+ const iconCenterX = r.left + r.width / 2;
7645
+ const left = Math.max(8, Math.min(window.innerWidth - popW - 8, iconCenterX - popW / 2));
7646
+ const iconCenterY = r.top + r.height / 2;
7647
+ const popAbove = iconCenterY > window.innerHeight / 2;
7648
+ const top = popAbove ? Math.max(8, r.top - popH - 6) : r.bottom + 6;
7649
+ setPopPos({ left, top });
7650
+ }, []);
7651
+ useEffect(() => {
7652
+ if (!open) return;
7653
+ placePopover();
7654
+ window.addEventListener("resize", placePopover);
7655
+ return () => window.removeEventListener("resize", placePopover);
7656
+ }, [open, placePopover]);
7657
+ useLayoutEffect(() => {
7658
+ if (open && popRef.current) placePopover();
7659
+ }, [open, placePopover]);
7660
+ useEffect(() => {
7661
+ if (!open) return;
7662
+ const onDown = (e) => {
7663
+ const t = e.target;
7664
+ if (popRef.current?.contains(t)) return;
7665
+ if (buttonRef.current?.contains(t)) return;
7666
+ setOpen(false);
7667
+ };
7668
+ const onKey = (e) => {
7669
+ if (e.key === "Escape") setOpen(false);
7670
+ };
7671
+ window.addEventListener("mousedown", onDown);
7672
+ window.addEventListener("keydown", onKey);
7673
+ return () => {
7674
+ window.removeEventListener("mousedown", onDown);
7675
+ window.removeEventListener("keydown", onKey);
7676
+ };
7677
+ }, [open]);
7678
+ const colour = status === "connected" ? "#16a34a" : status === "disconnected" ? "#dc2626" : "#9ca3af";
7679
+ const label = status === "connected" ? "Connected" : status === "disconnected" ? "Disconnected" : "Checking\u2026";
7680
+ const dotClass = status === "connected" ? "bg-green-500" : status === "disconnected" ? "bg-red-500" : "bg-gray-400";
7681
+ const userName = user ? user.name?.trim() || user.email || "\u2014" : "Not signed in";
7682
+ const userRole = user?.role || "";
7683
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
7684
+ /* @__PURE__ */ jsx(
7685
+ "button",
7686
+ {
7687
+ ref: buttonRef,
7688
+ type: "button",
7689
+ onClick: () => setOpen((o) => !o),
7690
+ className: `shrink-0 inline-flex items-center justify-center rounded-md p-2 transition-colors hover:bg-white/20 ${status === "disconnected" && !open ? "animate-pulse" : ""}`,
7691
+ title: `${label}${lastOkAt ? ` \xB7 last ok ${lastOkAt.toLocaleTimeString()}` : ""}`,
7692
+ "aria-label": label,
7693
+ "aria-expanded": open,
7694
+ children: /* @__PURE__ */ jsxs(
7695
+ "svg",
7696
+ {
7697
+ className: "h-5 w-5",
7698
+ viewBox: "0 0 24 24",
7699
+ fill: "none",
7700
+ stroke: colour,
7701
+ strokeWidth: 2,
7702
+ strokeLinecap: "round",
7703
+ strokeLinejoin: "round",
7704
+ children: [
7705
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
7706
+ /* @__PURE__ */ jsx("path", { d: "M2 12h20" }),
7707
+ /* @__PURE__ */ jsx("path", { d: "M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" })
7708
+ ]
7709
+ }
7710
+ )
7711
+ }
7712
+ ),
7713
+ open && createPortal(
7714
+ /* @__PURE__ */ jsxs(
7715
+ "div",
7716
+ {
7717
+ ref: popRef,
7718
+ role: "dialog",
7719
+ "aria-label": "Server connection details",
7720
+ style: { position: "fixed", left: popPos.left, top: popPos.top, width: 320, zIndex: 1e4 },
7721
+ className: "rounded-lg border border-gray-200 bg-white shadow-lg overflow-hidden text-gray-800",
7722
+ children: [
7723
+ /* @__PURE__ */ jsxs("div", { className: "px-4 py-3 border-b border-gray-100 bg-gray-50/60", children: [
7724
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
7725
+ /* @__PURE__ */ jsx("span", { className: `inline-block h-2 w-2 rounded-full ${dotClass} ${status === "disconnected" ? "animate-pulse" : ""}` }),
7726
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold text-gray-900", children: label }),
7727
+ status === "connected" && lastLatencyMs != null && /* @__PURE__ */ jsxs("span", { className: "ml-auto text-[11px] text-gray-500 tabular-nums", children: [
7728
+ lastLatencyMs,
7729
+ " ms"
7730
+ ] })
7731
+ ] }),
7732
+ /* @__PURE__ */ jsxs("p", { className: "mt-1 text-[11px] text-gray-500", children: [
7733
+ status === "connected" && lastOkAt && /* @__PURE__ */ jsxs(Fragment, { children: [
7734
+ "Last poll ",
7735
+ timeAgo3(lastOkAt),
7736
+ "."
7737
+ ] }),
7738
+ status === "disconnected" && lastFailAt && /* @__PURE__ */ jsxs(Fragment, { children: [
7739
+ "Last failure ",
7740
+ timeAgo3(lastFailAt),
7741
+ "."
7742
+ ] }),
7743
+ status === "checking" && /* @__PURE__ */ jsx(Fragment, { children: "First poll in flight\u2026" })
7744
+ ] })
7745
+ ] }),
7746
+ /* @__PURE__ */ jsxs("dl", { className: "px-4 py-3 text-[12px] grid grid-cols-[88px_1fr] gap-x-3 gap-y-1.5", children: [
7747
+ /* @__PURE__ */ jsx("dt", { className: "text-gray-500", children: "Server" }),
7748
+ /* @__PURE__ */ jsx("dd", { className: "font-mono text-gray-900 truncate", title: window.location.origin, children: window.location.host }),
7749
+ /* @__PURE__ */ jsx("dt", { className: "text-gray-500", children: "Protocol" }),
7750
+ /* @__PURE__ */ jsx("dd", { className: "text-gray-900", children: window.location.protocol.replace(":", "").toUpperCase() }),
7751
+ /* @__PURE__ */ jsx("dt", { className: "text-gray-500", children: "Browser" }),
7752
+ /* @__PURE__ */ jsx("dd", { className: browserOnline ? "text-gray-900" : "text-red-600 font-medium", children: browserOnline ? "Online" : "Offline" }),
7753
+ /* @__PURE__ */ jsx("dt", { className: "text-gray-500", children: "Last OK" }),
7754
+ /* @__PURE__ */ jsx("dd", { className: "text-gray-900", children: lastOkAt ? /* @__PURE__ */ jsxs(Fragment, { children: [
7755
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: lastOkAt.toLocaleTimeString() }),
7756
+ /* @__PURE__ */ jsxs("span", { className: "text-gray-400 ml-1", children: [
7757
+ "\xB7 ",
7758
+ timeAgo3(lastOkAt)
7759
+ ] })
7760
+ ] }) : "\u2014" }),
7761
+ lastFailAt && /* @__PURE__ */ jsxs(Fragment, { children: [
7762
+ /* @__PURE__ */ jsx("dt", { className: "text-gray-500", children: "Last fail" }),
7763
+ /* @__PURE__ */ jsxs("dd", { className: "text-gray-900", children: [
7764
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: lastFailAt.toLocaleTimeString() }),
7765
+ /* @__PURE__ */ jsxs("span", { className: "text-gray-400 ml-1", children: [
7766
+ "\xB7 ",
7767
+ timeAgo3(lastFailAt)
7768
+ ] })
7769
+ ] })
7770
+ ] }),
7771
+ /* @__PURE__ */ jsx("dt", { className: "text-gray-500", children: "Latency" }),
7772
+ /* @__PURE__ */ jsx("dd", { className: "text-gray-900 tabular-nums", children: lastLatencyMs != null ? `${lastLatencyMs} ms` : "\u2014" }),
7773
+ /* @__PURE__ */ jsx("dt", { className: "text-gray-500", children: "Poll" }),
7774
+ /* @__PURE__ */ jsxs("dd", { className: "text-gray-900", children: [
7775
+ "every ",
7776
+ Math.round(pollMs / 1e3),
7777
+ " s"
7778
+ ] }),
7779
+ /* @__PURE__ */ jsx("dt", { className: "text-gray-500 mt-2 col-span-2 border-t border-gray-100 pt-2 text-[10px] uppercase tracking-wide font-semibold", children: "Session" }),
7780
+ /* @__PURE__ */ jsx("dt", { className: "text-gray-500", children: "User" }),
7781
+ /* @__PURE__ */ jsx("dd", { className: "text-gray-900 truncate", title: userName, children: userName }),
7782
+ userRole && /* @__PURE__ */ jsxs(Fragment, { children: [
7783
+ /* @__PURE__ */ jsx("dt", { className: "text-gray-500", children: "Role" }),
7784
+ /* @__PURE__ */ jsx("dd", { className: "text-gray-900 truncate", title: userRole, children: userRole })
7785
+ ] }),
7786
+ /* @__PURE__ */ jsx("dt", { className: "text-gray-500 mt-2 col-span-2 border-t border-gray-100 pt-2 text-[10px] uppercase tracking-wide font-semibold", children: "Build" }),
7787
+ /* @__PURE__ */ jsx("dt", { className: "text-gray-500", children: "App" }),
7788
+ /* @__PURE__ */ jsx("dd", { className: "font-mono text-gray-900 text-[11px] truncate", title: version, children: version || "\u2014" }),
7789
+ /* @__PURE__ */ jsx("dt", { className: "text-gray-500", children: "UA" }),
7790
+ /* @__PURE__ */ jsx("dd", { className: "text-gray-900 text-[11px] truncate", title: navigator.userAgent, children: navigator.userAgent.split(" ").slice(-2).join(" ") })
7791
+ ] }),
7792
+ /* @__PURE__ */ jsxs("div", { className: "px-4 py-2 border-t border-gray-100 bg-gray-50/60 flex items-center justify-between", children: [
7793
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-gray-400", children: "Click \u21BB to ping now" }),
7794
+ /* @__PURE__ */ jsxs(
7795
+ "button",
7796
+ {
7797
+ type: "button",
7798
+ onClick: () => {
7799
+ setStatus("checking");
7800
+ ping();
7801
+ },
7802
+ disabled: status === "checking",
7803
+ className: "inline-flex items-center gap-1 text-xs font-medium text-blue-600 hover:text-blue-800 disabled:opacity-50",
7804
+ children: [
7805
+ /* @__PURE__ */ jsx("svg", { className: "h-3 w-3", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M4 4v5h5M20 20v-5h-5M5.5 9A7 7 0 0 1 17 6.5M18.5 15A7 7 0 0 1 7 17.5" }) }),
7806
+ "Refresh"
7807
+ ]
7808
+ }
7809
+ )
7810
+ ] })
7811
+ ]
7812
+ }
7813
+ ),
7814
+ document.body
7815
+ )
7816
+ ] });
7817
+ }
7818
+ function ChangePasswordForm({
7819
+ onSubmit,
7820
+ onSuccess,
7821
+ confirmOnSuccess = true,
7822
+ minLength = 8,
7823
+ submitLabel = "Change Password",
7824
+ doneLabel = "Done"
7825
+ }) {
7826
+ const [oldPassword, setOldPassword] = useState("");
7827
+ const [newPassword, setNewPassword] = useState("");
7828
+ const [confirmPassword, setConfirmPassword] = useState("");
7829
+ const [error, setError] = useState("");
7830
+ const [submitting, setSubmitting] = useState(false);
7831
+ const [success, setSuccess] = useState(false);
7832
+ const handleSubmit = async (e) => {
7833
+ e.preventDefault();
7834
+ if (!oldPassword) {
7835
+ setError("Current password is required.");
7836
+ return;
7837
+ }
7838
+ if (newPassword.length < minLength) {
7839
+ setError(`New password must be at least ${minLength} characters.`);
7840
+ return;
7841
+ }
7842
+ if (newPassword !== confirmPassword) {
7843
+ setError("Passwords do not match.");
7844
+ return;
7845
+ }
7846
+ setError("");
7847
+ setSubmitting(true);
7848
+ try {
7849
+ await onSubmit(oldPassword, newPassword);
7850
+ if (confirmOnSuccess) {
7851
+ setSuccess(true);
7852
+ } else {
7853
+ onSuccess?.();
7854
+ }
7855
+ } catch (err) {
7856
+ setError(err instanceof Error && err.message ? err.message : "Failed to change password.");
7857
+ } finally {
7858
+ setSubmitting(false);
7859
+ }
7860
+ };
7861
+ if (success) {
7862
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
7863
+ /* @__PURE__ */ jsxs("div", { className: "rounded-md bg-green-50 px-4 py-3 flex items-start gap-3", children: [
7864
+ /* @__PURE__ */ jsx("svg", { className: "h-5 w-5 text-green-600 shrink-0 mt-0.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" }) }),
7865
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-green-800", children: "Your password has been changed successfully." })
7866
+ ] }),
7867
+ /* @__PURE__ */ jsx("div", { className: "flex justify-end pt-2 border-t border-gray-200", children: /* @__PURE__ */ jsx(Button_default, { variant: "primary", onClick: () => onSuccess?.(), children: doneLabel }) })
7868
+ ] });
7869
+ }
7870
+ return /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [
7871
+ /* @__PURE__ */ jsx(FormField, { label: "Current Password", required: true, children: /* @__PURE__ */ jsx(Input_default, { type: "password", value: oldPassword, onChange: (e) => setOldPassword(e.target.value), required: true }) }),
7872
+ /* @__PURE__ */ jsx(FormField, { label: "New Password", required: true, children: /* @__PURE__ */ jsx(Input_default, { type: "password", value: newPassword, onChange: (e) => setNewPassword(e.target.value), required: true, minLength }) }),
7873
+ /* @__PURE__ */ jsx(FormField, { label: "Confirm Password", required: true, children: /* @__PURE__ */ jsx(Input_default, { type: "password", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), required: true }) }),
7874
+ error && /* @__PURE__ */ jsx("p", { className: "text-sm text-red-600", children: error }),
7875
+ /* @__PURE__ */ jsx("div", { className: "flex justify-end pt-2 border-t border-gray-200", children: /* @__PURE__ */ jsxs(Button_default, { type: "submit", variant: "primary", loading: submitting, children: [
7876
+ submitting ? "Changing..." : submitLabel,
7877
+ /* @__PURE__ */ jsx("kbd", { className: "rounded border border-blue-400/50 bg-blue-500/30 px-1.5 py-0.5 text-[10px] font-medium", children: CMD_ENTER })
7878
+ ] }) })
7879
+ ] });
7880
+ }
7881
+ var DEFAULT_BUTTON_CLASS = "inline-flex items-center gap-1.5 bg-white text-gray-700 border border-gray-300 px-3 py-1.5 text-sm font-medium rounded-lg hover:bg-gray-50 disabled:opacity-50";
7882
+ var DocumentIcon = /* @__PURE__ */ jsx("svg", { className: "h-4 w-4 text-gray-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" }) });
7883
+ function PdfActionButton({
7884
+ fetchPdf,
7885
+ filename,
7886
+ label = "PDF",
7887
+ className,
7888
+ disabled,
7889
+ onEmail,
7890
+ onPreviewOpened,
7891
+ icon
7892
+ }) {
7893
+ const [open, setOpen] = useState(false);
7894
+ const [loading, setLoading] = useState(false);
7895
+ const btnRef = useRef(null);
7896
+ const [menuPos, setMenuPos] = useState({ left: 0, top: 0 });
7897
+ const { openPage } = useWindowManager();
7898
+ useEffect(() => {
7899
+ if (open && btnRef.current) {
7900
+ const rect = btnRef.current.getBoundingClientRect();
7901
+ setMenuPos({ left: rect.left, top: rect.bottom + 4 });
7902
+ }
7903
+ }, [open]);
7904
+ const resolvePdf = async () => {
7905
+ setLoading(true);
7906
+ try {
7907
+ return await fetchPdf();
7908
+ } finally {
7909
+ setLoading(false);
7910
+ }
7911
+ };
7912
+ const handleDownload = async () => {
7913
+ setOpen(false);
7914
+ const blob = await resolvePdf();
7915
+ if (!blob) return;
7916
+ const url = window.URL.createObjectURL(blob);
7917
+ const a = document.createElement("a");
7918
+ a.href = url;
7919
+ a.download = filename;
7920
+ a.click();
7921
+ window.URL.revokeObjectURL(url);
7922
+ toast_default.success(`${filename} downloaded.`);
7923
+ };
7924
+ const handlePreview = async () => {
7925
+ setOpen(false);
7926
+ const handle = setPdfPreview({ filename, converting: true, convertingMessage: "LOADING PDF" });
7927
+ openPage("/preview");
7928
+ const blob = await resolvePdf();
7929
+ if (!blob) {
7930
+ handle.update({ filename, converting: false, convertingMessage: "Failed to load PDF." });
7931
+ return;
7932
+ }
7933
+ const url = window.URL.createObjectURL(blob);
7934
+ handle.update({ url, filename, ...onEmail ? { onEmail: () => onEmail(blob) } : {} });
7935
+ onPreviewOpened?.(filename);
7936
+ };
7937
+ const handleEmail = async () => {
7938
+ setOpen(false);
7939
+ const blob = await resolvePdf();
7940
+ if (!blob || !onEmail) return;
7941
+ onEmail(blob);
7942
+ };
7943
+ const btnCls = className || DEFAULT_BUTTON_CLASS;
7944
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
7945
+ /* @__PURE__ */ jsxs("button", { ref: btnRef, type: "button", onClick: () => setOpen(!open), disabled: disabled || loading, className: btnCls, "data-menu-toggle": true, children: [
7946
+ icon ?? DocumentIcon,
7947
+ loading ? "Loading..." : label,
7948
+ /* @__PURE__ */ jsx("svg", { className: "h-3 w-3 text-gray-400", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19.5 8.25l-7.5 7.5-7.5-7.5" }) })
7949
+ ] }),
7950
+ open && createPortal(
7951
+ /* @__PURE__ */ jsxs(PopupMenu, { style: menuPos, onClose: () => setOpen(false), minWidth: 180, children: [
7952
+ /* @__PURE__ */ jsxs(PopupMenuItem, { onClick: handlePreview, children: [
7953
+ /* @__PURE__ */ jsxs("svg", { className: "h-4 w-4 text-gray-400", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: [
7954
+ /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" }),
7955
+ /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" })
7956
+ ] }),
7957
+ "Preview"
7958
+ ] }),
7959
+ /* @__PURE__ */ jsxs(PopupMenuItem, { onClick: handleDownload, children: [
7960
+ /* @__PURE__ */ jsx("svg", { className: "h-4 w-4 text-gray-400", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" }) }),
7961
+ "Download"
7962
+ ] }),
7963
+ onEmail && /* @__PURE__ */ jsxs(Fragment, { children: [
7964
+ /* @__PURE__ */ jsx(PopupMenuDivider, {}),
7965
+ /* @__PURE__ */ jsxs(PopupMenuItem, { onClick: handleEmail, children: [
7966
+ /* @__PURE__ */ jsx("svg", { className: "h-4 w-4 text-gray-400", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" }) }),
7967
+ "Send by Email"
7968
+ ] })
7969
+ ] })
7970
+ ] }),
7971
+ document.body
7972
+ )
7973
+ ] });
7974
+ }
7975
+ var DAY_MS = 864e5;
7976
+ function toDayMs(dateStr) {
7977
+ if (!dateStr) return null;
7978
+ const t = new Date(dateStr).getTime();
7979
+ return Number.isFinite(t) ? t : null;
7980
+ }
7981
+ function fmtSliderDate(ms) {
7982
+ return formatDate(new Date(ms).toISOString().slice(0, 10));
7983
+ }
7984
+ var KIND_STYLES = {
7985
+ default: {
7986
+ base: "h-2.5 w-2.5 rounded-full bg-blue-500 border-white",
7987
+ active: "bg-blue-700",
7988
+ placeholder: "h-2.5 w-2.5 rounded-full bg-white border-gray-300 hover:border-gray-500"
7989
+ },
7990
+ dfm: {
7991
+ base: "h-2.5 w-2.5 rounded-sm bg-amber-500 border-white",
7992
+ active: "bg-amber-600",
7993
+ placeholder: "h-2.5 w-2.5 rounded-sm bg-white border-amber-300 hover:border-amber-500"
7994
+ },
7995
+ shipment: {
7996
+ base: "h-2.5 w-2.5 rotate-45 bg-emerald-500 border-white",
7997
+ active: "bg-emerald-600",
7998
+ placeholder: "h-2.5 w-2.5 rotate-45 bg-white border-emerald-300 hover:border-emerald-500"
7999
+ },
8000
+ testing: {
8001
+ base: "h-3 w-3 rounded-full bg-violet-500 border-white",
8002
+ active: "bg-violet-700",
8003
+ placeholder: "h-3 w-3 rounded-full bg-white border-violet-300 hover:border-violet-500",
8004
+ inner: /* @__PURE__ */ jsx("span", { className: "block h-1 w-1 rounded-full bg-white" })
8005
+ },
8006
+ completion: {
8007
+ base: "h-3.5 w-3.5 rounded-full bg-green-600 border-white",
8008
+ active: "bg-green-700",
8009
+ placeholder: "h-3.5 w-3.5 rounded-full bg-white border-green-300 hover:border-green-500",
8010
+ inner: /* @__PURE__ */ jsx("svg", { className: "h-2 w-2 text-white", fill: "none", stroke: "currentColor", strokeWidth: "3", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }) })
8011
+ }
8012
+ };
8013
+ function MilestoneTimeline({ title, milestones, summary, endDate, phaseLabels }) {
8014
+ const [hoveredKey, setHoveredKey] = useState(null);
8015
+ const [today] = useState(() => Date.now());
8016
+ const realDates = milestones.map((m) => toDayMs(m.date ?? null)).filter((t) => t !== null);
8017
+ const explicitEnd = toDayMs(endDate ?? null);
8018
+ let startMs;
8019
+ let endMs;
8020
+ if (realDates.length === 0) {
8021
+ endMs = explicitEnd ?? today;
8022
+ startMs = endMs - 30 * DAY_MS;
8023
+ } else {
8024
+ startMs = Math.min(...realDates);
8025
+ endMs = explicitEnd ?? Math.max(...realDates);
8026
+ if (endMs - startMs < DAY_MS) endMs = startMs + DAY_MS;
8027
+ }
8028
+ const span = Math.max(endMs - startMs, DAY_MS);
8029
+ const totalLeadDays = realDates.length > 0 ? Math.max(0, Math.round((endMs - startMs) / DAY_MS)) : 0;
8030
+ const autoSummary = totalLeadDays > 0 ? `${totalLeadDays.toLocaleString()} day${totalLeadDays === 1 ? "" : "s"} lead time` : void 0;
8031
+ const renderSummary = summary ?? autoSummary;
8032
+ const placed = milestones.map((m, i) => {
8033
+ const ms = toDayMs(m.date ?? null);
8034
+ if (ms !== null) return { ...m, ms, isPlaceholder: false };
8035
+ let prevMs = null;
8036
+ for (let j = i - 1; j >= 0; j--) {
8037
+ const v = toDayMs(milestones[j].date ?? null);
8038
+ if (v !== null) {
8039
+ prevMs = v;
8040
+ break;
8041
+ }
8042
+ }
8043
+ let nextMs = null;
8044
+ for (let j = i + 1; j < milestones.length; j++) {
8045
+ const v = toDayMs(milestones[j].date ?? null);
8046
+ if (v !== null) {
8047
+ nextMs = v;
8048
+ break;
8049
+ }
8050
+ }
8051
+ let fallback;
8052
+ if (prevMs !== null && nextMs !== null) fallback = (prevMs + nextMs) / 2;
8053
+ else if (prevMs !== null) fallback = Math.min(prevMs + DAY_MS, endMs);
8054
+ else if (nextMs !== null) fallback = Math.max(nextMs - DAY_MS, startMs);
8055
+ else fallback = startMs + span * (i + 1) / (milestones.length + 1);
8056
+ return { ...m, ms: fallback, isPlaceholder: true };
8057
+ });
8058
+ const todayMs = today;
8059
+ const phases = (() => {
8060
+ const groups = {};
8061
+ for (const m of placed) {
8062
+ if (!m.phase) continue;
8063
+ (groups[m.phase] = groups[m.phase] || []).push(m);
8064
+ }
8065
+ return Object.entries(groups).filter(([, ms]) => ms.length >= 2).map(([key, ms]) => ({
8066
+ key,
8067
+ label: phaseLabels?.[key] ?? key,
8068
+ minMs: Math.min(...ms.map((m) => m.ms)),
8069
+ maxMs: Math.max(...ms.map((m) => m.ms))
8070
+ }));
8071
+ })();
8072
+ return /* @__PURE__ */ jsxs("div", { className: "shrink-0", children: [
8073
+ /* @__PURE__ */ jsx("div", { className: "flex items-end justify-between mb-1 gap-2", children: /* @__PURE__ */ jsxs("h4", { className: "text-xs font-semibold text-gray-500 uppercase tracking-wide", children: [
8074
+ title,
8075
+ renderSummary && /* @__PURE__ */ jsx("span", { className: "ml-2 text-gray-400 normal-case", children: renderSummary })
8076
+ ] }) }),
8077
+ /* @__PURE__ */ jsx("div", { className: "border border-gray-200 rounded-lg bg-gray-50", children: /* @__PURE__ */ jsx("div", { className: "select-none", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-3 py-3", children: [
8078
+ placed.length > 0 && (() => {
8079
+ const m = placed[0];
8080
+ return /* @__PURE__ */ jsxs("div", { className: `text-[10px] leading-tight whitespace-nowrap text-right shrink-0 pointer-events-none ${m.isPlaceholder ? "text-gray-400 italic" : "text-gray-700"}`, children: [
8081
+ /* @__PURE__ */ jsx("div", { className: "font-medium", children: m.label }),
8082
+ !m.isPlaceholder && /* @__PURE__ */ jsx("div", { className: "text-[9px] text-gray-400 font-normal", children: fmtSliderDate(m.ms) })
8083
+ ] });
8084
+ })(),
8085
+ /* @__PURE__ */ jsxs("div", { className: "relative flex-1 flex flex-col justify-center", children: [
8086
+ /* @__PURE__ */ jsx("div", { className: "relative h-7 mb-1", children: placed.map((m, i) => {
8087
+ if (i === 0 || i === placed.length - 1) return null;
8088
+ if (i % 2 !== 0) return null;
8089
+ const p = (m.ms - startMs) / span * 100;
8090
+ if (p < -0.5 || p > 100.5) return null;
8091
+ const isHovered = m.key === hoveredKey;
8092
+ return /* @__PURE__ */ jsxs(
8093
+ "div",
8094
+ {
8095
+ className: `absolute bottom-0 -translate-x-1/2 text-center text-[10px] leading-tight whitespace-nowrap pointer-events-none ${m.isPlaceholder ? "text-gray-400 italic" : isHovered ? "text-blue-700 font-semibold" : "text-gray-700"}`,
8096
+ style: { left: `${Math.max(0, Math.min(100, p))}%` },
8097
+ children: [
8098
+ /* @__PURE__ */ jsx("div", { children: m.label }),
8099
+ !m.isPlaceholder && /* @__PURE__ */ jsx("div", { className: "text-[9px] text-gray-400 font-normal", children: fmtSliderDate(m.ms) })
8100
+ ]
8101
+ },
8102
+ `above-${m.key}`
8103
+ );
8104
+ }) }),
8105
+ /* @__PURE__ */ jsxs("div", { className: "relative h-2 bg-gray-200 rounded-full", children: [
8106
+ realDates.length > 0 && (() => {
8107
+ const lastReal = Math.min(Math.max(...realDates), todayMs);
8108
+ const fillPct = (lastReal - startMs) / span * 100;
8109
+ return /* @__PURE__ */ jsx(
8110
+ "div",
8111
+ {
8112
+ className: "absolute top-0 left-0 h-full bg-blue-300 rounded-full pointer-events-none",
8113
+ style: { width: `${Math.max(0, Math.min(100, fillPct))}%` }
8114
+ }
8115
+ );
8116
+ })(),
8117
+ todayMs > startMs && todayMs < endMs && /* @__PURE__ */ jsx(
8118
+ "div",
8119
+ {
8120
+ className: "absolute -top-1 h-4 w-px bg-gray-400 pointer-events-none",
8121
+ style: { left: `${(todayMs - startMs) / span * 100}%` },
8122
+ title: `Today \u2014 ${fmtSliderDate(todayMs)}`
8123
+ }
8124
+ ),
8125
+ placed.map((m) => {
8126
+ const p = (m.ms - startMs) / span * 100;
8127
+ if (p < -0.5 || p > 100.5) return null;
8128
+ const isHovered = m.key === hoveredKey;
8129
+ const style = KIND_STYLES[m.kind ?? "default"];
8130
+ const dotClass = m.isPlaceholder ? style.placeholder : isHovered ? `${style.base} ${style.active}` : style.base;
8131
+ const Tag = m.onClick ? "button" : "div";
8132
+ return /* @__PURE__ */ jsx(
8133
+ Tag,
8134
+ {
8135
+ type: m.onClick ? "button" : void 0,
8136
+ onMouseEnter: () => setHoveredKey(m.key),
8137
+ onMouseLeave: () => setHoveredKey((prev) => prev === m.key ? null : prev),
8138
+ onClick: m.onClick,
8139
+ className: `absolute top-1/2 -translate-x-1/2 -translate-y-1/2 border-2 shadow flex items-center justify-center ${dotClass} ${m.onClick ? "cursor-pointer" : "cursor-default"}`,
8140
+ style: { left: `${Math.max(0, Math.min(100, p))}%` },
8141
+ "aria-label": `${m.label}${m.isPlaceholder ? " (not reached yet)" : ` on ${fmtSliderDate(m.ms)}`}`,
8142
+ title: `${m.label}${m.isPlaceholder ? " \u2014 not reached yet" : ` \u2022 ${fmtSliderDate(m.ms)}`}${m.detail ? ` \u2014 ${m.detail}` : ""}`,
8143
+ children: !m.isPlaceholder && style.inner
8144
+ },
8145
+ m.key
8146
+ );
8147
+ })
8148
+ ] }),
8149
+ /* @__PURE__ */ jsx("div", { className: "relative h-7 mt-1", children: placed.map((m, i) => {
8150
+ if (i === 0 || i === placed.length - 1) return null;
8151
+ if (i % 2 === 0) return null;
8152
+ const p = (m.ms - startMs) / span * 100;
8153
+ if (p < -0.5 || p > 100.5) return null;
8154
+ const isHovered = m.key === hoveredKey;
8155
+ return /* @__PURE__ */ jsxs(
8156
+ "div",
8157
+ {
8158
+ className: `absolute top-0 -translate-x-1/2 text-center text-[10px] leading-tight whitespace-nowrap pointer-events-none ${m.isPlaceholder ? "text-gray-400 italic" : isHovered ? "text-blue-700 font-semibold" : "text-gray-700"}`,
8159
+ style: { left: `${Math.max(0, Math.min(100, p))}%` },
8160
+ children: [
8161
+ /* @__PURE__ */ jsx("div", { children: m.label }),
8162
+ !m.isPlaceholder && /* @__PURE__ */ jsx("div", { className: "text-[9px] text-gray-400 font-normal", children: fmtSliderDate(m.ms) })
8163
+ ]
8164
+ },
8165
+ `below-${m.key}`
8166
+ );
8167
+ }) }),
8168
+ phases.length > 0 && /* @__PURE__ */ jsx("div", { className: "relative h-5 mt-2", children: phases.map((ph) => {
8169
+ const leftPct = Math.max(0, (ph.minMs - startMs) / span * 100);
8170
+ const rightPct = Math.min(100, (ph.maxMs - startMs) / span * 100);
8171
+ const widthPct = Math.max(rightPct - leftPct, 0.5);
8172
+ const centerPct = leftPct + widthPct / 2;
8173
+ return /* @__PURE__ */ jsxs("div", { children: [
8174
+ /* @__PURE__ */ jsx(
8175
+ "div",
8176
+ {
8177
+ className: "absolute border-l border-r border-b border-slate-300 rounded-b-md pointer-events-none",
8178
+ style: { left: `${leftPct}%`, width: `${widthPct}%`, top: 0, height: 8 }
8179
+ }
8180
+ ),
8181
+ /* @__PURE__ */ jsxs(
8182
+ "div",
8183
+ {
8184
+ className: "absolute -translate-x-1/2 text-[9px] italic text-slate-500 whitespace-nowrap pointer-events-none",
8185
+ style: { left: `${centerPct}%`, top: 9 },
8186
+ children: [
8187
+ ph.label,
8188
+ " ",
8189
+ /* @__PURE__ */ jsx("span", { className: "text-slate-400 not-italic", children: "\xB7 parallel" })
8190
+ ]
8191
+ }
8192
+ )
8193
+ ] }, `phase-${ph.key}`);
8194
+ }) })
8195
+ ] }),
8196
+ placed.length > 1 && (() => {
8197
+ const m = placed[placed.length - 1];
8198
+ return /* @__PURE__ */ jsxs("div", { className: `text-[10px] leading-tight whitespace-nowrap text-left shrink-0 pointer-events-none ${m.isPlaceholder ? "text-gray-400 italic" : "text-gray-700"}`, children: [
8199
+ /* @__PURE__ */ jsx("div", { className: "font-medium", children: m.label }),
8200
+ !m.isPlaceholder && /* @__PURE__ */ jsx("div", { className: "text-[9px] text-gray-400 font-normal", children: fmtSliderDate(m.ms) })
8201
+ ] });
8202
+ })()
8203
+ ] }) }) })
8204
+ ] });
8205
+ }
6844
8206
  function Sparkline({
6845
8207
  data,
6846
8208
  width = 120,
@@ -7561,6 +8923,6 @@ function useEditHotkey(callback) {
7561
8923
  }, [callback, isActive]);
7562
8924
  }
7563
8925
 
7564
- export { ALT, ALT_SHIFT_D, ALT_SHIFT_E, ALT_SHIFT_N, Accordion, AuthScreen, Avatar, AvatarGroup, Banner, BarChart, BehaviorPanel, Button_default as Button, CMD_A, CMD_DOT, CMD_ENTER, CMD_K, CMD_S, Card, ChatTemplate, Checkbox_default as Checkbox, CheckoutTemplate, ColoredBadge, Customization, DEV_BANNER_TEXT, DashboardTemplate, DataTablePage, Desktop, DesktopHostProvider, DevIndicator, DonutChart, ENTER, EmailTemplate, EmptyState, EntityList, ErrorPage, FilterBar, FormField, FormLayoutPage, GalleryTemplate, GlobalSearch, HelpCenter, INPUT_BASE, Input_default as Input, Kanban, Label, Layout, ListFooter, LoadingSpinner2 as LoadingSpinner, MOD, Markdown, NotificationBell, PageHeader, Pagination, Radio_default as Radio, ResizableTable, SHIFT, SearchableSelect, Select_default as Select, ShellEntityFetcherProvider, ShortcutHelp, SidebarGroupLabel, SidebarNavItem, SoundsPanel, Sparkline, StartMenu, StatCard, StatusBadge, StatusBadgeProvider, SystemPreferences, Tabs, Textarea_default as Textarea, Tooltip, TopNav, WidgetManager, applyDevTitle, createWindowRegistry, formatDate, inputClasses, isDevEnv, isMac, useClickOutside, useColumnConfig, useDesktopHost, useEditHotkey, useFilters, useInfiniteScroll, useNewHotkey, useShellEntityFetcher, useSort, useTableNav };
8926
+ export { ALT, ALT_SHIFT_D, ALT_SHIFT_E, ALT_SHIFT_N, Accordion, AuthScreen, Avatar, AvatarGroup, Banner, BarChart, BehaviorPanel, BulkImportGrid, Button_default as Button, CMD_A, CMD_DOT, CMD_ENTER, CMD_K, CMD_S, Card, ChangePasswordForm, ChatTemplate, Checkbox_default as Checkbox, CheckoutTemplate, ColoredBadge, ContainerFillChart, Customization, DEV_BANNER_TEXT, DashboardTemplate, DataTablePage, Desktop, DesktopHostProvider, DevIndicator, DonutChart, ENTER, EmailTemplate, EmptyState, EntityList, ErrorPage, FilterBar, FormField, FormLayoutPage, GalleryTemplate, GlobalSearch, HelpCenter, INPUT_BASE, Input_default as Input, Kanban, Label, Layout, ListFooter, LoadingSpinner2 as LoadingSpinner, MOD, Markdown, MilestoneTimeline, NotificationBell, PageHeader, Pagination, PdfActionButton, Radio_default as Radio, ResizableTable, SHIFT, SearchableSelect, Select_default as Select, ServerStatusIndicator, ShellEntityFetcherProvider, ShortcutHelp, SidebarGroupLabel, SidebarNavItem, SoundsPanel, Sparkline, StartMenu, StatCard, StatusBadge, StatusBadgeProvider, SystemPreferences, Tabs, Textarea_default as Textarea, Tooltip, TopNav, WidgetManager, applyDevTitle, createWindowRegistry, findDuplicateKeys, formatDate, inputClasses, isDevEnv, isMac, mergeBulkItems, useClickOutside, useColumnConfig, useDesktopHost, useEditHotkey, useFilters, useInfiniteScroll, useNewHotkey, useShellEntityFetcher, useSort, useTableNav };
7565
8927
  //# sourceMappingURL=index.js.map
7566
8928
  //# sourceMappingURL=index.js.map