@campfire-interactive/volume-intelligence-ui 0.1.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 (3) hide show
  1. package/dist/index.d.ts +401 -0
  2. package/dist/index.js +1716 -0
  3. package/package.json +43 -0
package/dist/index.js ADDED
@@ -0,0 +1,1716 @@
1
+ // src/VolumeImport.tsx
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { AlertTriangle, FileUp, Loader2, RotateCcw } from "lucide-react";
4
+
5
+ // #style-inject:#style-inject
6
+ function styleInject(css, { insertAt } = {}) {
7
+ if (!css || typeof document === "undefined") return;
8
+ const head = document.head || document.getElementsByTagName("head")[0];
9
+ const style = document.createElement("style");
10
+ style.type = "text/css";
11
+ if (insertAt === "top") {
12
+ if (head.firstChild) {
13
+ head.insertBefore(style, head.firstChild);
14
+ } else {
15
+ head.appendChild(style);
16
+ }
17
+ } else {
18
+ head.appendChild(style);
19
+ }
20
+ if (style.styleSheet) {
21
+ style.styleSheet.cssText = css;
22
+ } else {
23
+ style.appendChild(document.createTextNode(css));
24
+ }
25
+ }
26
+
27
+ // src/styles.css
28
+ styleInject(".cfi-volume-import {\n font-family: var(--cfi-font-family, system-ui, sans-serif);\n font-size: var(--cfi-font-size-base, 14px);\n color: var(--cfi-color-gray-900, #111827);\n display: flex;\n flex-direction: column;\n gap: var(--cfi-spacing-md, 12px);\n max-width: 640px;\n}\n.cfi-volume-import .cfi-vi-field {\n display: flex;\n flex-direction: column;\n gap: var(--cfi-spacing-xs, 4px);\n}\n.cfi-volume-import .cfi-vi-row {\n display: flex;\n gap: var(--cfi-spacing-md, 12px);\n flex-wrap: wrap;\n}\n.cfi-volume-import .cfi-vi-row .cfi-vi-field {\n flex: 1 1 8rem;\n}\n.cfi-volume-import .cfi-vi-label {\n font-size: var(--cfi-font-size-sm, 12px);\n font-weight: 600;\n color: var(--cfi-color-gray-700, #374151);\n}\n.cfi-volume-import .cfi-vi-select,\n.cfi-volume-import .cfi-vi-file {\n padding: 6px 8px;\n border: 1px solid var(--cfi-color-gray-300, #d1d5db);\n border-radius: var(--cfi-radius-md, 6px);\n background: #fff;\n font: inherit;\n color: inherit;\n}\n.cfi-volume-import .cfi-vi-select:focus,\n.cfi-volume-import .cfi-vi-file:focus {\n outline: 2px solid var(--cfi-color-primary-600, #2563eb);\n outline-offset: 1px;\n}\n.cfi-volume-import .cfi-vi-button {\n display: inline-flex;\n align-items: center;\n gap: var(--cfi-spacing-xs, 6px);\n align-self: flex-start;\n padding: 8px 14px;\n border: none;\n border-radius: var(--cfi-radius-md, 6px);\n background: var(--cfi-color-primary-600, #2563eb);\n color: #fff;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n}\n.cfi-volume-import .cfi-vi-button:hover:not(:disabled) {\n background: var(--cfi-color-primary-700, #1d4ed8);\n}\n.cfi-volume-import .cfi-vi-button:disabled {\n background: var(--cfi-color-gray-300, #d1d5db);\n color: var(--cfi-color-gray-500, #6b7280);\n cursor: not-allowed;\n}\n.cfi-volume-import .cfi-vi-loading,\n.cfi-volume-import .cfi-vi-alert {\n display: flex;\n align-items: center;\n gap: var(--cfi-spacing-xs, 6px);\n padding: 8px 10px;\n border-radius: var(--cfi-radius-md, 6px);\n font-size: var(--cfi-font-size-sm, 13px);\n}\n.cfi-volume-import .cfi-vi-alert--error {\n background: var(--cfi-color-gray-100, #fef2f2);\n color: var(--cfi-color-error, #b91c1c);\n}\n.cfi-volume-import .cfi-vi-alert--warn {\n background: #fffbeb;\n color: #92400e;\n align-items: flex-start;\n}\n.cfi-volume-import .cfi-vi-blockers {\n margin: 0;\n padding-left: 1.1rem;\n}\n.cfi-volume-import .cfi-vi-status {\n border: 1px solid var(--cfi-color-gray-200, #e5e7eb);\n border-radius: var(--cfi-radius-md, 6px);\n padding: var(--cfi-spacing-md, 12px);\n display: flex;\n flex-direction: column;\n gap: var(--cfi-spacing-sm, 8px);\n}\n.cfi-volume-import .cfi-vi-status-head {\n display: flex;\n align-items: center;\n gap: var(--cfi-spacing-sm, 8px);\n}\n.cfi-volume-import .cfi-vi-filename {\n font-weight: 600;\n}\n.cfi-volume-import .cfi-vi-badge {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 2px 8px;\n border-radius: 999px;\n font-size: var(--cfi-font-size-xs, 11px);\n text-transform: capitalize;\n background: var(--cfi-color-gray-100, #f3f4f6);\n color: var(--cfi-color-gray-700, #374151);\n}\n.cfi-volume-import .cfi-vi-badge--done {\n background: #dcfce7;\n color: #166534;\n}\n.cfi-volume-import .cfi-vi-badge--done_with_errors {\n background: #fef9c3;\n color: #854d0e;\n}\n.cfi-volume-import .cfi-vi-badge--failed {\n background: #fee2e2;\n color: #991b1b;\n}\n.cfi-volume-import .cfi-vi-badge--processing,\n.cfi-volume-import .cfi-vi-badge--pending,\n.cfi-volume-import .cfi-vi-badge--awaiting_calendar {\n background: #dbeafe;\n color: #1e40af;\n}\n.cfi-volume-import .cfi-vi-link {\n background: none;\n border: none;\n padding: 0;\n display: inline-flex;\n align-items: center;\n gap: 4px;\n color: var(--cfi-color-primary-600, #2563eb);\n font: inherit;\n cursor: pointer;\n}\n.cfi-volume-import .cfi-vi-stats {\n display: flex;\n gap: var(--cfi-spacing-md, 12px);\n color: var(--cfi-color-gray-600, #4b5563);\n font-size: var(--cfi-font-size-sm, 13px);\n}\n.cfi-volume-import .cfi-vi-errors {\n width: 100%;\n border-collapse: collapse;\n font-size: var(--cfi-font-size-sm, 13px);\n}\n.cfi-volume-import .cfi-vi-errors th,\n.cfi-volume-import .cfi-vi-errors td {\n text-align: left;\n padding: 4px 8px;\n border-bottom: 1px solid var(--cfi-color-gray-200, #e5e7eb);\n vertical-align: top;\n}\n.cfi-volume-import .cfi-vi-spin {\n animation: cfi-vi-spin 0.8s linear infinite;\n}\n@keyframes cfi-vi-spin {\n to {\n transform: rotate(360deg);\n }\n}\n.cfi-vi {\n font-family: var(--cfi-font-family, system-ui, sans-serif);\n font-size: var(--cfi-font-size-base, 14px);\n color: var(--cfi-color-gray-900, #111827);\n display: flex;\n flex-direction: column;\n gap: var(--cfi-spacing-md, 12px);\n}\n.cfi-vi *,\n.cfi-vi *::before,\n.cfi-vi *::after {\n box-sizing: border-box;\n}\n.cfi-vi .cfi-vi-row {\n display: flex;\n align-items: center;\n gap: var(--cfi-spacing-sm, 8px);\n}\n.cfi-vi .cfi-vi-row--between {\n justify-content: space-between;\n}\n.cfi-vi .cfi-vi-spacer {\n flex: 1;\n}\n.cfi-vi .cfi-vi-muted {\n color: var(--cfi-color-gray-600, #4b5563);\n}\n.cfi-vi .cfi-vi-error {\n color: var(--cfi-color-error, #b91c1c);\n font-size: var(--cfi-font-size-sm, 13px);\n}\n.cfi-vi .cfi-vi-mono {\n font-family:\n ui-monospace,\n SFMono-Regular,\n Menlo,\n monospace;\n font-size: var(--cfi-font-size-xs, 11px);\n}\n.cfi-vi .cfi-vi-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n border-radius: var(--cfi-radius-md, 6px);\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n border: 1px solid transparent;\n white-space: nowrap;\n}\n.cfi-vi .cfi-vi-btn--sm {\n padding: 4px 10px;\n font-size: var(--cfi-font-size-sm, 13px);\n}\n.cfi-vi .cfi-vi-btn--md {\n padding: 8px 14px;\n}\n.cfi-vi .cfi-vi-btn:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.cfi-vi .cfi-vi-btn--primary {\n background: var(--cfi-color-primary-600, #2563eb);\n color: #fff;\n}\n.cfi-vi .cfi-vi-btn--primary:hover:not(:disabled) {\n background: var(--cfi-color-primary-700, #1d4ed8);\n}\n.cfi-vi .cfi-vi-btn--outline {\n background: #fff;\n border-color: var(--cfi-color-gray-300, #d1d5db);\n color: var(--cfi-color-gray-900, #111827);\n}\n.cfi-vi .cfi-vi-btn--outline:hover:not(:disabled) {\n background: var(--cfi-color-gray-50, #f9fafb);\n}\n.cfi-vi .cfi-vi-btn--ghost {\n background: transparent;\n color: var(--cfi-color-gray-700, #374151);\n}\n.cfi-vi .cfi-vi-btn--ghost:hover:not(:disabled) {\n background: var(--cfi-color-gray-100, #f3f4f6);\n}\n.cfi-vi .cfi-vi-btn--destructive {\n background: var(--cfi-color-error, #dc2626);\n color: #fff;\n}\n.cfi-vi .cfi-vi-btn--destructive:hover:not(:disabled) {\n filter: brightness(0.92);\n}\n.cfi-vi .cfi-vi-icon-btn {\n background: none;\n border: none;\n cursor: pointer;\n padding: 4px;\n color: var(--cfi-color-gray-500, #6b7280);\n border-radius: var(--cfi-radius-sm, 4px);\n display: inline-flex;\n}\n.cfi-vi .cfi-vi-icon-btn:hover {\n background: var(--cfi-color-gray-100, #f3f4f6);\n}\n.cfi-vi .cfi-vi-tag {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 1px 8px;\n border-radius: 999px;\n font-size: var(--cfi-font-size-xs, 11px);\n font-weight: 600;\n}\n.cfi-vi .cfi-vi-tag--default {\n background: var(--cfi-color-gray-100, #f3f4f6);\n color: var(--cfi-color-gray-700, #374151);\n}\n.cfi-vi .cfi-vi-tag--outline {\n background: transparent;\n border: 1px solid var(--cfi-color-gray-300, #d1d5db);\n color: var(--cfi-color-gray-700, #374151);\n}\n.cfi-vi .cfi-vi-tag--secondary {\n background: var(--cfi-color-gray-200, #e5e7eb);\n color: var(--cfi-color-gray-700, #374151);\n}\n.cfi-vi .cfi-vi-tag--success {\n background: #16a34a;\n color: #fff;\n}\n.cfi-vi .cfi-vi-tag--purple {\n background: var(--cfi-color-violet-600, #7c3aed);\n color: #fff;\n}\n.cfi-vi .cfi-vi-card {\n border: 1px solid var(--cfi-color-gray-200, #e5e7eb);\n border-radius: var(--cfi-radius-lg, 8px);\n background: #fff;\n}\n.cfi-vi .cfi-vi-card--active {\n border-color: #86efac;\n background: #f0fdf4;\n}\n.cfi-vi .cfi-vi-card--hover:hover {\n border-color: var(--cfi-color-primary-600, #2563eb);\n}\n.cfi-vi .cfi-vi-card-head {\n padding: var(--cfi-spacing-md, 12px);\n padding-bottom: var(--cfi-spacing-sm, 8px);\n}\n.cfi-vi .cfi-vi-card-body {\n padding: 0 var(--cfi-spacing-md, 12px) var(--cfi-spacing-md, 12px);\n}\n.cfi-vi .cfi-vi-card-title {\n font-size: var(--cfi-font-size-base, 15px);\n font-weight: 600;\n display: inline-flex;\n align-items: center;\n gap: 8px;\n}\n.cfi-vi .cfi-vi-link {\n background: none;\n border: none;\n padding: 0;\n color: inherit;\n font: inherit;\n cursor: pointer;\n text-align: left;\n}\n.cfi-vi .cfi-vi-link:hover {\n text-decoration: underline;\n}\n.cfi-vi .cfi-vi-field {\n display: flex;\n flex-direction: column;\n gap: var(--cfi-spacing-xs, 4px);\n}\n.cfi-vi .cfi-vi-label {\n font-size: var(--cfi-font-size-sm, 12px);\n font-weight: 600;\n color: var(--cfi-color-gray-700, #374151);\n}\n.cfi-vi .cfi-vi-hint {\n font-size: var(--cfi-font-size-xs, 11px);\n color: var(--cfi-color-gray-500, #6b7280);\n margin: 2px 0 0;\n}\n.cfi-vi .cfi-vi-input,\n.cfi-vi select.cfi-vi-input {\n padding: 6px 8px;\n border: 1px solid var(--cfi-color-gray-300, #d1d5db);\n border-radius: var(--cfi-radius-md, 6px);\n background: #fff;\n font: inherit;\n color: inherit;\n width: 100%;\n}\n.cfi-vi .cfi-vi-input:focus {\n outline: 2px solid var(--cfi-color-primary-600, #2563eb);\n outline-offset: 1px;\n}\n.cfi-vi-modal-overlay {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.4);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 50;\n padding: 16px;\n}\n.cfi-vi-modal {\n background: #fff;\n border-radius: var(--cfi-radius-lg, 8px);\n width: 100%;\n max-width: 32rem;\n max-height: 90vh;\n overflow: auto;\n box-shadow: var(--cfi-shadow-lg, 0 10px 30px rgba(0,0,0,0.2));\n font-family: var(--cfi-font-family, system-ui, sans-serif);\n color: var(--cfi-color-gray-900, #111827);\n display: flex;\n flex-direction: column;\n}\n.cfi-vi-modal-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: var(--cfi-spacing-md, 14px);\n border-bottom: 1px solid var(--cfi-color-gray-200, #e5e7eb);\n}\n.cfi-vi-modal-title {\n font-size: var(--cfi-font-size-lg, 16px);\n font-weight: 600;\n margin: 0;\n}\n.cfi-vi-modal-body {\n padding: var(--cfi-spacing-md, 14px);\n display: flex;\n flex-direction: column;\n gap: var(--cfi-spacing-md, 12px);\n}\n.cfi-vi-modal-foot {\n display: flex;\n justify-content: flex-end;\n gap: var(--cfi-spacing-sm, 8px);\n padding: var(--cfi-spacing-md, 14px);\n border-top: 1px solid var(--cfi-color-gray-200, #e5e7eb);\n}\n.cfi-vi-icon-btn {\n background: none;\n border: none;\n cursor: pointer;\n padding: 4px;\n color: var(--cfi-color-gray-500, #6b7280);\n border-radius: 4px;\n display: inline-flex;\n}\n.cfi-vi .cfi-vi-spinner {\n display: inline-block;\n border: 2px solid var(--cfi-color-gray-300, #d1d5db);\n border-top-color: var(--cfi-color-primary-600, #2563eb);\n border-radius: 50%;\n animation: cfi-vi-spin 0.8s linear infinite;\n}\n.cfi-vi .cfi-vi-list {\n display: flex;\n flex-direction: column;\n gap: var(--cfi-spacing-sm, 10px);\n}\n.cfi-vi .cfi-vi-list-head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n}\n.cfi-vi .cfi-vi-h1 {\n font-size: var(--cfi-font-size-lg, 18px);\n font-weight: 600;\n margin: 0;\n}\n.cfi-vi .cfi-vi-active-line {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: var(--cfi-font-size-sm, 13px);\n color: var(--cfi-color-gray-600, #4b5563);\n margin-top: 2px;\n}\n.cfi-vi .cfi-vi-active-name {\n font-weight: 600;\n color: #15803d;\n}\n.cfi-vi .cfi-vi-active-extras {\n margin-top: var(--cfi-spacing-md, 16px);\n padding-top: var(--cfi-spacing-md, 16px);\n border-top: 1px solid var(--cfi-color-gray-200, #e5e7eb);\n display: flex;\n flex-direction: column;\n gap: var(--cfi-spacing-md, 16px);\n}\n.cfi-vi-mapper {\n display: grid;\n grid-template-columns: 1fr;\n gap: var(--cfi-spacing-lg, 24px);\n align-items: start;\n}\n@media (min-width: 1024px) {\n .cfi-vi-mapper {\n grid-template-columns: 1fr 1fr;\n }\n}\n.cfi-vi-mapper .cfi-vi-mapper-col {\n display: flex;\n flex-direction: column;\n gap: var(--cfi-spacing-lg, 24px);\n min-width: 0;\n}\n.cfi-vi-mapper .cfi-vi-mapper-section {\n display: flex;\n flex-direction: column;\n gap: var(--cfi-spacing-sm, 8px);\n}\n.cfi-vi-mapper .cfi-vi-mapper-section-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n.cfi-vi-mapper .cfi-vi-mapper-h3 {\n font-size: var(--cfi-font-size-xs, 11px);\n font-weight: 700;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n color: var(--cfi-color-gray-500, #6b7280);\n margin: 0;\n}\n.cfi-vi-mapper .cfi-vi-mapper-note {\n font-size: var(--cfi-font-size-xs, 11px);\n color: var(--cfi-color-gray-500, #6b7280);\n}\n.cfi-vi-mapper .cfi-vi-mapper-box {\n border: 1px solid var(--cfi-color-gray-100, #f3f4f6);\n border-radius: var(--cfi-radius-lg, 8px);\n background: #fff;\n padding: var(--cfi-spacing-md, 14px);\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n.cfi-vi-mapper .cfi-vi-mapper-err {\n font-size: var(--cfi-font-size-xs, 11px);\n color: var(--cfi-color-error, #b91c1c);\n margin: 2px 0 0;\n}\n.cfi-vi-mapper .cfi-vi-mapper-save {\n display: flex;\n align-items: center;\n gap: 12px;\n padding-top: 4px;\n}\n.cfi-vi-mapper .cfi-vi-mapper-dirty {\n font-size: var(--cfi-font-size-xs, 11px);\n color: #b45309;\n}\n.cfi-vi-mapper .cfi-vi-hier-row {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.cfi-vi-mapper .cfi-vi-hier-elbow {\n width: 18px;\n align-self: stretch;\n border-left: 1px solid var(--cfi-color-gray-200, #e5e7eb);\n border-bottom: 1px solid var(--cfi-color-gray-200, #e5e7eb);\n height: 18px;\n margin-bottom: 9px;\n flex-shrink: 0;\n}\n.cfi-vi-mapper .cfi-vi-hier-dot {\n width: 14px;\n height: 14px;\n border-radius: 50%;\n border: 2px solid var(--cfi-color-gray-300, #d1d5db);\n background: #fff;\n flex-shrink: 0;\n}\n.cfi-vi-mapper .cfi-vi-hier-dot--filled {\n border-color: var(--cfi-color-primary-600, #6366f1);\n background: var(--cfi-color-primary-50, #eef2ff);\n}\n.cfi-vi-mapper .cfi-vi-hier-name {\n font-size: var(--cfi-font-size-xs, 12px);\n font-weight: 600;\n color: var(--cfi-color-gray-600, #4b5563);\n width: 96px;\n flex-shrink: 0;\n}\n.cfi-vi-mapper .cfi-vi-leaf {\n font-size: 10px;\n font-weight: 600;\n color: #b45309;\n background: #fffbeb;\n border: 1px solid #fde68a;\n padding: 1px 6px;\n border-radius: 999px;\n flex-shrink: 0;\n}\n.cfi-vi-mapper .cfi-vi-dropzone {\n flex: 1;\n min-height: 36px;\n border-radius: var(--cfi-radius-md, 6px);\n border: 2px solid;\n display: flex;\n align-items: center;\n padding: 0 10px;\n min-width: 0;\n transition: all 0.12s;\n}\n.cfi-vi-mapper .cfi-vi-dropzone--active {\n border-color: var(--cfi-color-primary-600, #6366f1);\n background: var(--cfi-color-primary-50, #eef2ff);\n}\n.cfi-vi-mapper .cfi-vi-dropzone--filled {\n border-style: solid;\n border-color: var(--cfi-color-gray-200, #e5e7eb);\n background: #fff;\n}\n.cfi-vi-mapper .cfi-vi-dropzone--empty {\n border-style: dashed;\n border-color: var(--cfi-color-gray-200, #e5e7eb);\n background: var(--cfi-color-gray-50, #f9fafb);\n}\n.cfi-vi-mapper .cfi-vi-dropzone--covered {\n border-style: solid;\n border-color: #a7f3d0;\n background: #ecfdf5;\n}\n.cfi-vi-mapper .cfi-vi-dropzone-ph {\n font-size: var(--cfi-font-size-xs, 12px);\n color: var(--cfi-color-gray-400, #9ca3af);\n user-select: none;\n}\n.cfi-vi-mapper .cfi-vi-base-hint {\n font-size: 10px;\n font-family: ui-monospace, monospace;\n color: #1d4ed8;\n background: #eff6ff;\n border: 1px solid #bfdbfe;\n padding: 1px 6px;\n border-radius: 4px;\n flex-shrink: 0;\n white-space: nowrap;\n}\n.cfi-vi-mapper .cfi-vi-auto-row {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n}\n.cfi-vi-mapper .cfi-vi-auto-label {\n width: 160px;\n flex-shrink: 0;\n padding-top: 6px;\n}\n.cfi-vi-mapper .cfi-vi-auto-label-text {\n font-size: var(--cfi-font-size-xs, 12px);\n font-weight: 600;\n color: var(--cfi-color-gray-700, #374151);\n}\n.cfi-vi-mapper .cfi-vi-auto-cover {\n font-size: 10px;\n margin: 2px 0 0;\n color: var(--cfi-color-gray-500, #6b7280);\n}\n.cfi-vi-mapper .cfi-vi-auto-cover--on {\n color: #047857;\n}\n.cfi-vi-mapper .cfi-vi-auto-auto {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n min-width: 0;\n color: #047857;\n}\n.cfi-vi-mapper .cfi-vi-auto-auto-label {\n font-size: var(--cfi-font-size-xs, 12px);\n font-family: ui-monospace, monospace;\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.cfi-vi-mapper .cfi-vi-auto-auto-tag {\n font-size: 10px;\n font-style: italic;\n color: #059669;\n flex-shrink: 0;\n}\n.cfi-vi-mapper .cfi-vi-auto-transform {\n width: 224px;\n flex-shrink: 0;\n padding-top: 6px;\n font-size: 11px;\n color: var(--cfi-color-gray-500, #6b7280);\n line-height: 1.3;\n}\n.cfi-vi-mapper .cfi-vi-auto-transform-label {\n font-weight: 600;\n}\n.cfi-vi-mapper .cfi-vi-pool-col {\n display: flex;\n flex-direction: column;\n gap: var(--cfi-spacing-sm, 8px);\n}\n@media (min-width: 1024px) {\n .cfi-vi-mapper .cfi-vi-pool-col {\n position: sticky;\n top: 16px;\n align-self: start;\n }\n}\n.cfi-vi-mapper .cfi-vi-search {\n position: relative;\n}\n.cfi-vi-mapper .cfi-vi-search .cfi-vi-input {\n padding-left: 28px;\n}\n.cfi-vi-mapper .cfi-vi-search-icon {\n position: absolute;\n left: 8px;\n top: 50%;\n transform: translateY(-50%);\n color: var(--cfi-color-gray-400, #9ca3af);\n pointer-events: none;\n}\n.cfi-vi-mapper .cfi-vi-pool {\n border: 2px dashed var(--cfi-color-gray-200, #e5e7eb);\n background: var(--cfi-color-gray-50, #f9fafb);\n border-radius: var(--cfi-radius-lg, 8px);\n padding: 12px;\n min-height: 200px;\n max-height: 600px;\n overflow-y: auto;\n}\n.cfi-vi-mapper .cfi-vi-pool--active {\n border-color: var(--cfi-color-primary-600, #818cf8);\n background: var(--cfi-color-primary-50, #eef2ff);\n}\n.cfi-vi-mapper .cfi-vi-pool-empty {\n font-size: var(--cfi-font-size-xs, 12px);\n color: var(--cfi-color-gray-400, #9ca3af);\n font-style: italic;\n}\n.cfi-vi-mapper .cfi-vi-chips {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n}\n.cfi-vi-mapper .cfi-vi-chip {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 4px 10px;\n border-radius: var(--cfi-radius-md, 6px);\n background: #fff;\n border: 1px solid var(--cfi-color-gray-200, #e5e7eb);\n cursor: grab;\n user-select: none;\n font-size: var(--cfi-font-size-xs, 12px);\n font-family: ui-monospace, monospace;\n color: var(--cfi-color-gray-700, #374151);\n}\n.cfi-vi-mapper .cfi-vi-chip:hover {\n border-color: var(--cfi-color-primary-600, #818cf8);\n background: var(--cfi-color-primary-50, #eef2ff);\n}\n.cfi-vi-mapper .cfi-vi-chip:active {\n cursor: grabbing;\n}\n.cfi-vi-mapper .cfi-vi-chip--assigned {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n min-width: 0;\n cursor: grab;\n}\n.cfi-vi-mapper .cfi-vi-chip--assigned:active {\n cursor: grabbing;\n}\n.cfi-vi-mapper .cfi-vi-chip-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--cfi-color-gray-400, #9ca3af);\n flex-shrink: 0;\n}\n.cfi-vi-mapper .cfi-vi-chip-dot--on {\n background: var(--cfi-color-primary-600, #6366f1);\n}\n.cfi-vi-mapper .cfi-vi-chip-label {\n font-size: var(--cfi-font-size-xs, 12px);\n font-family: ui-monospace, monospace;\n color: #3730a3;\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.cfi-vi-mapper .cfi-vi-chip-x {\n margin-left: 4px;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n border: none;\n background: none;\n cursor: pointer;\n color: var(--cfi-color-gray-400, #9ca3af);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n.cfi-vi-mapper .cfi-vi-chip-x:hover {\n background: #fee2e2;\n color: #ef4444;\n}\n.cfi-vi .cfi-vi-spin {\n animation: cfi-vi-spin 0.8s linear infinite;\n}\n.cfi-vi .cfi-vi-detail {\n display: flex;\n flex-direction: column;\n gap: var(--cfi-spacing-lg, 20px);\n}\n.cfi-vi .cfi-vi-h2 {\n font-size: var(--cfi-font-size-base, 15px);\n font-weight: 600;\n margin: 0;\n}\n.cfi-vi .cfi-vi-panel {\n border: 1px solid var(--cfi-color-gray-200, #e5e7eb);\n border-radius: var(--cfi-radius-md, 6px);\n background: #fff;\n padding: var(--cfi-spacing-md, 16px);\n display: flex;\n flex-direction: column;\n gap: var(--cfi-spacing-sm, 12px);\n}\n.cfi-vi .cfi-vi-grid2 {\n display: grid;\n grid-template-columns: 1fr;\n gap: var(--cfi-spacing-md, 16px);\n}\n@media (min-width: 768px) {\n .cfi-vi .cfi-vi-grid2 {\n grid-template-columns: 1fr 1fr;\n }\n}\n.cfi-vi .cfi-vi-lock {\n display: flex;\n align-items: center;\n gap: 8px;\n border: 1px solid #fde68a;\n background: #fffbeb;\n color: #92400e;\n border-radius: var(--cfi-radius-md, 6px);\n padding: 8px 12px;\n font-size: var(--cfi-font-size-xs, 12px);\n}\n.cfi-vi .cfi-vi-table {\n width: 100%;\n border-collapse: collapse;\n border: 1px solid var(--cfi-color-gray-200, #e5e7eb);\n border-radius: var(--cfi-radius-md, 6px);\n font-size: var(--cfi-font-size-sm, 13px);\n}\n.cfi-vi .cfi-vi-table th,\n.cfi-vi .cfi-vi-table td {\n text-align: left;\n padding: 6px 10px;\n border-bottom: 1px solid var(--cfi-color-gray-100, #f3f4f6);\n vertical-align: middle;\n}\n.cfi-vi .cfi-vi-table th {\n font-size: var(--cfi-font-size-xs, 11px);\n font-weight: 600;\n color: var(--cfi-color-gray-500, #6b7280);\n}\n.cfi-vi .cfi-vi-table tr:last-child td {\n border-bottom: none;\n}\n.cfi-vi .cfi-vi-danger {\n border: 1px solid rgba(220, 38, 38, 0.4);\n background: rgba(220, 38, 38, 0.05);\n border-radius: var(--cfi-radius-md, 6px);\n padding: var(--cfi-spacing-md, 16px);\n display: flex;\n flex-direction: column;\n gap: var(--cfi-spacing-sm, 12px);\n}\n.cfi-vi .cfi-vi-aliases {\n display: flex;\n flex-direction: column;\n gap: var(--cfi-spacing-sm, 12px);\n}\n.cfi-vi .cfi-vi-alias-rows {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n");
29
+
30
+ // src/VolumeImport.tsx
31
+ import { jsx, jsxs } from "react/jsx-runtime";
32
+ var MONTH_NAMES = [
33
+ "January",
34
+ "February",
35
+ "March",
36
+ "April",
37
+ "May",
38
+ "June",
39
+ "July",
40
+ "August",
41
+ "September",
42
+ "October",
43
+ "November",
44
+ "December"
45
+ ];
46
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
47
+ "done",
48
+ "done_with_errors",
49
+ "failed"
50
+ ]);
51
+ var STATUS_LABELS = {
52
+ awaiting_calendar: "awaiting calendar",
53
+ done_with_errors: "done with errors"
54
+ };
55
+ var DEFAULT_POLL_MS = 3e3;
56
+ function isTerminal(status) {
57
+ return TERMINAL_STATUSES.has(status);
58
+ }
59
+ function yearOptions() {
60
+ const y = (/* @__PURE__ */ new Date()).getFullYear();
61
+ return [y + 1, y, y - 1, y - 2];
62
+ }
63
+ function VolumeImport(props) {
64
+ const { transport, defaultSourceId, pollIntervalMs = DEFAULT_POLL_MS, onUploaded, className } = props;
65
+ const [sources, setSources] = useState(null);
66
+ const [sourcesError, setSourcesError] = useState(null);
67
+ const [selectedId, setSelectedId] = useState(defaultSourceId ?? null);
68
+ const [file, setFile] = useState(null);
69
+ const [year, setYear] = useState(() => (/* @__PURE__ */ new Date()).getFullYear());
70
+ const [month, setMonth] = useState(() => (/* @__PURE__ */ new Date()).getMonth() + 1);
71
+ const [lag, setLag] = useState(2);
72
+ const [submitting, setSubmitting] = useState(false);
73
+ const [submitError, setSubmitError] = useState(null);
74
+ const [upload, setUpload] = useState(null);
75
+ const [errors, setErrors] = useState(null);
76
+ const fileInputRef = useRef(null);
77
+ const pollTimer = useRef(null);
78
+ useEffect(() => {
79
+ let alive = true;
80
+ transport.listSources().then((list) => {
81
+ if (!alive) return;
82
+ setSources(list);
83
+ setSelectedId((cur) => cur ?? defaultSourceId ?? list.find((s) => s.ready !== false)?.id ?? list[0]?.id ?? null);
84
+ }).catch((e) => alive && setSourcesError(e instanceof Error ? e.message : "Failed to load sources"));
85
+ return () => {
86
+ alive = false;
87
+ };
88
+ }, [transport, defaultSourceId]);
89
+ const selected = useMemo(
90
+ () => sources?.find((s) => s.id === selectedId) ?? null,
91
+ [sources, selectedId]
92
+ );
93
+ const isWide = selected?.layout !== "tall";
94
+ useEffect(() => {
95
+ if (!upload || isTerminal(upload.status)) return;
96
+ pollTimer.current = setTimeout(() => {
97
+ transport.getUpload(upload.id).then(setUpload).catch(() => {
98
+ });
99
+ }, pollIntervalMs);
100
+ return () => {
101
+ if (pollTimer.current) clearTimeout(pollTimer.current);
102
+ };
103
+ }, [upload, transport, pollIntervalMs]);
104
+ useEffect(() => {
105
+ if (!upload || !isTerminal(upload.status)) return;
106
+ onUploaded?.(upload);
107
+ if (upload.status === "done_with_errors" || upload.status === "failed") {
108
+ transport.listErrors(upload.id, { page: 1 }).then((r) => setErrors(r.errors)).catch(() => setErrors(null));
109
+ }
110
+ }, [upload?.id, upload?.status]);
111
+ const blockers = selected?.blockers ?? [];
112
+ const ready = !!selected && selected.ready !== false && blockers.length === 0;
113
+ const canSubmit = ready && !!file && !submitting;
114
+ const handleSubmit = useCallback(async () => {
115
+ if (!selected || !file) return;
116
+ setSubmitting(true);
117
+ setSubmitError(null);
118
+ setErrors(null);
119
+ try {
120
+ const rec = await transport.uploadFile({
121
+ sourceId: selected.id,
122
+ file,
123
+ ...isWide ? { uploadYear: year, uploadMonth: month, actualsLagMonths: lag } : {}
124
+ });
125
+ setUpload(rec);
126
+ setFile(null);
127
+ if (fileInputRef.current) fileInputRef.current.value = "";
128
+ } catch (e) {
129
+ setSubmitError(e instanceof Error ? e.message : "Upload failed");
130
+ } finally {
131
+ setSubmitting(false);
132
+ }
133
+ }, [selected, file, isWide, year, month, lag, transport]);
134
+ const handleReprocess = useCallback(async () => {
135
+ if (!upload || !transport.reprocess) return;
136
+ try {
137
+ setUpload(await transport.reprocess(upload.id));
138
+ setErrors(null);
139
+ } catch (e) {
140
+ setSubmitError(e instanceof Error ? e.message : "Reprocess failed");
141
+ }
142
+ }, [upload, transport]);
143
+ const rootClass = className ? `cfi-volume-import ${className}` : "cfi-volume-import";
144
+ if (sourcesError) {
145
+ return /* @__PURE__ */ jsx("div", { className: rootClass, children: /* @__PURE__ */ jsxs("div", { className: "cfi-vi-alert cfi-vi-alert--error", children: [
146
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 16, "aria-hidden": true }),
147
+ " ",
148
+ /* @__PURE__ */ jsx("span", { children: sourcesError })
149
+ ] }) });
150
+ }
151
+ if (!sources) {
152
+ return /* @__PURE__ */ jsx("div", { className: rootClass, children: /* @__PURE__ */ jsxs("div", { className: "cfi-vi-loading", children: [
153
+ /* @__PURE__ */ jsx(Loader2, { size: 16, className: "cfi-vi-spin", "aria-hidden": true }),
154
+ " Loading sources\u2026"
155
+ ] }) });
156
+ }
157
+ return /* @__PURE__ */ jsxs("div", { className: rootClass, children: [
158
+ /* @__PURE__ */ jsxs("div", { className: "cfi-vi-field", children: [
159
+ /* @__PURE__ */ jsx("label", { className: "cfi-vi-label", htmlFor: "cfi-vi-source", children: "Source" }),
160
+ /* @__PURE__ */ jsxs(
161
+ "select",
162
+ {
163
+ id: "cfi-vi-source",
164
+ className: "cfi-vi-select",
165
+ value: selectedId ?? "",
166
+ onChange: (e) => setSelectedId(e.target.value || null),
167
+ children: [
168
+ sources.length === 0 && /* @__PURE__ */ jsx("option", { value: "", children: "No sources available" }),
169
+ sources.map((s) => /* @__PURE__ */ jsxs("option", { value: s.id, children: [
170
+ s.name,
171
+ s.layout === "tall" ? " (tall)" : ""
172
+ ] }, s.id))
173
+ ]
174
+ }
175
+ )
176
+ ] }),
177
+ blockers.length > 0 && /* @__PURE__ */ jsxs("div", { className: "cfi-vi-alert cfi-vi-alert--warn", children: [
178
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 16, "aria-hidden": true }),
179
+ /* @__PURE__ */ jsx("ul", { className: "cfi-vi-blockers", children: blockers.map((b, i) => /* @__PURE__ */ jsx("li", { children: b }, i)) })
180
+ ] }),
181
+ isWide && /* @__PURE__ */ jsxs("div", { className: "cfi-vi-row", children: [
182
+ /* @__PURE__ */ jsxs("div", { className: "cfi-vi-field", children: [
183
+ /* @__PURE__ */ jsx("label", { className: "cfi-vi-label", htmlFor: "cfi-vi-year", children: "Year" }),
184
+ /* @__PURE__ */ jsx("select", { id: "cfi-vi-year", className: "cfi-vi-select", value: year, onChange: (e) => setYear(Number(e.target.value)), children: yearOptions().map((y) => /* @__PURE__ */ jsx("option", { value: y, children: y }, y)) })
185
+ ] }),
186
+ /* @__PURE__ */ jsxs("div", { className: "cfi-vi-field", children: [
187
+ /* @__PURE__ */ jsx("label", { className: "cfi-vi-label", htmlFor: "cfi-vi-month", children: "Month" }),
188
+ /* @__PURE__ */ jsx("select", { id: "cfi-vi-month", className: "cfi-vi-select", value: month, onChange: (e) => setMonth(Number(e.target.value)), children: MONTH_NAMES.map((name, i) => /* @__PURE__ */ jsx("option", { value: i + 1, children: name }, name)) })
189
+ ] }),
190
+ /* @__PURE__ */ jsxs("div", { className: "cfi-vi-field", children: [
191
+ /* @__PURE__ */ jsx("label", { className: "cfi-vi-label", htmlFor: "cfi-vi-lag", children: "Actuals lag (months)" }),
192
+ /* @__PURE__ */ jsx(
193
+ "input",
194
+ {
195
+ id: "cfi-vi-lag",
196
+ type: "number",
197
+ min: 0,
198
+ max: 12,
199
+ className: "cfi-vi-select",
200
+ value: lag,
201
+ onChange: (e) => setLag(Math.max(0, Math.min(12, Number(e.target.value))))
202
+ }
203
+ )
204
+ ] })
205
+ ] }),
206
+ /* @__PURE__ */ jsxs("div", { className: "cfi-vi-field", children: [
207
+ /* @__PURE__ */ jsx("label", { className: "cfi-vi-label", htmlFor: "cfi-vi-file", children: "File (.xlsx / .xls)" }),
208
+ /* @__PURE__ */ jsx(
209
+ "input",
210
+ {
211
+ id: "cfi-vi-file",
212
+ ref: fileInputRef,
213
+ type: "file",
214
+ accept: ".xlsx,.xls",
215
+ className: "cfi-vi-file",
216
+ onChange: (e) => setFile(e.target.files?.[0] ?? null)
217
+ }
218
+ )
219
+ ] }),
220
+ submitError && /* @__PURE__ */ jsxs("div", { className: "cfi-vi-alert cfi-vi-alert--error", children: [
221
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 16, "aria-hidden": true }),
222
+ " ",
223
+ /* @__PURE__ */ jsx("span", { children: submitError })
224
+ ] }),
225
+ /* @__PURE__ */ jsxs("button", { type: "button", className: "cfi-vi-button", disabled: !canSubmit, onClick: handleSubmit, children: [
226
+ submitting ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "cfi-vi-spin", "aria-hidden": true }) : /* @__PURE__ */ jsx(FileUp, { size: 16, "aria-hidden": true }),
227
+ submitting ? "Uploading\u2026" : "Upload"
228
+ ] }),
229
+ upload && /* @__PURE__ */ jsx(UploadStatusPanel, { upload, errors, onReprocess: transport.reprocess ? handleReprocess : void 0 })
230
+ ] });
231
+ }
232
+ function UploadStatusPanel(props) {
233
+ const { upload, errors, onReprocess } = props;
234
+ const label = STATUS_LABELS[upload.status] ?? upload.status;
235
+ const pending = !isTerminal(upload.status);
236
+ return /* @__PURE__ */ jsxs("div", { className: "cfi-vi-status", children: [
237
+ /* @__PURE__ */ jsxs("div", { className: "cfi-vi-status-head", children: [
238
+ /* @__PURE__ */ jsx("span", { className: "cfi-vi-filename", children: upload.fileName }),
239
+ /* @__PURE__ */ jsxs("span", { className: `cfi-vi-badge cfi-vi-badge--${upload.status}`, children: [
240
+ pending && /* @__PURE__ */ jsx(Loader2, { size: 12, className: "cfi-vi-spin", "aria-hidden": true }),
241
+ " ",
242
+ label
243
+ ] }),
244
+ upload.status === "failed" && onReprocess && /* @__PURE__ */ jsxs("button", { type: "button", className: "cfi-vi-link", onClick: onReprocess, children: [
245
+ /* @__PURE__ */ jsx(RotateCcw, { size: 12, "aria-hidden": true }),
246
+ " Reprocess"
247
+ ] })
248
+ ] }),
249
+ upload.stats && (upload.stats.programs != null || upload.stats.volumes != null) && /* @__PURE__ */ jsxs("div", { className: "cfi-vi-stats", children: [
250
+ upload.stats.programs != null && /* @__PURE__ */ jsxs("span", { children: [
251
+ upload.stats.programs,
252
+ " programs"
253
+ ] }),
254
+ upload.stats.volumes != null && /* @__PURE__ */ jsxs("span", { children: [
255
+ upload.stats.volumes,
256
+ " volume rows"
257
+ ] })
258
+ ] }),
259
+ errors && errors.length > 0 && /* @__PURE__ */ jsxs("table", { className: "cfi-vi-errors", children: [
260
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
261
+ /* @__PURE__ */ jsx("th", { children: "Row" }),
262
+ /* @__PURE__ */ jsx("th", { children: "Type" }),
263
+ /* @__PURE__ */ jsx("th", { children: "Message" })
264
+ ] }) }),
265
+ /* @__PURE__ */ jsx("tbody", { children: errors.map((e, i) => /* @__PURE__ */ jsxs("tr", { children: [
266
+ /* @__PURE__ */ jsx("td", { children: e.row ?? "\u2014" }),
267
+ /* @__PURE__ */ jsx("td", { children: e.type }),
268
+ /* @__PURE__ */ jsx("td", { children: e.message })
269
+ ] }, e.id ?? i)) })
270
+ ] })
271
+ ] });
272
+ }
273
+
274
+ // src/VolumeSourcesList.tsx
275
+ import { useCallback as useCallback2, useEffect as useEffect2, useState as useState2 } from "react";
276
+ import { CheckCircle2, Sparkles } from "lucide-react";
277
+
278
+ // src/ui.tsx
279
+ import { X } from "lucide-react";
280
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
281
+ function Root(props) {
282
+ const cls = props.className ? `cfi-vi ${props.className}` : "cfi-vi";
283
+ return /* @__PURE__ */ jsx2("div", { className: cls, children: props.children });
284
+ }
285
+ function Btn(props) {
286
+ const { variant = "primary", size = "md", className, ...rest } = props;
287
+ const cls = ["cfi-vi-btn", `cfi-vi-btn--${variant}`, `cfi-vi-btn--${size}`, className].filter(Boolean).join(" ");
288
+ return /* @__PURE__ */ jsx2("button", { type: "button", className: cls, ...rest });
289
+ }
290
+ function Badge(props) {
291
+ return /* @__PURE__ */ jsx2("span", { className: `cfi-vi-tag cfi-vi-tag--${props.variant ?? "default"}`, title: props.title, children: props.children });
292
+ }
293
+ function Card(props) {
294
+ return /* @__PURE__ */ jsx2("div", { className: props.className ? `cfi-vi-card ${props.className}` : "cfi-vi-card", children: props.children });
295
+ }
296
+ function CardHeader(props) {
297
+ return /* @__PURE__ */ jsx2("div", { className: "cfi-vi-card-head", children: props.children });
298
+ }
299
+ function CardBody(props) {
300
+ return /* @__PURE__ */ jsx2("div", { className: "cfi-vi-card-body", children: props.children });
301
+ }
302
+ function Field(props) {
303
+ return /* @__PURE__ */ jsxs2("div", { className: "cfi-vi-field", children: [
304
+ /* @__PURE__ */ jsx2("label", { className: "cfi-vi-label", htmlFor: props.htmlFor, children: props.label }),
305
+ props.children,
306
+ props.hint && /* @__PURE__ */ jsx2("p", { className: "cfi-vi-hint", children: props.hint })
307
+ ] });
308
+ }
309
+ function TextInput(props) {
310
+ const { className, ...rest } = props;
311
+ return /* @__PURE__ */ jsx2("input", { className: className ? `cfi-vi-input ${className}` : "cfi-vi-input", ...rest });
312
+ }
313
+ function Modal(props) {
314
+ if (!props.open) return null;
315
+ return /* @__PURE__ */ jsx2("div", { className: "cfi-vi-modal-overlay", role: "dialog", "aria-modal": "true", onClick: props.onClose, children: /* @__PURE__ */ jsxs2("div", { className: "cfi-vi-modal", onClick: (e) => e.stopPropagation(), children: [
316
+ /* @__PURE__ */ jsxs2("div", { className: "cfi-vi-modal-head", children: [
317
+ /* @__PURE__ */ jsx2("h2", { className: "cfi-vi-modal-title", children: props.title }),
318
+ /* @__PURE__ */ jsx2("button", { type: "button", className: "cfi-vi-icon-btn", "aria-label": "Close", onClick: props.onClose, children: /* @__PURE__ */ jsx2(X, { size: 16, "aria-hidden": true }) })
319
+ ] }),
320
+ /* @__PURE__ */ jsx2("div", { className: "cfi-vi-modal-body", children: props.children }),
321
+ props.footer && /* @__PURE__ */ jsx2("div", { className: "cfi-vi-modal-foot", children: props.footer })
322
+ ] }) });
323
+ }
324
+
325
+ // src/VolumeSourcesList.tsx
326
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
327
+ var EMPTY_FORM = { code: "", name: "", description: "", headerRowIndex: 0, volumeHeaderFormat: "MMM yyyy" };
328
+ function VolumeSourcesList({ transport, onOpenSource, renderActiveSourceExtras, className }) {
329
+ const [sources, setSources] = useState2([]);
330
+ const [activeSourceId, setActiveSourceId] = useState2(null);
331
+ const [templates, setTemplates] = useState2([]);
332
+ const [loading, setLoading] = useState2(true);
333
+ const [error, setError] = useState2(null);
334
+ const [showCreate, setShowCreate] = useState2(false);
335
+ const [showTemplates, setShowTemplates] = useState2(false);
336
+ const [form, setForm] = useState2(EMPTY_FORM);
337
+ const [saving, setSaving] = useState2(false);
338
+ const [activating, setActivating] = useState2(null);
339
+ const [addingTemplate, setAddingTemplate] = useState2(null);
340
+ const [confirmActivate, setConfirmActivate] = useState2(null);
341
+ const load = useCallback2(async () => {
342
+ try {
343
+ setLoading(true);
344
+ const [allSources, config, allTemplates] = await Promise.all([
345
+ transport.listSources(),
346
+ transport.getTenantConfig(),
347
+ transport.listSourceTemplates()
348
+ ]);
349
+ setSources(allSources);
350
+ setActiveSourceId(config?.sourceId ?? null);
351
+ setTemplates(allTemplates);
352
+ } catch (e) {
353
+ setError(e instanceof Error ? e.message : "Failed to load sources");
354
+ } finally {
355
+ setLoading(false);
356
+ }
357
+ }, [transport]);
358
+ useEffect2(() => {
359
+ void load();
360
+ }, [load]);
361
+ const availableTemplates = templates.filter((t) => !sources.some((s) => s.code === t.code));
362
+ const handleAddFromTemplate = useCallback2(async (code) => {
363
+ setAddingTemplate(code);
364
+ setError(null);
365
+ try {
366
+ const created = await transport.createSourceFromTemplate(code);
367
+ setShowTemplates(false);
368
+ onOpenSource(created.id);
369
+ } catch (e) {
370
+ setError(e instanceof Error ? e.message : "Failed to add source");
371
+ } finally {
372
+ setAddingTemplate(null);
373
+ }
374
+ }, [transport, onOpenSource]);
375
+ const handleCreate = useCallback2(async () => {
376
+ if (!form.code || !form.name) return;
377
+ setSaving(true);
378
+ try {
379
+ await transport.createSource(form);
380
+ setShowCreate(false);
381
+ setForm(EMPTY_FORM);
382
+ await load();
383
+ } catch (e) {
384
+ setError(e instanceof Error ? e.message : "Failed to create source");
385
+ } finally {
386
+ setSaving(false);
387
+ }
388
+ }, [form, transport, load]);
389
+ const doActivate = useCallback2(async (sourceId) => {
390
+ setActivating(sourceId);
391
+ setConfirmActivate(null);
392
+ try {
393
+ await transport.activateSource(sourceId);
394
+ await load();
395
+ } catch (e) {
396
+ setError(e instanceof Error ? e.message : "Failed to activate source");
397
+ } finally {
398
+ setActivating(null);
399
+ }
400
+ }, [transport, load]);
401
+ const handleActivate = useCallback2((src) => {
402
+ if (activeSourceId && activeSourceId !== src.id) {
403
+ setConfirmActivate(src);
404
+ return;
405
+ }
406
+ void doActivate(src.id);
407
+ }, [activeSourceId, doActivate]);
408
+ const currentActive = sources.find((s) => s.id === activeSourceId);
409
+ if (loading) return /* @__PURE__ */ jsx3(Root, { className, children: /* @__PURE__ */ jsx3("p", { className: "cfi-vi-muted", children: "Loading\u2026" }) });
410
+ return /* @__PURE__ */ jsxs3(Root, { className, children: [
411
+ /* @__PURE__ */ jsxs3("div", { className: "cfi-vi-list-head", children: [
412
+ /* @__PURE__ */ jsxs3("div", { children: [
413
+ /* @__PURE__ */ jsx3("h1", { className: "cfi-vi-h1", children: "Volume Sources" }),
414
+ currentActive && /* @__PURE__ */ jsxs3("p", { className: "cfi-vi-active-line", children: [
415
+ /* @__PURE__ */ jsx3(CheckCircle2, { size: 14, color: "#16a34a", "aria-hidden": true }),
416
+ "Active: ",
417
+ /* @__PURE__ */ jsx3("span", { className: "cfi-vi-active-name", children: currentActive.name }),
418
+ /* @__PURE__ */ jsxs3("span", { className: "cfi-vi-muted", children: [
419
+ "(",
420
+ currentActive.code,
421
+ ")"
422
+ ] })
423
+ ] })
424
+ ] }),
425
+ /* @__PURE__ */ jsxs3("div", { className: "cfi-vi-row", children: [
426
+ availableTemplates.length > 0 && /* @__PURE__ */ jsxs3(Btn, { variant: "outline", size: "sm", onClick: () => setShowTemplates(true), children: [
427
+ /* @__PURE__ */ jsx3(Sparkles, { size: 14, "aria-hidden": true }),
428
+ " Add from template"
429
+ ] }),
430
+ /* @__PURE__ */ jsx3(Btn, { size: "sm", onClick: () => setShowCreate(true), children: "Add Source" })
431
+ ] })
432
+ ] }),
433
+ error && /* @__PURE__ */ jsx3("p", { className: "cfi-vi-error", children: error }),
434
+ /* @__PURE__ */ jsxs3("div", { className: "cfi-vi-list", children: [
435
+ sources.length === 0 && /* @__PURE__ */ jsx3("p", { className: "cfi-vi-muted", children: "No sources defined yet." }),
436
+ sources.map((src) => {
437
+ const isActive = src.id === activeSourceId;
438
+ return /* @__PURE__ */ jsxs3(Card, { className: isActive ? "cfi-vi-card--active" : "cfi-vi-card--hover", children: [
439
+ /* @__PURE__ */ jsx3(CardHeader, { children: /* @__PURE__ */ jsxs3("div", { className: "cfi-vi-row cfi-vi-row--between", children: [
440
+ /* @__PURE__ */ jsxs3("span", { className: "cfi-vi-card-title", children: [
441
+ /* @__PURE__ */ jsx3("button", { type: "button", className: "cfi-vi-link", onClick: () => onOpenSource(src.id), children: src.name }),
442
+ isActive && /* @__PURE__ */ jsx3(Badge, { variant: "success", children: "Active" }),
443
+ src.kind === "overlay" && /* @__PURE__ */ jsx3(Badge, { variant: "purple", title: "Overlay sources are active whenever data is uploaded \u2014 no Set Active step.", children: "Overlay" })
444
+ ] }),
445
+ /* @__PURE__ */ jsxs3("span", { className: "cfi-vi-row", children: [
446
+ /* @__PURE__ */ jsx3(Badge, { variant: "outline", children: src.code }),
447
+ !src.isActive && /* @__PURE__ */ jsx3(Badge, { variant: "secondary", children: "Disabled" }),
448
+ !isActive && src.kind === "base" && /* @__PURE__ */ jsx3(Btn, { variant: "outline", size: "sm", disabled: activating === src.id, onClick: () => handleActivate(src), children: activating === src.id ? "Activating\u2026" : "Set Active" })
449
+ ] })
450
+ ] }) }),
451
+ /* @__PURE__ */ jsxs3(CardBody, { children: [
452
+ /* @__PURE__ */ jsxs3("p", { className: "cfi-vi-muted", style: { fontSize: "var(--cfi-font-size-sm, 13px)" }, children: [
453
+ src.description || "No description",
454
+ " \xB7 ",
455
+ /* @__PURE__ */ jsx3("span", { className: "cfi-vi-mono", children: src.volumeHeaderFormat })
456
+ ] }),
457
+ src._count && /* @__PURE__ */ jsxs3("p", { className: "cfi-vi-muted", style: { fontSize: "var(--cfi-font-size-xs, 11px)", marginTop: 4 }, children: [
458
+ src._count.segments,
459
+ " segments detected"
460
+ ] }),
461
+ isActive && renderActiveSourceExtras && /* @__PURE__ */ jsx3("div", { className: "cfi-vi-active-extras", children: renderActiveSourceExtras(src) })
462
+ ] })
463
+ ] }, src.id);
464
+ })
465
+ ] }),
466
+ /* @__PURE__ */ jsxs3(
467
+ Modal,
468
+ {
469
+ open: !!confirmActivate,
470
+ onClose: () => setConfirmActivate(null),
471
+ title: "Switch active source?",
472
+ footer: /* @__PURE__ */ jsxs3(Fragment, { children: [
473
+ /* @__PURE__ */ jsx3(Btn, { variant: "outline", onClick: () => setConfirmActivate(null), children: "Cancel" }),
474
+ /* @__PURE__ */ jsx3(Btn, { variant: "destructive", onClick: () => confirmActivate && void doActivate(confirmActivate.id), children: "Switch and wipe data" })
475
+ ] }),
476
+ children: [
477
+ /* @__PURE__ */ jsxs3("p", { children: [
478
+ "Switching the active source to ",
479
+ /* @__PURE__ */ jsx3("strong", { children: confirmActivate?.name }),
480
+ " (",
481
+ confirmActivate?.code,
482
+ ")."
483
+ ] }),
484
+ /* @__PURE__ */ jsx3("p", { className: "cfi-vi-error", children: "This permanently wipes all ingested data for this tenant \u2014 volume hierarchy, forecast data, edits, scenarios, uploads, and audit log. Your level 1\u20133 mapping and segment configuration are preserved. This cannot be undone." })
485
+ ]
486
+ }
487
+ ),
488
+ /* @__PURE__ */ jsxs3(
489
+ Modal,
490
+ {
491
+ open: showCreate,
492
+ onClose: () => setShowCreate(false),
493
+ title: "Add volume source",
494
+ footer: /* @__PURE__ */ jsxs3(Fragment, { children: [
495
+ /* @__PURE__ */ jsx3(Btn, { variant: "outline", onClick: () => setShowCreate(false), children: "Cancel" }),
496
+ /* @__PURE__ */ jsx3(Btn, { onClick: handleCreate, disabled: saving || !form.code || !form.name, children: saving ? "Creating\u2026" : "Create" })
497
+ ] }),
498
+ children: [
499
+ /* @__PURE__ */ jsx3(Field, { label: "Code", children: /* @__PURE__ */ jsx3(TextInput, { placeholder: "e.g. IHS", value: form.code, onChange: (e) => setForm((f) => ({ ...f, code: e.target.value.toUpperCase() })) }) }),
500
+ /* @__PURE__ */ jsx3(Field, { label: "Name", children: /* @__PURE__ */ jsx3(TextInput, { placeholder: "e.g. IHS Market Production Forecast", value: form.name, onChange: (e) => setForm((f) => ({ ...f, name: e.target.value })) }) }),
501
+ /* @__PURE__ */ jsx3(Field, { label: "Description", children: /* @__PURE__ */ jsx3(TextInput, { value: form.description, onChange: (e) => setForm((f) => ({ ...f, description: e.target.value })) }) }),
502
+ /* @__PURE__ */ jsx3(Field, { label: "Volume header format", hint: 'e.g. "MMM yyyy" for "Jan 2025", or "MMM_yyyy VehicleVolume" for AFS', children: /* @__PURE__ */ jsx3(TextInput, { placeholder: "MMM yyyy", value: form.volumeHeaderFormat, onChange: (e) => setForm((f) => ({ ...f, volumeHeaderFormat: e.target.value })) }) }),
503
+ /* @__PURE__ */ jsx3(Field, { label: "Header row index (0-based)", children: /* @__PURE__ */ jsx3(TextInput, { type: "number", value: form.headerRowIndex, onChange: (e) => setForm((f) => ({ ...f, headerRowIndex: parseInt(e.target.value, 10) || 0 })) }) })
504
+ ]
505
+ }
506
+ ),
507
+ /* @__PURE__ */ jsxs3(
508
+ Modal,
509
+ {
510
+ open: showTemplates,
511
+ onClose: () => setShowTemplates(false),
512
+ title: "Add source from template",
513
+ footer: /* @__PURE__ */ jsx3(Btn, { variant: "outline", onClick: () => setShowTemplates(false), children: "Close" }),
514
+ children: [
515
+ /* @__PURE__ */ jsx3("p", { className: "cfi-vi-muted", children: "Templates ship with their segments and default mappings wired up \u2014 pick one to skip manual setup. You can rename, edit columns, or change the layout on the source detail page afterward." }),
516
+ availableTemplates.length === 0 ? /* @__PURE__ */ jsxs3("p", { className: "cfi-vi-muted", children: [
517
+ "You already have every available template. Use ",
518
+ /* @__PURE__ */ jsx3("strong", { children: "Add Source" }),
519
+ " to add another by hand."
520
+ ] }) : availableTemplates.map((t) => /* @__PURE__ */ jsxs3(Card, { className: "cfi-vi-card--hover", children: [
521
+ /* @__PURE__ */ jsx3(CardHeader, { children: /* @__PURE__ */ jsxs3("div", { className: "cfi-vi-row cfi-vi-row--between", children: [
522
+ /* @__PURE__ */ jsxs3("span", { className: "cfi-vi-card-title", children: [
523
+ t.name,
524
+ " ",
525
+ /* @__PURE__ */ jsx3(Badge, { variant: "outline", children: t.code })
526
+ ] }),
527
+ /* @__PURE__ */ jsx3(Btn, { size: "sm", onClick: () => void handleAddFromTemplate(t.code), disabled: addingTemplate !== null, children: addingTemplate === t.code ? "Adding\u2026" : "Add" })
528
+ ] }) }),
529
+ t.description && /* @__PURE__ */ jsx3(CardBody, { children: /* @__PURE__ */ jsx3("p", { className: "cfi-vi-muted", style: { fontSize: "var(--cfi-font-size-xs, 11px)" }, children: t.description }) })
530
+ ] }, t.code))
531
+ ]
532
+ }
533
+ )
534
+ ] });
535
+ }
536
+
537
+ // src/UnifiedSegmentMapper.tsx
538
+ import { useEffect as useEffect3, useMemo as useMemo2, useState as useState3 } from "react";
539
+ import { Search, CheckCircle2 as CheckCircle22 } from "lucide-react";
540
+
541
+ // src/constants.ts
542
+ var HIERARCHY_LEVEL_COUNT = 6;
543
+ var DEFAULT_LEVEL_NAMES = {
544
+ 1: "OEM",
545
+ 2: "Platform",
546
+ 3: "Program",
547
+ 4: "Plant",
548
+ 5: "Nameplate",
549
+ 6: "Trim Level"
550
+ };
551
+ var AUTOMOTIVE_SLOTS = [
552
+ { key: "brand", label: "Brand", required: false },
553
+ { key: "oem_parent", label: "Brand Owner / OEM", required: true, hierarchyCoveredByLevel: 1 },
554
+ { key: "nameplate", label: "Nameplate", required: true, hierarchyCoveredByLevel: 5 },
555
+ { key: "platform", label: "Platform", required: true, hierarchyCoveredByLevel: 2 },
556
+ { key: "program", label: "Program", required: true, hierarchyCoveredByLevel: 3 },
557
+ { key: "region", label: "Region", required: true },
558
+ { key: "plant_name", label: "Plant Name", required: true, hierarchyCoveredByLevel: 4 },
559
+ { key: "plant_country", label: "Plant Country", required: true, defaultTransform: "normalise_country" },
560
+ { key: "sop_date", label: "SOP (Start of Production)", required: true, defaultTransform: "parse_iso_date" },
561
+ { key: "eop_date", label: "EOP (End of Production)", required: true, defaultTransform: "parse_iso_date" }
562
+ ];
563
+ var AUTOMOTIVE_TRANSFORM_LABELS = {
564
+ normalise_country: "Convert country code (e.g. USA \u2192 US)",
565
+ parse_iso_date: "Parse to ISO date (YYYY-MM-DD)"
566
+ };
567
+
568
+ // src/UnifiedSegmentMapper.tsx
569
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
570
+ var LEVELS = [1, 2, 3, 4, 5, 6];
571
+ function UnifiedSegmentMapper({
572
+ segments,
573
+ hierarchyInitial,
574
+ automotiveInitial,
575
+ automotiveSlots,
576
+ transformRules,
577
+ transformRuleLabels,
578
+ onSave,
579
+ saving,
580
+ hierarchyMode = "tree",
581
+ baseHierarchyHint
582
+ }) {
583
+ const isSparse = hierarchyMode === "sparse";
584
+ const [hierAssign, setHierAssign] = useState3({});
585
+ const [autoAssign, setAutoAssign] = useState3({});
586
+ const [searchQuery, setSearchQuery] = useState3("");
587
+ const [dragging, setDragging] = useState3(null);
588
+ const [dropTarget, setDropTarget] = useState3(null);
589
+ useEffect3(() => {
590
+ const initHier = {};
591
+ for (const lv of LEVELS) {
592
+ initHier[lv] = hierarchyInitial.find((m) => m.targetKey === `level${lv}`)?.segmentId ?? null;
593
+ }
594
+ setHierAssign(initHier);
595
+ const initAuto = {};
596
+ for (const slot of automotiveSlots) {
597
+ const existing = automotiveInitial.find((m) => m.targetKey === slot.key);
598
+ initAuto[slot.key] = {
599
+ segmentId: existing?.segmentId ?? null,
600
+ transformRule: existing?.transformRule ?? slot.defaultTransform ?? null
601
+ };
602
+ }
603
+ setAutoAssign(initAuto);
604
+ }, [hierarchyInitial, automotiveInitial, automotiveSlots]);
605
+ const segmentById = useMemo2(() => new Map(segments.map((s) => [s.id, s])), [segments]);
606
+ const usedIds = useMemo2(() => {
607
+ const s = /* @__PURE__ */ new Set();
608
+ for (const v of Object.values(hierAssign)) if (v) s.add(v);
609
+ for (const a of Object.values(autoAssign)) if (a.segmentId) s.add(a.segmentId);
610
+ return s;
611
+ }, [hierAssign, autoAssign]);
612
+ const poolSegments = useMemo2(() => {
613
+ const q = searchQuery.trim().toLowerCase();
614
+ return segments.filter((s) => !usedIds.has(s.id)).filter((s) => !q || s.label.toLowerCase().includes(q)).sort((a, b) => a.label.localeCompare(b.label));
615
+ }, [segments, usedIds, searchQuery]);
616
+ const hierarchyCoveredKeys = useMemo2(() => {
617
+ const covered = /* @__PURE__ */ new Set();
618
+ for (const slot of automotiveSlots) {
619
+ if (slot.hierarchyCoveredByLevel == null) continue;
620
+ const lvSegId = hierAssign[slot.hierarchyCoveredByLevel];
621
+ if (lvSegId && !autoAssign[slot.key]?.segmentId) covered.add(slot.key);
622
+ }
623
+ return covered;
624
+ }, [automotiveSlots, hierAssign, autoAssign]);
625
+ const hierarchyErrors = useMemo2(() => {
626
+ if (isSparse) return [];
627
+ const errors = [];
628
+ for (let n = 2; n <= 6; n++) {
629
+ if (hierAssign[n] && !hierAssign[n - 1]) {
630
+ errors.push(`${DEFAULT_LEVEL_NAMES[n]} requires ${DEFAULT_LEVEL_NAMES[n - 1]} to be set first`);
631
+ }
632
+ }
633
+ return errors;
634
+ }, [hierAssign, isSparse]);
635
+ const missingAutomotive = useMemo2(() => {
636
+ const out = [];
637
+ for (const slot of automotiveSlots) {
638
+ if (hierarchyCoveredKeys.has(slot.key)) continue;
639
+ const explicitRequired = slot.required || slot.hierarchyCoveredByLevel != null && hierAssign[slot.hierarchyCoveredByLevel] == null;
640
+ if (explicitRequired && !autoAssign[slot.key]?.segmentId) out.push(slot.label);
641
+ }
642
+ return out;
643
+ }, [automotiveSlots, hierarchyCoveredKeys, hierAssign, autoAssign]);
644
+ const isDirty = useMemo2(() => {
645
+ for (const lv of LEVELS) {
646
+ const cur = hierAssign[lv] ?? null;
647
+ const saved = hierarchyInitial.find((m) => m.targetKey === `level${lv}`)?.segmentId ?? null;
648
+ if (cur !== saved) return true;
649
+ }
650
+ for (const slot of automotiveSlots) {
651
+ const cur = autoAssign[slot.key] ?? { segmentId: null, transformRule: null };
652
+ const saved = automotiveInitial.find((m) => m.targetKey === slot.key);
653
+ const savedSeg = saved?.segmentId ?? null;
654
+ const savedRule = saved?.transformRule ?? slot.defaultTransform ?? null;
655
+ if (cur.segmentId !== savedSeg || cur.transformRule !== savedRule) return true;
656
+ }
657
+ return false;
658
+ }, [hierAssign, autoAssign, hierarchyInitial, automotiveInitial, automotiveSlots]);
659
+ const canSave = isDirty && hierarchyErrors.length === 0 && missingAutomotive.length === 0 && !saving;
660
+ function startDrag(payload) {
661
+ setDragging(payload);
662
+ }
663
+ function endDrag() {
664
+ setDragging(null);
665
+ setDropTarget(null);
666
+ }
667
+ function dropOnHier(level, payload) {
668
+ const parts = payload.split(":");
669
+ const kind = parts[0];
670
+ const segId = parts[2] ?? parts[1];
671
+ setHierAssign((prev) => {
672
+ const next = { ...prev };
673
+ const displaced = next[level] ?? null;
674
+ next[level] = segId;
675
+ if (kind === "hier") {
676
+ const fromLevel = parseInt(parts[1], 10);
677
+ next[fromLevel] = displaced;
678
+ }
679
+ return next;
680
+ });
681
+ if (kind === "auto") {
682
+ const slotKey = parts[1];
683
+ setAutoAssign((p) => ({ ...p, [slotKey]: { segmentId: null, transformRule: p[slotKey]?.transformRule ?? null } }));
684
+ }
685
+ endDrag();
686
+ }
687
+ function dropOnAuto(slotKey, payload) {
688
+ const parts = payload.split(":");
689
+ const kind = parts[0];
690
+ const segId = parts[2] ?? parts[1];
691
+ setAutoAssign((prev) => {
692
+ const slot = automotiveSlots.find((s) => s.key === slotKey);
693
+ return {
694
+ ...prev,
695
+ [slotKey]: { segmentId: segId, transformRule: prev[slotKey]?.transformRule ?? slot?.defaultTransform ?? null }
696
+ };
697
+ });
698
+ if (kind === "hier") {
699
+ const fromLevel = parseInt(parts[1], 10);
700
+ setHierAssign((p) => ({ ...p, [fromLevel]: null }));
701
+ } else if (kind === "auto") {
702
+ const fromKey = parts[1];
703
+ if (fromKey !== slotKey) {
704
+ setAutoAssign((p) => ({ ...p, [fromKey]: { segmentId: null, transformRule: p[fromKey]?.transformRule ?? null } }));
705
+ }
706
+ }
707
+ endDrag();
708
+ }
709
+ function dropOnPool(payload) {
710
+ const parts = payload.split(":");
711
+ const kind = parts[0];
712
+ if (kind === "hier") {
713
+ const lv = parseInt(parts[1], 10);
714
+ setHierAssign((p) => ({ ...p, [lv]: null }));
715
+ } else if (kind === "auto") {
716
+ const slotKey = parts[1];
717
+ setAutoAssign((p) => ({ ...p, [slotKey]: { segmentId: null, transformRule: p[slotKey]?.transformRule ?? null } }));
718
+ }
719
+ endDrag();
720
+ }
721
+ function clearAuto(slotKey) {
722
+ setAutoAssign((p) => ({ ...p, [slotKey]: { segmentId: null, transformRule: p[slotKey]?.transformRule ?? null } }));
723
+ }
724
+ function setAutoTransform(slotKey, rule) {
725
+ setAutoAssign((p) => ({ ...p, [slotKey]: { ...p[slotKey], transformRule: rule } }));
726
+ }
727
+ async function handleSave() {
728
+ const hierEntries = [];
729
+ for (const lv of LEVELS) {
730
+ const segId = hierAssign[lv];
731
+ if (segId) hierEntries.push({ targetKey: `level${lv}`, segmentId: segId });
732
+ }
733
+ const autoEntries = [];
734
+ for (const slot of automotiveSlots) {
735
+ const a = autoAssign[slot.key];
736
+ if (a?.segmentId) {
737
+ autoEntries.push({ targetKey: slot.key, segmentId: a.segmentId, transformRule: a.transformRule ?? slot.defaultTransform ?? null });
738
+ }
739
+ }
740
+ await onSave(hierEntries, autoEntries);
741
+ }
742
+ void transformRules;
743
+ return /* @__PURE__ */ jsxs4("div", { className: "cfi-vi cfi-vi-mapper", children: [
744
+ /* @__PURE__ */ jsxs4("div", { className: "cfi-vi-mapper-col", children: [
745
+ /* @__PURE__ */ jsxs4("section", { className: "cfi-vi-mapper-section", children: [
746
+ /* @__PURE__ */ jsxs4("div", { className: "cfi-vi-mapper-section-head", children: [
747
+ /* @__PURE__ */ jsx4("h3", { className: "cfi-vi-mapper-h3", children: "Hierarchy Tree" }),
748
+ /* @__PURE__ */ jsx4("span", { className: "cfi-vi-mapper-note", children: isSparse ? "Drag chips onto whichever levels your file actually has. Empty levels are fine." : "Drag a chip onto a level. L(n) needs L(n\u22121) first." })
749
+ ] }),
750
+ /* @__PURE__ */ jsx4("div", { className: "cfi-vi-mapper-box", children: LEVELS.map((level) => /* @__PURE__ */ jsx4(
751
+ HierarchyRow,
752
+ {
753
+ level,
754
+ segId: hierAssign[level] ?? null,
755
+ segmentById,
756
+ isDropTarget: dropTarget === `hier:${level}`,
757
+ onDragOver: () => setDropTarget(`hier:${level}`),
758
+ onDragLeave: () => setDropTarget(null),
759
+ onDrop: () => dragging && dropOnHier(level, dragging),
760
+ onChipDragStart: (segId) => startDrag(`hier:${level}:${segId}`),
761
+ onChipDragEnd: endDrag,
762
+ onChipRemove: () => setHierAssign((p) => ({ ...p, [level]: null })),
763
+ sparse: isSparse,
764
+ baseHint: baseHierarchyHint?.[level]
765
+ },
766
+ level
767
+ )) }),
768
+ hierarchyErrors.length > 0 && /* @__PURE__ */ jsx4("div", { children: hierarchyErrors.map((err) => /* @__PURE__ */ jsxs4("p", { className: "cfi-vi-mapper-err", children: [
769
+ "\u26A0 ",
770
+ err
771
+ ] }, err)) })
772
+ ] }),
773
+ automotiveSlots.length > 0 && /* @__PURE__ */ jsxs4("section", { className: "cfi-vi-mapper-section", children: [
774
+ /* @__PURE__ */ jsxs4("div", { className: "cfi-vi-mapper-section-head", children: [
775
+ /* @__PURE__ */ jsx4("h3", { className: "cfi-vi-mapper-h3", children: "Automotive Fields" }),
776
+ /* @__PURE__ */ jsx4("span", { className: "cfi-vi-mapper-note", children: "Brand / Nameplate / Plant Name come from the hierarchy unless overridden." })
777
+ ] }),
778
+ /* @__PURE__ */ jsx4("div", { className: "cfi-vi-mapper-box", children: automotiveSlots.map((slot) => {
779
+ const covered = hierarchyCoveredKeys.has(slot.key);
780
+ const hierarchySegId = slot.hierarchyCoveredByLevel != null ? hierAssign[slot.hierarchyCoveredByLevel] : null;
781
+ const assignment = autoAssign[slot.key];
782
+ return /* @__PURE__ */ jsx4(
783
+ AutomotiveRow,
784
+ {
785
+ slot,
786
+ covered,
787
+ hierarchySegment: covered && hierarchySegId ? segmentById.get(hierarchySegId) : void 0,
788
+ assignment,
789
+ segmentById,
790
+ transformRuleLabels,
791
+ hierarchyEmpty: slot.hierarchyCoveredByLevel != null && !hierAssign[slot.hierarchyCoveredByLevel],
792
+ isDropTarget: dropTarget === `auto:${slot.key}`,
793
+ onDragOver: () => setDropTarget(`auto:${slot.key}`),
794
+ onDragLeave: () => setDropTarget(null),
795
+ onDrop: () => dragging && dropOnAuto(slot.key, dragging),
796
+ onChipDragStart: (segId) => startDrag(`auto:${slot.key}:${segId}`),
797
+ onChipDragEnd: endDrag,
798
+ onChipRemove: () => clearAuto(slot.key),
799
+ onTransformChange: (rule) => setAutoTransform(slot.key, rule)
800
+ },
801
+ slot.key
802
+ );
803
+ }) }),
804
+ missingAutomotive.length > 0 && /* @__PURE__ */ jsxs4("p", { className: "cfi-vi-mapper-err", children: [
805
+ "\u26A0 Required: ",
806
+ missingAutomotive.join(", ")
807
+ ] })
808
+ ] }),
809
+ /* @__PURE__ */ jsxs4("div", { className: "cfi-vi-mapper-save", children: [
810
+ /* @__PURE__ */ jsx4(Btn, { size: "sm", onClick: handleSave, disabled: !canSave, children: saving ? "Saving\u2026" : "Save Mapping" }),
811
+ isDirty && hierarchyErrors.length === 0 && missingAutomotive.length === 0 && !saving && /* @__PURE__ */ jsx4("span", { className: "cfi-vi-mapper-dirty", children: "Unsaved changes" }),
812
+ !isDirty && /* @__PURE__ */ jsx4("span", { className: "cfi-vi-muted", children: "Up to date" })
813
+ ] })
814
+ ] }),
815
+ /* @__PURE__ */ jsxs4("aside", { className: "cfi-vi-pool-col", children: [
816
+ /* @__PURE__ */ jsxs4("div", { className: "cfi-vi-mapper-section-head", children: [
817
+ /* @__PURE__ */ jsx4("h3", { className: "cfi-vi-mapper-h3", children: "Segment Pool" }),
818
+ /* @__PURE__ */ jsxs4("span", { className: "cfi-vi-mapper-note", children: [
819
+ poolSegments.length,
820
+ " of ",
821
+ segments.length,
822
+ " unused"
823
+ ] })
824
+ ] }),
825
+ /* @__PURE__ */ jsxs4("div", { className: "cfi-vi-search", children: [
826
+ /* @__PURE__ */ jsx4(Search, { size: 14, className: "cfi-vi-search-icon", "aria-hidden": true }),
827
+ /* @__PURE__ */ jsx4(TextInput, { type: "search", placeholder: "Filter segments\u2026", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value) })
828
+ ] }),
829
+ /* @__PURE__ */ jsx4(
830
+ "div",
831
+ {
832
+ className: `cfi-vi-pool ${dropTarget === "pool" ? "cfi-vi-pool--active" : ""}`,
833
+ onDragOver: (e) => {
834
+ e.preventDefault();
835
+ setDropTarget("pool");
836
+ },
837
+ onDragLeave: (e) => {
838
+ if (!e.currentTarget.contains(e.relatedTarget)) setDropTarget(null);
839
+ },
840
+ onDrop: (e) => {
841
+ e.preventDefault();
842
+ if (dragging) dropOnPool(dragging);
843
+ },
844
+ children: segments.length === 0 ? /* @__PURE__ */ jsx4("p", { className: "cfi-vi-pool-empty", children: "No segments detected. Run \u201CDetect segments\u201D from the source page first." }) : poolSegments.length === 0 ? /* @__PURE__ */ jsx4("p", { className: "cfi-vi-pool-empty", children: searchQuery ? "No segments match the filter." : "All segments assigned \u2014 drop a chip here to return it." }) : /* @__PURE__ */ jsx4("div", { className: "cfi-vi-chips", children: poolSegments.map((seg) => /* @__PURE__ */ jsx4(SegmentChip, { label: seg.label, onDragStart: () => startDrag(`pool:${seg.id}`), onDragEnd: endDrag }, seg.id)) })
845
+ }
846
+ ),
847
+ /* @__PURE__ */ jsx4("p", { className: "cfi-vi-mapper-note", children: "Drag chips onto a hierarchy level or an automotive field. Drop a chip back here to unassign." })
848
+ ] })
849
+ ] });
850
+ }
851
+ function HierarchyRow({
852
+ level,
853
+ segId,
854
+ segmentById,
855
+ isDropTarget,
856
+ onDragOver,
857
+ onDragLeave,
858
+ onDrop,
859
+ onChipDragStart,
860
+ onChipDragEnd,
861
+ onChipRemove,
862
+ sparse,
863
+ baseHint
864
+ }) {
865
+ const seg = segId ? segmentById.get(segId) : void 0;
866
+ const isLeaf = level === 6;
867
+ const indent = sparse ? 0 : (level - 1) * 22;
868
+ const levelName = DEFAULT_LEVEL_NAMES[level];
869
+ return /* @__PURE__ */ jsxs4("div", { className: "cfi-vi-hier-row", style: { paddingLeft: `${indent}px` }, children: [
870
+ !sparse && level > 1 && /* @__PURE__ */ jsx4("div", { className: "cfi-vi-hier-elbow" }),
871
+ /* @__PURE__ */ jsx4("div", { className: `cfi-vi-hier-dot ${seg ? "cfi-vi-hier-dot--filled" : ""}` }),
872
+ /* @__PURE__ */ jsx4("span", { className: "cfi-vi-hier-name", children: levelName }),
873
+ isLeaf && !sparse && /* @__PURE__ */ jsx4("span", { className: "cfi-vi-leaf", children: "Leaf" }),
874
+ /* @__PURE__ */ jsx4(
875
+ "div",
876
+ {
877
+ className: `cfi-vi-dropzone ${isDropTarget ? "cfi-vi-dropzone--active" : seg ? "cfi-vi-dropzone--filled" : "cfi-vi-dropzone--empty"}`,
878
+ onDragOver: (e) => {
879
+ e.preventDefault();
880
+ onDragOver();
881
+ },
882
+ onDragLeave: (e) => {
883
+ if (!e.currentTarget.contains(e.relatedTarget)) onDragLeave();
884
+ },
885
+ onDrop: (e) => {
886
+ e.preventDefault();
887
+ onDrop();
888
+ },
889
+ children: seg ? /* @__PURE__ */ jsx4(AssignedChip, { seg, onDragStart: () => onChipDragStart(seg.id), onDragEnd: onChipDragEnd, onRemove: onChipRemove }) : /* @__PURE__ */ jsx4("span", { className: "cfi-vi-dropzone-ph", children: isDropTarget ? "Drop here" : "Drop a segment\u2026" })
890
+ }
891
+ ),
892
+ baseHint && /* @__PURE__ */ jsxs4("span", { className: "cfi-vi-base-hint", title: `Base source uses "${baseHint}" on ${levelName}. Match here for the overlay to join.`, children: [
893
+ "base: ",
894
+ baseHint
895
+ ] })
896
+ ] });
897
+ }
898
+ function AutomotiveRow({
899
+ slot,
900
+ covered,
901
+ hierarchySegment,
902
+ assignment,
903
+ segmentById,
904
+ transformRuleLabels,
905
+ hierarchyEmpty,
906
+ isDropTarget,
907
+ onDragOver,
908
+ onDragLeave,
909
+ onDrop,
910
+ onChipDragStart,
911
+ onChipDragEnd,
912
+ onChipRemove
913
+ }) {
914
+ const assignedSeg = assignment?.segmentId ? segmentById.get(assignment.segmentId) : void 0;
915
+ const requiredBadge = (slot.required || hierarchyEmpty) && !covered && !assignedSeg;
916
+ return /* @__PURE__ */ jsxs4("div", { className: "cfi-vi-auto-row", children: [
917
+ /* @__PURE__ */ jsxs4("div", { className: "cfi-vi-auto-label", children: [
918
+ /* @__PURE__ */ jsxs4("div", { className: "cfi-vi-row", children: [
919
+ /* @__PURE__ */ jsx4("span", { className: "cfi-vi-auto-label-text", children: slot.label }),
920
+ requiredBadge && /* @__PURE__ */ jsx4(Badge, { variant: "outline", children: "required" })
921
+ ] }),
922
+ slot.hierarchyCoveredByLevel != null && /* @__PURE__ */ jsxs4("p", { className: `cfi-vi-auto-cover ${covered ? "cfi-vi-auto-cover--on" : ""}`, children: [
923
+ covered ? "\u2713 " : "",
924
+ "From L",
925
+ slot.hierarchyCoveredByLevel,
926
+ " hierarchy"
927
+ ] })
928
+ ] }),
929
+ /* @__PURE__ */ jsx4(
930
+ "div",
931
+ {
932
+ className: `cfi-vi-dropzone ${isDropTarget ? "cfi-vi-dropzone--active" : assignedSeg ? "cfi-vi-dropzone--filled" : covered ? "cfi-vi-dropzone--covered" : "cfi-vi-dropzone--empty"}`,
933
+ onDragOver: (e) => {
934
+ e.preventDefault();
935
+ onDragOver();
936
+ },
937
+ onDragLeave: (e) => {
938
+ if (!e.currentTarget.contains(e.relatedTarget)) onDragLeave();
939
+ },
940
+ onDrop: (e) => {
941
+ e.preventDefault();
942
+ onDrop();
943
+ },
944
+ children: assignedSeg ? /* @__PURE__ */ jsx4(AssignedChip, { seg: assignedSeg, onDragStart: () => onChipDragStart(assignedSeg.id), onDragEnd: onChipDragEnd, onRemove: onChipRemove }) : covered && hierarchySegment ? /* @__PURE__ */ jsxs4("div", { className: "cfi-vi-auto-auto", title: "Auto-supplied from hierarchy. Drop a different chip here to override.", children: [
945
+ /* @__PURE__ */ jsx4(CheckCircle22, { size: 14, "aria-hidden": true }),
946
+ /* @__PURE__ */ jsx4("span", { className: "cfi-vi-auto-auto-label", children: hierarchySegment.label }),
947
+ /* @__PURE__ */ jsx4("span", { className: "cfi-vi-auto-auto-tag", children: "auto" })
948
+ ] }) : /* @__PURE__ */ jsx4("span", { className: "cfi-vi-dropzone-ph", children: isDropTarget ? "Drop here" : "Drop a segment\u2026" })
949
+ }
950
+ ),
951
+ slot.defaultTransform && !covered && /* @__PURE__ */ jsxs4("div", { className: "cfi-vi-auto-transform", children: [
952
+ /* @__PURE__ */ jsx4("span", { className: "cfi-vi-auto-transform-label", children: "Transform:" }),
953
+ " ",
954
+ transformRuleLabels[slot.defaultTransform] ?? slot.defaultTransform
955
+ ] })
956
+ ] });
957
+ }
958
+ function AssignedChip({
959
+ seg,
960
+ onDragStart,
961
+ onDragEnd,
962
+ onRemove
963
+ }) {
964
+ return /* @__PURE__ */ jsxs4("div", { draggable: true, onDragStart, onDragEnd, className: "cfi-vi-chip--assigned", children: [
965
+ /* @__PURE__ */ jsx4("div", { className: "cfi-vi-chip-dot cfi-vi-chip-dot--on" }),
966
+ /* @__PURE__ */ jsx4("span", { className: "cfi-vi-chip-label", children: seg.label }),
967
+ /* @__PURE__ */ jsx4("button", { type: "button", onClick: (e) => {
968
+ e.stopPropagation();
969
+ onRemove();
970
+ }, className: "cfi-vi-chip-x", title: "Remove \u2014 returns to pool", children: "\xD7" })
971
+ ] });
972
+ }
973
+ function SegmentChip({ label, onDragStart, onDragEnd }) {
974
+ return /* @__PURE__ */ jsxs4("div", { draggable: true, onDragStart, onDragEnd, className: "cfi-vi-chip", children: [
975
+ /* @__PURE__ */ jsx4("div", { className: "cfi-vi-chip-dot" }),
976
+ label
977
+ ] });
978
+ }
979
+
980
+ // src/VolumeSourceDetail.tsx
981
+ import { useEffect as useEffect6, useRef as useRef2, useState as useState6 } from "react";
982
+ import { KeyRound, Lock, Rows, Columns, AlertTriangle as AlertTriangle2, Layers } from "lucide-react";
983
+
984
+ // src/TallLayoutMappings.tsx
985
+ import { useEffect as useEffect4, useMemo as useMemo3, useState as useState4 } from "react";
986
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
987
+ var NONE = "__none__";
988
+ function TallLayoutMappings({ transport, source, locked }) {
989
+ const [loading, setLoading] = useState4(true);
990
+ const [saving, setSaving] = useState4(null);
991
+ const [error, setError] = useState4(null);
992
+ const [periodConfigured, setPeriodConfigured] = useState4(false);
993
+ const [valueConfigured, setValueConfigured] = useState4(false);
994
+ const [periodSegmentId, setPeriodSegmentId] = useState4(NONE);
995
+ const [valueSegmentId, setValueSegmentId] = useState4(NONE);
996
+ async function load() {
997
+ try {
998
+ setLoading(true);
999
+ const period = await transport.listSegmentMapping(source.id, "period");
1000
+ const value = await transport.listSegmentMapping(source.id, "value");
1001
+ const periodRow = period.items.find((r) => r.targetCategory === "period" && r.targetKey === "week_date");
1002
+ const valueRow = value.items.find((r) => r.targetCategory === "value" && r.targetKey === "quantity");
1003
+ setPeriodSegmentId(periodRow?.segmentId ?? NONE);
1004
+ setValueSegmentId(valueRow?.segmentId ?? NONE);
1005
+ setPeriodConfigured(!!periodRow);
1006
+ setValueConfigured(!!valueRow);
1007
+ } catch (e) {
1008
+ setError(e instanceof Error ? e.message : "Failed to load mappings");
1009
+ } finally {
1010
+ setLoading(false);
1011
+ }
1012
+ }
1013
+ useEffect4(() => {
1014
+ void load();
1015
+ }, [source.id]);
1016
+ const sortedSegments = useMemo3(
1017
+ () => source.segments.slice().sort((a, b) => a.label.localeCompare(b.label)),
1018
+ [source.segments]
1019
+ );
1020
+ async function saveCategory(category, segId, targetKey) {
1021
+ if (locked) return;
1022
+ setSaving(category);
1023
+ setError(null);
1024
+ try {
1025
+ const entries = segId === NONE ? [] : [{ targetKey, segmentId: segId }];
1026
+ await transport.replaceSegmentMappingCategory(source.id, category, entries);
1027
+ await load();
1028
+ } catch (e) {
1029
+ setError(e instanceof Error ? e.message : "Failed to save mapping");
1030
+ } finally {
1031
+ setSaving(null);
1032
+ }
1033
+ }
1034
+ if (loading) return /* @__PURE__ */ jsx5("p", { className: "cfi-vi-muted", children: "Loading tall-layout mappings\u2026" });
1035
+ const ready = periodConfigured && valueConfigured;
1036
+ return /* @__PURE__ */ jsxs5("section", { className: "cfi-vi-panel", children: [
1037
+ /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-row cfi-vi-row--between", children: [
1038
+ /* @__PURE__ */ jsx5("h2", { className: "cfi-vi-h2", children: "Tall-Layout Mappings" }),
1039
+ /* @__PURE__ */ jsx5("span", { style: { fontSize: "var(--cfi-font-size-xs, 12px)", color: ready ? "#16a34a" : "#b45309" }, children: ready ? "Ready for upload" : "Missing period or value mapping" })
1040
+ ] }),
1041
+ /* @__PURE__ */ jsx5("p", { className: "cfi-vi-muted", children: "Tall sources need to know which file column carries the period date and the volume value." }),
1042
+ error && /* @__PURE__ */ jsx5("p", { className: "cfi-vi-error", children: error }),
1043
+ /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-field", children: [
1044
+ /* @__PURE__ */ jsx5("label", { className: "cfi-vi-label", children: "Period column (Week Ending)" }),
1045
+ /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-row", children: [
1046
+ /* @__PURE__ */ jsxs5(
1047
+ "select",
1048
+ {
1049
+ className: "cfi-vi-input",
1050
+ value: periodSegmentId,
1051
+ disabled: locked || saving !== null,
1052
+ onChange: (e) => setPeriodSegmentId(e.target.value),
1053
+ children: [
1054
+ /* @__PURE__ */ jsx5("option", { value: NONE, children: "\u2014 Not mapped \u2014" }),
1055
+ sortedSegments.map((s) => /* @__PURE__ */ jsx5("option", { value: s.id, children: s.label }, s.id))
1056
+ ]
1057
+ }
1058
+ ),
1059
+ /* @__PURE__ */ jsx5(Btn, { size: "sm", onClick: () => void saveCategory("period", periodSegmentId, "week_date"), disabled: locked || saving !== null, children: saving === "period" ? "Saving\u2026" : "Save" })
1060
+ ] })
1061
+ ] }),
1062
+ /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-field", children: [
1063
+ /* @__PURE__ */ jsx5("label", { className: "cfi-vi-label", children: "Quantity column (Volume)" }),
1064
+ /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-row", children: [
1065
+ /* @__PURE__ */ jsxs5(
1066
+ "select",
1067
+ {
1068
+ className: "cfi-vi-input",
1069
+ value: valueSegmentId,
1070
+ disabled: locked || saving !== null,
1071
+ onChange: (e) => setValueSegmentId(e.target.value),
1072
+ children: [
1073
+ /* @__PURE__ */ jsx5("option", { value: NONE, children: "\u2014 Not mapped \u2014" }),
1074
+ sortedSegments.map((s) => /* @__PURE__ */ jsx5("option", { value: s.id, children: s.label }, s.id))
1075
+ ]
1076
+ }
1077
+ ),
1078
+ /* @__PURE__ */ jsx5(Btn, { size: "sm", onClick: () => void saveCategory("value", valueSegmentId, "quantity"), disabled: locked || saving !== null, children: saving === "value" ? "Saving\u2026" : "Save" })
1079
+ ] })
1080
+ ] })
1081
+ ] });
1082
+ }
1083
+
1084
+ // src/ValueAliasesCard.tsx
1085
+ import { useEffect as useEffect5, useMemo as useMemo4, useState as useState5 } from "react";
1086
+ import { Plus, Trash2, ArrowRight, Save, RotateCw } from "lucide-react";
1087
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1088
+ function ValueAliasesCard({ transport, sourceId, locked, isOverlay }) {
1089
+ const [selectedLevel, setSelectedLevel] = useState5(4);
1090
+ const [aliases, setAliases] = useState5([]);
1091
+ const [draft, setDraft] = useState5([]);
1092
+ const [saving, setSaving] = useState5(false);
1093
+ const [error, setError] = useState5(null);
1094
+ const [loading, setLoading] = useState5(true);
1095
+ const [relinking, setRelinking] = useState5(false);
1096
+ const [relinkResult, setRelinkResult] = useState5(null);
1097
+ useEffect5(() => {
1098
+ let cancelled = false;
1099
+ setLoading(true);
1100
+ void transport.listValueAliases(sourceId).then((items) => {
1101
+ if (!cancelled) {
1102
+ setAliases(items);
1103
+ setError(null);
1104
+ }
1105
+ }).catch((e) => {
1106
+ if (!cancelled) setError(e instanceof Error ? e.message : "Failed to load aliases");
1107
+ }).finally(() => {
1108
+ if (!cancelled) setLoading(false);
1109
+ });
1110
+ return () => {
1111
+ cancelled = true;
1112
+ };
1113
+ }, [sourceId, transport]);
1114
+ useEffect5(() => {
1115
+ setDraft(
1116
+ aliases.filter((a) => a.level === selectedLevel).map((a) => ({ id: a.id, rawValue: a.rawValue, canonicalValue: a.canonicalValue }))
1117
+ );
1118
+ }, [aliases, selectedLevel]);
1119
+ const isDirty = useMemo4(() => {
1120
+ const saved = aliases.filter((a) => a.level === selectedLevel).map((a) => `${a.rawValue}${a.canonicalValue}`).sort();
1121
+ const current = draft.filter((d) => d.rawValue.trim() && d.canonicalValue.trim()).map((d) => `${d.rawValue.trim().toUpperCase()}${d.canonicalValue.trim().toUpperCase()}`).sort();
1122
+ if (saved.length !== current.length) return true;
1123
+ for (let i = 0; i < saved.length; i++) if (saved[i] !== current[i]) return true;
1124
+ return false;
1125
+ }, [aliases, draft, selectedLevel]);
1126
+ const addRow = () => setDraft((p) => [...p, { id: null, rawValue: "", canonicalValue: "" }]);
1127
+ const updateRow = (i, patch) => setDraft((p) => p.map((r, idx) => idx === i ? { ...r, ...patch } : r));
1128
+ const removeRow = (i) => setDraft((p) => p.filter((_, idx) => idx !== i));
1129
+ async function handleRelink() {
1130
+ setRelinking(true);
1131
+ setError(null);
1132
+ setRelinkResult(null);
1133
+ try {
1134
+ const r = await transport.relinkOverlaySource(sourceId);
1135
+ setRelinkResult(`Re-linked ${r.totalOverlay} overlay rows: ${r.linked} linked, ${r.unlinked} unlinked, ${r.ambiguous} multi-linked.`);
1136
+ } catch (e) {
1137
+ setError(e instanceof Error ? e.message : "Re-link failed");
1138
+ } finally {
1139
+ setRelinking(false);
1140
+ }
1141
+ }
1142
+ async function handleSave() {
1143
+ setSaving(true);
1144
+ setError(null);
1145
+ try {
1146
+ const entries = draft.filter((d) => d.rawValue.trim() && d.canonicalValue.trim()).map((d) => ({ rawValue: d.rawValue.trim().toUpperCase(), canonicalValue: d.canonicalValue.trim().toUpperCase() }));
1147
+ const updated = await transport.replaceLevelValueAliases(sourceId, selectedLevel, entries);
1148
+ setAliases((prev) => [...prev.filter((a) => a.level !== selectedLevel), ...updated]);
1149
+ } catch (e) {
1150
+ setError(e instanceof Error ? e.message : "Failed to save aliases");
1151
+ } finally {
1152
+ setSaving(false);
1153
+ }
1154
+ }
1155
+ if (loading) return /* @__PURE__ */ jsx6("p", { className: "cfi-vi-muted", children: "Loading aliases\u2026" });
1156
+ return /* @__PURE__ */ jsxs6("div", { className: "cfi-vi-aliases", children: [
1157
+ /* @__PURE__ */ jsxs6("div", { className: "cfi-vi-row cfi-vi-row--between", children: [
1158
+ /* @__PURE__ */ jsxs6("div", { children: [
1159
+ /* @__PURE__ */ jsx6("h2", { className: "cfi-vi-h2", children: "Value Aliases" }),
1160
+ /* @__PURE__ */ jsxs6("p", { className: "cfi-vi-muted", style: { fontSize: "var(--cfi-font-size-xs, 11px)" }, children: [
1161
+ "Normalise level values when this source uses different vocabulary than the base/overlay it joins to (e.g. ",
1162
+ /* @__PURE__ */ jsx6("span", { className: "cfi-vi-mono", children: '"MICHIGAN" \u2192 "MICHIGAN ASSEMBLY"' }),
1163
+ "). Applied at match time \u2014 no re-upload."
1164
+ ] })
1165
+ ] }),
1166
+ /* @__PURE__ */ jsxs6("div", { className: "cfi-vi-field", style: { minWidth: 140 }, children: [
1167
+ /* @__PURE__ */ jsx6("label", { className: "cfi-vi-label", children: "Level" }),
1168
+ /* @__PURE__ */ jsx6("select", { className: "cfi-vi-input", value: String(selectedLevel), onChange: (e) => setSelectedLevel(parseInt(e.target.value, 10)), children: [1, 2, 3, 4, 5, 6].map((n) => /* @__PURE__ */ jsxs6("option", { value: n, children: [
1169
+ "L",
1170
+ n,
1171
+ " \u2014 ",
1172
+ DEFAULT_LEVEL_NAMES[n]
1173
+ ] }, n)) })
1174
+ ] })
1175
+ ] }),
1176
+ /* @__PURE__ */ jsxs6("div", { className: "cfi-vi-panel", children: [
1177
+ draft.length === 0 ? /* @__PURE__ */ jsxs6("p", { className: "cfi-vi-muted", style: { fontStyle: "italic", fontSize: "var(--cfi-font-size-xs, 12px)" }, children: [
1178
+ "No aliases at L",
1179
+ selectedLevel,
1180
+ " (",
1181
+ DEFAULT_LEVEL_NAMES[selectedLevel],
1182
+ "). Click ",
1183
+ /* @__PURE__ */ jsx6("strong", { children: "+ Add alias" }),
1184
+ " to create one."
1185
+ ] }) : /* @__PURE__ */ jsx6("div", { className: "cfi-vi-alias-rows", children: draft.map((row, i) => /* @__PURE__ */ jsxs6("div", { className: "cfi-vi-row", children: [
1186
+ /* @__PURE__ */ jsx6(
1187
+ "input",
1188
+ {
1189
+ className: "cfi-vi-input cfi-vi-mono",
1190
+ type: "text",
1191
+ value: row.rawValue,
1192
+ disabled: locked,
1193
+ placeholder: "Raw value (e.g. MICHIGAN)",
1194
+ onChange: (e) => updateRow(i, { rawValue: e.target.value })
1195
+ }
1196
+ ),
1197
+ /* @__PURE__ */ jsx6(ArrowRight, { size: 14, className: "cfi-vi-muted", "aria-hidden": true }),
1198
+ /* @__PURE__ */ jsx6(
1199
+ "input",
1200
+ {
1201
+ className: "cfi-vi-input cfi-vi-mono",
1202
+ type: "text",
1203
+ value: row.canonicalValue,
1204
+ disabled: locked,
1205
+ placeholder: "Canonical (e.g. MICHIGAN ASSEMBLY)",
1206
+ onChange: (e) => updateRow(i, { canonicalValue: e.target.value })
1207
+ }
1208
+ ),
1209
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "cfi-vi-icon-btn", onClick: () => removeRow(i), disabled: locked, title: "Remove row", children: /* @__PURE__ */ jsx6(Trash2, { size: 14, "aria-hidden": true }) })
1210
+ ] }, i)) }),
1211
+ /* @__PURE__ */ jsxs6("div", { className: "cfi-vi-row", style: { flexWrap: "wrap", paddingTop: 4 }, children: [
1212
+ /* @__PURE__ */ jsxs6(Btn, { variant: "outline", size: "sm", onClick: addRow, disabled: locked, children: [
1213
+ /* @__PURE__ */ jsx6(Plus, { size: 14, "aria-hidden": true }),
1214
+ " Add alias"
1215
+ ] }),
1216
+ /* @__PURE__ */ jsxs6(Btn, { size: "sm", onClick: () => void handleSave(), disabled: !isDirty || saving || locked, children: [
1217
+ /* @__PURE__ */ jsx6(Save, { size: 14, "aria-hidden": true }),
1218
+ " ",
1219
+ saving ? "Saving\u2026" : "Save"
1220
+ ] }),
1221
+ isDirty && !saving && /* @__PURE__ */ jsx6("span", { className: "cfi-vi-mapper-dirty", children: "Unsaved changes" }),
1222
+ !isDirty && !saving && draft.length > 0 && /* @__PURE__ */ jsx6("span", { className: "cfi-vi-muted", children: "Up to date" }),
1223
+ isOverlay && /* @__PURE__ */ jsxs6(
1224
+ Btn,
1225
+ {
1226
+ variant: "outline",
1227
+ size: "sm",
1228
+ onClick: () => void handleRelink(),
1229
+ disabled: relinking || isDirty,
1230
+ title: isDirty ? "Save aliases first, then re-link" : "Re-run overlay \u2192 base resolution against current aliases",
1231
+ children: [
1232
+ /* @__PURE__ */ jsx6(RotateCw, { size: 14, className: relinking ? "cfi-vi-spin" : "", "aria-hidden": true }),
1233
+ " ",
1234
+ relinking ? "Re-linking\u2026" : "Re-link overlay"
1235
+ ]
1236
+ }
1237
+ )
1238
+ ] }),
1239
+ error && /* @__PURE__ */ jsx6("p", { className: "cfi-vi-error", children: error }),
1240
+ relinkResult && /* @__PURE__ */ jsx6("p", { style: { fontSize: "var(--cfi-font-size-xs, 12px)", color: "#15803d" }, children: relinkResult })
1241
+ ] })
1242
+ ] });
1243
+ }
1244
+
1245
+ // src/VolumeSourceDetail.tsx
1246
+ import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1247
+ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, className }) {
1248
+ const id = sourceId;
1249
+ const [source, setSource] = useState6(null);
1250
+ const [allSources, setAllSources] = useState6([]);
1251
+ const [loading, setLoading] = useState6(true);
1252
+ const [error, setError] = useState6(null);
1253
+ const [detecting, setDetecting] = useState6(false);
1254
+ const [detectMsg, setDetectMsg] = useState6(null);
1255
+ const [savingKey, setSavingKey] = useState6(false);
1256
+ const [selectedKeyId, setSelectedKeyId] = useState6("__none__");
1257
+ const [hasData, setHasData] = useState6(false);
1258
+ const fileRef = useRef2(null);
1259
+ const [savingLayout, setSavingLayout] = useState6(false);
1260
+ const [editLayout, setEditLayout] = useState6("wide");
1261
+ const [editValueDateFormat, setEditValueDateFormat] = useState6("yyyy-MM-dd");
1262
+ const [newSegmentLabel, setNewSegmentLabel] = useState6("");
1263
+ const [newSegmentDataType, setNewSegmentDataType] = useState6("string");
1264
+ const [addingSegment, setAddingSegment] = useState6(false);
1265
+ const [clearConfirm, setClearConfirm] = useState6("");
1266
+ const [clearing, setClearing] = useState6(false);
1267
+ const [clearResult, setClearResult] = useState6(null);
1268
+ const [mappingKeys, setMappingKeys] = useState6(null);
1269
+ const [mappingRows, setMappingRows] = useState6([]);
1270
+ const [savingMapping, setSavingMapping] = useState6(false);
1271
+ const [baseHierarchyHint, setBaseHierarchyHint] = useState6({});
1272
+ async function load() {
1273
+ if (!id) return;
1274
+ try {
1275
+ setLoading(true);
1276
+ const [src, uploads, keys, mapping, sources] = await Promise.all([
1277
+ transport.getSource(id),
1278
+ transport.listUploads({ sourceId: id, limit: 1 }),
1279
+ transport.getSegmentMappingKeys(),
1280
+ transport.listSegmentMapping(id),
1281
+ transport.listSources()
1282
+ ]);
1283
+ setAllSources(sources);
1284
+ setSource(src);
1285
+ setSelectedKeyId(src.uniqueKeySegmentId ?? "__none__");
1286
+ setEditLayout(src.layout);
1287
+ setEditValueDateFormat(src.valueDateFormat);
1288
+ setHasData(uploads.items.some((u) => u.status === "done" || u.status === "done_with_errors"));
1289
+ setMappingKeys(keys);
1290
+ setMappingRows(mapping.items);
1291
+ if (src.kind === "overlay") {
1292
+ try {
1293
+ const tenantConfig = await transport.getTenantConfig();
1294
+ if (tenantConfig?.sourceId && tenantConfig.sourceId !== id) {
1295
+ const [baseSrc, baseMapping] = await Promise.all([
1296
+ transport.getSource(tenantConfig.sourceId),
1297
+ transport.listSegmentMapping(tenantConfig.sourceId)
1298
+ ]);
1299
+ const baseSegLabel = new Map(baseSrc.segments.map((s) => [s.id, s.label]));
1300
+ const hint = {};
1301
+ for (const row of baseMapping.items) {
1302
+ if (row.targetCategory !== "hierarchy_level") continue;
1303
+ const m = /^level(\d)$/.exec(row.targetKey);
1304
+ if (!m) continue;
1305
+ const label = baseSegLabel.get(row.segmentId);
1306
+ if (label) hint[parseInt(m[1], 10)] = label;
1307
+ }
1308
+ setBaseHierarchyHint(hint);
1309
+ } else {
1310
+ setBaseHierarchyHint({});
1311
+ }
1312
+ } catch {
1313
+ setBaseHierarchyHint({});
1314
+ }
1315
+ } else {
1316
+ setBaseHierarchyHint({});
1317
+ }
1318
+ } catch (e) {
1319
+ setError(e instanceof Error ? e.message : "Failed to load source");
1320
+ } finally {
1321
+ setLoading(false);
1322
+ }
1323
+ }
1324
+ useEffect6(() => {
1325
+ void load();
1326
+ }, [id]);
1327
+ async function handleDetect(file) {
1328
+ if (!id) return;
1329
+ setDetecting(true);
1330
+ setDetectMsg(null);
1331
+ setError(null);
1332
+ try {
1333
+ const result = await transport.detectSegments(id, file);
1334
+ setDetectMsg(`Detected ${result.detected} segments (skipped ${result.skipped} volume/summary columns)`);
1335
+ await load();
1336
+ } catch (e) {
1337
+ setError(e instanceof Error ? e.message : "Detect failed");
1338
+ } finally {
1339
+ setDetecting(false);
1340
+ }
1341
+ }
1342
+ async function handleDelete(segmentId) {
1343
+ try {
1344
+ await transport.deleteSegment(segmentId);
1345
+ await load();
1346
+ } catch (e) {
1347
+ setError(e instanceof Error ? e.message : "Delete failed");
1348
+ }
1349
+ }
1350
+ async function handleSaveMapping(hierarchy, automotive) {
1351
+ if (!id) return;
1352
+ setSavingMapping(true);
1353
+ setError(null);
1354
+ try {
1355
+ const hResp = await transport.replaceSegmentMappingCategory(id, "hierarchy_level", hierarchy);
1356
+ const aResp = await transport.replaceSegmentMappingCategory(id, "automotive_field", automotive);
1357
+ setMappingRows([...hResp.items, ...aResp.items]);
1358
+ } catch (e) {
1359
+ setError(e instanceof Error ? e.message : "Failed to save mapping");
1360
+ } finally {
1361
+ setSavingMapping(false);
1362
+ }
1363
+ }
1364
+ async function handleClearData() {
1365
+ if (!id) return;
1366
+ setClearing(true);
1367
+ setError(null);
1368
+ setClearResult(null);
1369
+ try {
1370
+ const result = await transport.clearSourceData(id, clearConfirm);
1371
+ setClearResult(`Cleared ${result.criteria.toLocaleString()} criteria and ${result.uploads.toLocaleString()} uploads. Configuration (segments, mappings) preserved.`);
1372
+ setClearConfirm("");
1373
+ await load();
1374
+ } catch (e) {
1375
+ setError(e instanceof Error ? e.message : "Clear failed");
1376
+ } finally {
1377
+ setClearing(false);
1378
+ }
1379
+ }
1380
+ async function handleAddSegment() {
1381
+ if (!id || !source) return;
1382
+ const label = newSegmentLabel.trim();
1383
+ if (!label) return;
1384
+ setAddingSegment(true);
1385
+ setError(null);
1386
+ try {
1387
+ const nextOrder = source.segments.length === 0 ? 0 : Math.max(...source.segments.map((s) => s.sortOrder)) + 1;
1388
+ await transport.createSegment(id, { label, dataType: newSegmentDataType, sortOrder: nextOrder });
1389
+ setNewSegmentLabel("");
1390
+ setNewSegmentDataType("string");
1391
+ await load();
1392
+ } catch (e) {
1393
+ setError(e instanceof Error ? e.message : "Add segment failed");
1394
+ } finally {
1395
+ setAddingSegment(false);
1396
+ }
1397
+ }
1398
+ async function saveUniqueKey() {
1399
+ if (!id) return;
1400
+ setSavingKey(true);
1401
+ try {
1402
+ await transport.updateSource(id, { uniqueKeySegmentId: selectedKeyId === "__none__" ? null : selectedKeyId });
1403
+ await load();
1404
+ } catch (e) {
1405
+ setError(e instanceof Error ? e.message : "Failed to save key");
1406
+ } finally {
1407
+ setSavingKey(false);
1408
+ }
1409
+ }
1410
+ async function saveLayoutAndRouting() {
1411
+ if (!id) return;
1412
+ setSavingLayout(true);
1413
+ setError(null);
1414
+ try {
1415
+ await transport.updateSource(id, { layout: editLayout, valueDateFormat: editValueDateFormat });
1416
+ await load();
1417
+ } catch (e) {
1418
+ setError(e instanceof Error ? e.message : "Failed to save layout");
1419
+ } finally {
1420
+ setSavingLayout(false);
1421
+ }
1422
+ }
1423
+ if (loading) return /* @__PURE__ */ jsx7(Root, { className, children: /* @__PURE__ */ jsx7("p", { className: "cfi-vi-muted", children: "Loading\u2026" }) });
1424
+ if (!source) return /* @__PURE__ */ jsx7(Root, { className, children: /* @__PURE__ */ jsx7("p", { className: "cfi-vi-error", children: "Source not found" }) });
1425
+ const keySegment = source.segments.find((s) => s.id === source.uniqueKeySegmentId);
1426
+ const isOverlay = source.kind === "overlay";
1427
+ return /* @__PURE__ */ jsx7(Root, { className, children: /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-detail", children: [
1428
+ /* @__PURE__ */ jsxs7("div", { children: [
1429
+ /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-row", style: { gap: 8 }, children: [
1430
+ /* @__PURE__ */ jsx7("span", { className: "cfi-vi-muted", style: { fontSize: "var(--cfi-font-size-xs, 12px)" }, children: "Source:" }),
1431
+ /* @__PURE__ */ jsx7(
1432
+ "select",
1433
+ {
1434
+ className: "cfi-vi-input",
1435
+ style: { width: "auto" },
1436
+ value: source.id,
1437
+ onChange: (e) => {
1438
+ if (e.target.value !== source.id) onOpenSource(e.target.value);
1439
+ },
1440
+ children: allSources.slice().sort((a, b) => a.name.localeCompare(b.name)).map((s) => /* @__PURE__ */ jsxs7("option", { value: s.id, children: [
1441
+ s.name,
1442
+ " (",
1443
+ s.code,
1444
+ ")",
1445
+ s.kind === "overlay" ? " \xB7 overlay" : ""
1446
+ ] }, s.id))
1447
+ }
1448
+ ),
1449
+ onBack && /* @__PURE__ */ jsx7("button", { type: "button", className: "cfi-vi-link cfi-vi-muted", style: { fontSize: "var(--cfi-font-size-xs, 12px)" }, onClick: onBack, children: "All sources \u2192" })
1450
+ ] }),
1451
+ /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-row", style: { flexWrap: "wrap", marginTop: 4 }, children: [
1452
+ /* @__PURE__ */ jsx7("h1", { className: "cfi-vi-h1", children: source.name }),
1453
+ /* @__PURE__ */ jsx7(Badge, { variant: "outline", children: source.code }),
1454
+ isOverlay && /* @__PURE__ */ jsx7(Badge, { variant: "purple", children: "Overlay" })
1455
+ ] }),
1456
+ /* @__PURE__ */ jsxs7("p", { className: "cfi-vi-muted", style: { fontSize: "var(--cfi-font-size-xs, 12px)", marginTop: 2 }, children: [
1457
+ "Header row ",
1458
+ source.headerRowIndex,
1459
+ " \xB7 Format ",
1460
+ /* @__PURE__ */ jsx7("span", { className: "cfi-vi-mono", children: source.volumeHeaderFormat }),
1461
+ " \xB7",
1462
+ " ",
1463
+ source.layout === "tall" ? /* @__PURE__ */ jsxs7("span", { className: "cfi-vi-row", style: { display: "inline-flex", gap: 4 }, children: [
1464
+ /* @__PURE__ */ jsx7(Rows, { size: 12, "aria-hidden": true }),
1465
+ " tall"
1466
+ ] }) : /* @__PURE__ */ jsxs7("span", { className: "cfi-vi-row", style: { display: "inline-flex", gap: 4 }, children: [
1467
+ /* @__PURE__ */ jsx7(Columns, { size: 12, "aria-hidden": true }),
1468
+ " wide"
1469
+ ] }),
1470
+ source.layout === "tall" && /* @__PURE__ */ jsxs7(Fragment2, { children: [
1471
+ " \xB7 Period ",
1472
+ /* @__PURE__ */ jsx7("span", { className: "cfi-vi-mono", children: source.valueDateFormat })
1473
+ ] }),
1474
+ source.description && /* @__PURE__ */ jsxs7(Fragment2, { children: [
1475
+ " \xB7 ",
1476
+ source.description
1477
+ ] })
1478
+ ] })
1479
+ ] }),
1480
+ hasData && /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-lock", children: [
1481
+ /* @__PURE__ */ jsx7(Lock, { size: 14, "aria-hidden": true }),
1482
+ " This source has loaded data. Configuration is locked \u2014 clear data below before making changes."
1483
+ ] }),
1484
+ /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-grid2", children: [
1485
+ /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-panel", children: [
1486
+ /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-row", children: [
1487
+ editLayout === "tall" ? /* @__PURE__ */ jsx7(Rows, { size: 16, className: "cfi-vi-muted", "aria-hidden": true }) : /* @__PURE__ */ jsx7(Columns, { size: 16, className: "cfi-vi-muted", "aria-hidden": true }),
1488
+ /* @__PURE__ */ jsx7("h2", { className: "cfi-vi-h2", children: "Layout" })
1489
+ ] }),
1490
+ /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-row", style: { alignItems: "flex-end", gap: 12 }, children: [
1491
+ /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-field", style: { flex: 1 }, children: [
1492
+ /* @__PURE__ */ jsx7("label", { className: "cfi-vi-label", children: "Layout" }),
1493
+ /* @__PURE__ */ jsxs7(
1494
+ "select",
1495
+ {
1496
+ className: "cfi-vi-input",
1497
+ value: editLayout,
1498
+ disabled: hasData || savingLayout,
1499
+ onChange: (e) => setEditLayout(e.target.value),
1500
+ children: [
1501
+ /* @__PURE__ */ jsx7("option", { value: "wide", children: "wide" }),
1502
+ /* @__PURE__ */ jsx7("option", { value: "tall", children: "tall" })
1503
+ ]
1504
+ }
1505
+ )
1506
+ ] }),
1507
+ editLayout === "tall" && /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-field", style: { flex: 1 }, children: [
1508
+ /* @__PURE__ */ jsx7("label", { className: "cfi-vi-label", children: "Period format" }),
1509
+ /* @__PURE__ */ jsx7(
1510
+ TextInput,
1511
+ {
1512
+ className: "cfi-vi-mono",
1513
+ value: editValueDateFormat,
1514
+ placeholder: "yyyy-MM-dd",
1515
+ disabled: hasData || savingLayout,
1516
+ onChange: (e) => setEditValueDateFormat(e.target.value)
1517
+ }
1518
+ )
1519
+ ] })
1520
+ ] }),
1521
+ /* @__PURE__ */ jsx7(Btn, { size: "sm", onClick: () => void saveLayoutAndRouting(), disabled: hasData || savingLayout, children: savingLayout ? "Saving\u2026" : "Save layout" })
1522
+ ] }),
1523
+ /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-panel", children: [
1524
+ /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-row", children: [
1525
+ /* @__PURE__ */ jsx7(KeyRound, { size: 16, className: "cfi-vi-muted", "aria-hidden": true }),
1526
+ /* @__PURE__ */ jsx7("h2", { className: "cfi-vi-h2", children: "Unique Key Column" }),
1527
+ keySegment && /* @__PURE__ */ jsx7(Badge, { variant: "secondary", children: keySegment.label })
1528
+ ] }),
1529
+ source.segments.length === 0 ? /* @__PURE__ */ jsx7("p", { className: "cfi-vi-muted", style: { fontSize: "var(--cfi-font-size-xs, 12px)" }, children: "Detect segments first to enable this option." }) : /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-row", style: { alignItems: "flex-end" }, children: [
1530
+ /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-field", style: { flex: 1 }, children: [
1531
+ /* @__PURE__ */ jsx7("label", { className: "cfi-vi-label", children: "Key Segment" }),
1532
+ /* @__PURE__ */ jsxs7(
1533
+ "select",
1534
+ {
1535
+ className: "cfi-vi-input",
1536
+ value: selectedKeyId,
1537
+ disabled: hasData,
1538
+ onChange: (e) => setSelectedKeyId(e.target.value),
1539
+ children: [
1540
+ /* @__PURE__ */ jsx7("option", { value: "__none__", children: "None \u2014 use display name" }),
1541
+ source.segments.slice().sort((a, b) => a.label.localeCompare(b.label)).map((seg) => /* @__PURE__ */ jsx7("option", { value: seg.id, children: seg.label }, seg.id))
1542
+ ]
1543
+ }
1544
+ )
1545
+ ] }),
1546
+ /* @__PURE__ */ jsx7(Btn, { size: "sm", onClick: saveUniqueKey, disabled: savingKey || hasData, children: savingKey ? "Saving\u2026" : "Save" })
1547
+ ] })
1548
+ ] })
1549
+ ] }),
1550
+ source.layout === "tall" && /* @__PURE__ */ jsx7(TallLayoutMappings, { transport, source, locked: hasData }),
1551
+ /* @__PURE__ */ jsxs7("div", { children: [
1552
+ /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-row", children: [
1553
+ /* @__PURE__ */ jsx7(Layers, { size: 16, className: "cfi-vi-muted", "aria-hidden": true }),
1554
+ /* @__PURE__ */ jsx7("h2", { className: "cfi-vi-h2", children: "Segment Mapping" }),
1555
+ isOverlay && /* @__PURE__ */ jsx7("span", { className: "cfi-vi-muted", style: { fontSize: "var(--cfi-font-size-xs, 12px)" }, children: "\xB7 Map at least the levels your base source uses for the overlay merge to land." })
1556
+ ] }),
1557
+ source.segments.length === 0 ? /* @__PURE__ */ jsx7("p", { className: "cfi-vi-muted", children: "No segments yet. Add segments below (Detect from file or Add manually) before mapping." }) : /* @__PURE__ */ jsx7(
1558
+ UnifiedSegmentMapper,
1559
+ {
1560
+ segments: source.segments.slice().sort((a, b) => a.label.localeCompare(b.label)),
1561
+ hierarchyInitial: mappingRows.filter((m) => m.targetCategory === "hierarchy_level"),
1562
+ automotiveInitial: mappingRows.filter((m) => m.targetCategory === "automotive_field"),
1563
+ automotiveSlots: isOverlay ? [] : AUTOMOTIVE_SLOTS,
1564
+ transformRules: mappingKeys?.transformRules ?? [],
1565
+ transformRuleLabels: AUTOMOTIVE_TRANSFORM_LABELS,
1566
+ onSave: handleSaveMapping,
1567
+ saving: savingMapping,
1568
+ hierarchyMode: isOverlay ? "sparse" : "tree",
1569
+ baseHierarchyHint: isOverlay ? baseHierarchyHint : void 0
1570
+ }
1571
+ )
1572
+ ] }),
1573
+ /* @__PURE__ */ jsx7(ValueAliasesCard, { transport, sourceId: source.id, locked: false, isOverlay }),
1574
+ /* @__PURE__ */ jsxs7("div", { children: [
1575
+ /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-row", children: [
1576
+ /* @__PURE__ */ jsxs7("h2", { className: "cfi-vi-h2", children: [
1577
+ "Segments (",
1578
+ source.segments.length,
1579
+ ")"
1580
+ ] }),
1581
+ /* @__PURE__ */ jsx7(Btn, { variant: "outline", size: "sm", onClick: () => fileRef.current?.click(), disabled: detecting || hasData, children: detecting ? "Detecting\u2026" : "Detect from file" }),
1582
+ /* @__PURE__ */ jsx7(
1583
+ "input",
1584
+ {
1585
+ ref: fileRef,
1586
+ type: "file",
1587
+ accept: ".xlsx,.xls",
1588
+ style: { display: "none" },
1589
+ onChange: (e) => {
1590
+ const f = e.target.files?.[0];
1591
+ if (f) void handleDetect(f);
1592
+ e.target.value = "";
1593
+ }
1594
+ }
1595
+ )
1596
+ ] }),
1597
+ detectMsg && /* @__PURE__ */ jsx7("p", { style: { fontSize: "var(--cfi-font-size-sm, 13px)", color: "#16a34a" }, children: detectMsg }),
1598
+ error && /* @__PURE__ */ jsx7("p", { className: "cfi-vi-error", children: error }),
1599
+ /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-panel", style: { maxWidth: "42rem" }, children: [
1600
+ /* @__PURE__ */ jsx7("div", { className: "cfi-vi-muted", style: { fontSize: "var(--cfi-font-size-xs, 12px)" }, children: "Add segment manually \u2014 for columns not covered by auto-detect." }),
1601
+ /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-row", style: { alignItems: "flex-end" }, children: [
1602
+ /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-field", style: { flex: 1 }, children: [
1603
+ /* @__PURE__ */ jsx7("label", { className: "cfi-vi-label", children: "Label" }),
1604
+ /* @__PURE__ */ jsx7(
1605
+ TextInput,
1606
+ {
1607
+ className: "cfi-vi-mono",
1608
+ value: newSegmentLabel,
1609
+ placeholder: "e.g. Color",
1610
+ disabled: hasData || addingSegment,
1611
+ onChange: (e) => setNewSegmentLabel(e.target.value),
1612
+ onKeyDown: (e) => {
1613
+ if (e.key === "Enter" && newSegmentLabel.trim()) void handleAddSegment();
1614
+ }
1615
+ }
1616
+ )
1617
+ ] }),
1618
+ /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-field", style: { width: 128 }, children: [
1619
+ /* @__PURE__ */ jsx7("label", { className: "cfi-vi-label", children: "Data Type" }),
1620
+ /* @__PURE__ */ jsxs7(
1621
+ "select",
1622
+ {
1623
+ className: "cfi-vi-input",
1624
+ value: newSegmentDataType,
1625
+ disabled: hasData || addingSegment,
1626
+ onChange: (e) => setNewSegmentDataType(e.target.value),
1627
+ children: [
1628
+ /* @__PURE__ */ jsx7("option", { value: "string", children: "string" }),
1629
+ /* @__PURE__ */ jsx7("option", { value: "number", children: "number" }),
1630
+ /* @__PURE__ */ jsx7("option", { value: "date", children: "date" })
1631
+ ]
1632
+ }
1633
+ )
1634
+ ] }),
1635
+ /* @__PURE__ */ jsx7(Btn, { size: "sm", onClick: () => void handleAddSegment(), disabled: hasData || addingSegment || !newSegmentLabel.trim(), children: addingSegment ? "Adding\u2026" : "Add segment" })
1636
+ ] })
1637
+ ] }),
1638
+ source.segments.length === 0 ? /* @__PURE__ */ jsx7("p", { className: "cfi-vi-muted", children: "No segments yet. Upload a sample file to auto-detect." }) : /* @__PURE__ */ jsxs7("table", { className: "cfi-vi-table", children: [
1639
+ /* @__PURE__ */ jsx7("thead", { children: /* @__PURE__ */ jsxs7("tr", { children: [
1640
+ /* @__PURE__ */ jsx7("th", { style: { width: 48 }, children: "#" }),
1641
+ /* @__PURE__ */ jsx7("th", { children: "Label" }),
1642
+ /* @__PURE__ */ jsx7("th", { children: "Data Type" }),
1643
+ /* @__PURE__ */ jsx7("th", { style: { width: 80 }, children: "Key" }),
1644
+ /* @__PURE__ */ jsx7("th", { style: { width: 64 } })
1645
+ ] }) }),
1646
+ /* @__PURE__ */ jsx7("tbody", { children: source.segments.map((seg) => /* @__PURE__ */ jsxs7("tr", { children: [
1647
+ /* @__PURE__ */ jsx7("td", { className: "cfi-vi-muted", children: seg.sortOrder }),
1648
+ /* @__PURE__ */ jsx7("td", { className: "cfi-vi-mono", children: seg.label }),
1649
+ /* @__PURE__ */ jsx7("td", { className: "cfi-vi-muted", children: seg.dataType }),
1650
+ /* @__PURE__ */ jsx7("td", { children: seg.id === source.uniqueKeySegmentId && /* @__PURE__ */ jsxs7(Badge, { variant: "secondary", children: [
1651
+ /* @__PURE__ */ jsx7(KeyRound, { size: 12, "aria-hidden": true }),
1652
+ " Key"
1653
+ ] }) }),
1654
+ /* @__PURE__ */ jsx7("td", { children: /* @__PURE__ */ jsx7(
1655
+ "button",
1656
+ {
1657
+ type: "button",
1658
+ className: "cfi-vi-icon-btn",
1659
+ style: { color: "var(--cfi-color-error, #dc2626)" },
1660
+ onClick: () => void handleDelete(seg.id),
1661
+ disabled: hasData,
1662
+ title: "Delete segment",
1663
+ children: "\xD7"
1664
+ }
1665
+ ) })
1666
+ ] }, seg.id)) })
1667
+ ] })
1668
+ ] }),
1669
+ /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-danger", children: [
1670
+ /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-row", style: { color: "var(--cfi-color-error, #dc2626)" }, children: [
1671
+ /* @__PURE__ */ jsx7(AlertTriangle2, { size: 16, "aria-hidden": true }),
1672
+ /* @__PURE__ */ jsx7("h2", { className: "cfi-vi-h2", children: "Danger zone \u2014 clear source data" })
1673
+ ] }),
1674
+ /* @__PURE__ */ jsxs7("p", { className: "cfi-vi-muted", style: { fontSize: "var(--cfi-font-size-sm, 13px)" }, children: [
1675
+ "Deletes every upload, criteria, volume actual, prediction, and forecast edit tied to",
1676
+ " ",
1677
+ /* @__PURE__ */ jsx7("span", { className: "cfi-vi-mono", children: source.name }),
1678
+ ". Other sources are untouched. Configuration (segments, mappings, key column, layout) stays in place. There is no undo."
1679
+ ] }),
1680
+ /* @__PURE__ */ jsxs7("p", { className: "cfi-vi-muted", style: { fontSize: "var(--cfi-font-size-sm, 13px)" }, children: [
1681
+ "To confirm, type ",
1682
+ /* @__PURE__ */ jsx7("span", { className: "cfi-vi-mono", style: { fontWeight: 600 }, children: "DELETE" }),
1683
+ " below."
1684
+ ] }),
1685
+ /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-row", style: { alignItems: "flex-end", maxWidth: "28rem" }, children: [
1686
+ /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-field", style: { flex: 1 }, children: [
1687
+ /* @__PURE__ */ jsx7("label", { className: "cfi-vi-label", children: "Confirmation" }),
1688
+ /* @__PURE__ */ jsx7(
1689
+ TextInput,
1690
+ {
1691
+ className: "cfi-vi-mono",
1692
+ value: clearConfirm,
1693
+ placeholder: "DELETE",
1694
+ disabled: clearing,
1695
+ onChange: (e) => setClearConfirm(e.target.value)
1696
+ }
1697
+ )
1698
+ ] }),
1699
+ /* @__PURE__ */ jsx7(Btn, { variant: "destructive", size: "sm", onClick: () => void handleClearData(), disabled: clearConfirm !== "DELETE" || clearing, children: clearing ? "Clearing\u2026" : "Clear all data" })
1700
+ ] }),
1701
+ clearResult && /* @__PURE__ */ jsx7("p", { style: { fontSize: "var(--cfi-font-size-sm, 13px)", color: "#15803d" }, children: clearResult })
1702
+ ] })
1703
+ ] }) });
1704
+ }
1705
+ export {
1706
+ AUTOMOTIVE_SLOTS,
1707
+ AUTOMOTIVE_TRANSFORM_LABELS,
1708
+ DEFAULT_LEVEL_NAMES,
1709
+ HIERARCHY_LEVEL_COUNT,
1710
+ TallLayoutMappings,
1711
+ UnifiedSegmentMapper,
1712
+ ValueAliasesCard,
1713
+ VolumeImport,
1714
+ VolumeSourceDetail,
1715
+ VolumeSourcesList
1716
+ };