mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 16:35:24 +02:00
feat: add column pinning (freeze) functionality in Pricing Units tab
- Add Pin Columns button to select columns to freeze - Implement sticky columns with left positioning for pinned columns - Support pinning dynamic pricing and container columns - Add Pin icon indicator on pinned columns - Include reset and unpin all options in pin panel
This commit is contained in:
@@ -195,7 +195,6 @@ export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: Ed
|
|||||||
const executeGenerate = async () => {
|
const executeGenerate = async () => {
|
||||||
if (!pendingGeminiField) return;
|
if (!pendingGeminiField) return;
|
||||||
const field = pendingGeminiField;
|
const field = pendingGeminiField;
|
||||||
setPendingGeminiField(null);
|
|
||||||
setLoadingField(field);
|
setLoadingField(field);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
@@ -283,8 +282,10 @@ export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: Ed
|
|||||||
const textarea = document.getElementById(`field-${field}`);
|
const textarea = document.getElementById(`field-${field}`);
|
||||||
if (textarea) textarea.focus();
|
if (textarea) textarea.focus();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
setPendingGeminiField(null);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message || 'An error occurred during generation.');
|
setError(err.message || 'An error occurred during generation.');
|
||||||
|
setPendingGeminiField(null); // Close modal on error so error message is visible in panel
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingField(null);
|
setLoadingField(null);
|
||||||
}
|
}
|
||||||
@@ -489,6 +490,7 @@ export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: Ed
|
|||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={!!pendingGeminiField}
|
isOpen={!!pendingGeminiField}
|
||||||
|
isLoading={loadingField !== null}
|
||||||
onConfirm={executeGenerate}
|
onConfirm={executeGenerate}
|
||||||
onCancel={() => setPendingGeminiField(null)}
|
onCancel={() => setPendingGeminiField(null)}
|
||||||
title="Confirm AI Action"
|
title="Confirm AI Action"
|
||||||
|
|||||||
+262
-39
@@ -16,6 +16,8 @@ import {
|
|||||||
Search,
|
Search,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Maximize2,
|
Maximize2,
|
||||||
|
Pin,
|
||||||
|
PinOff,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
import { ColumnFilterPopover } from './ColumnFilterPopover';
|
import { ColumnFilterPopover } from './ColumnFilterPopover';
|
||||||
@@ -101,6 +103,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
const [selectedSearchItems, setSelectedSearchItems] = useState<Set<string>>(new Set());
|
const [selectedSearchItems, setSelectedSearchItems] = useState<Set<string>>(new Set());
|
||||||
const searchDropdownRef = useRef<HTMLDivElement>(null);
|
const searchDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Pinned columns state
|
||||||
|
const [pinnedColumns, setPinnedColumns] = useState<Set<string>>(new Set(['articleNo', 'articleName']));
|
||||||
|
const [showPinPanel, setShowPinPanel] = useState(false);
|
||||||
|
|
||||||
// Close search dropdown on click outside
|
// Close search dropdown on click outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
@@ -113,8 +119,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const searchSuggestions = useMemo(() => {
|
const searchSuggestions = useMemo(() => {
|
||||||
if (!search && selectedSearchItems.size === 0) return data.slice(0, 50);
|
if (!search) return data.slice(0, 50);
|
||||||
if (!search) return [];
|
|
||||||
|
|
||||||
const terms = search.toLowerCase().split(/\s+/).filter(Boolean);
|
const terms = search.toLowerCase().split(/\s+/).filter(Boolean);
|
||||||
if (terms.length === 0) return data.slice(0, 50);
|
if (terms.length === 0) return data.slice(0, 50);
|
||||||
@@ -559,6 +564,62 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
...srpCols,
|
...srpCols,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ── Pinned columns for freeze ──────────────────────────────────────────────
|
||||||
|
const allPinnableColumns = useMemo(() => {
|
||||||
|
const cols: { key: string; label: string; index: number }[] = [
|
||||||
|
{ key: 'articleNo', label: 'Article No', index: COLUMNS.ARTICLE_NO },
|
||||||
|
{ key: 'articleName', label: 'Article Name', index: COLUMNS.ARTICLE_NAME },
|
||||||
|
{ key: 'line', label: 'Line', index: COLUMNS.LINE },
|
||||||
|
{ key: 'classification', label: 'Classification', index: COLUMNS.CLASSIFICATION },
|
||||||
|
{ key: 'productType', label: 'Type', index: COLUMNS.PRODUCT_TYPE },
|
||||||
|
{ key: 'itemToLogistic', label: 'Item to Logistic', index: COLUMNS.ITEM_TO_LOGISTIC },
|
||||||
|
{ key: 'unitsOuter', label: 'Units/Outer', index: unitsOuterIdx },
|
||||||
|
{ key: 'outerW', label: 'Outer W', index: COLUMNS.OUTER_W },
|
||||||
|
{ key: 'outerL', label: 'Outer L', index: COLUMNS.OUTER_L },
|
||||||
|
{ key: 'outerH', label: 'Outer H', index: COLUMNS.OUTER_H },
|
||||||
|
];
|
||||||
|
pricingEditableCols.forEach(col => {
|
||||||
|
cols.push({ key: `prc_${col.index}`, label: col.name, index: col.index });
|
||||||
|
});
|
||||||
|
containerCols.forEach(col => {
|
||||||
|
cols.push({ key: `con_${col.index}`, label: col.name, index: col.index });
|
||||||
|
});
|
||||||
|
return cols;
|
||||||
|
}, [COLUMNS, unitsOuterIdx, pricingEditableCols, containerCols]);
|
||||||
|
|
||||||
|
const togglePinnedColumn = (key: string) => {
|
||||||
|
setPinnedColumns(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(key)) {
|
||||||
|
next.delete(key);
|
||||||
|
} else {
|
||||||
|
next.add(key);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Column order for sticky positioning
|
||||||
|
const columnOrder = ['articleNo', 'articleName', 'line', 'classification', 'productType', 'itemToLogistic', 'unitsOuter', 'outerW', 'outerL', 'outerH'];
|
||||||
|
|
||||||
|
// Calculate sticky left position for each pinned column
|
||||||
|
const getStickyLeft = useMemo(() => {
|
||||||
|
const pinnedOrder = columnOrder.filter(k => pinnedColumns.has(k));
|
||||||
|
return (key: string): number | null => {
|
||||||
|
if (!pinnedColumns.has(key)) return null;
|
||||||
|
const idx = pinnedOrder.indexOf(key);
|
||||||
|
if (idx === -1) return null;
|
||||||
|
let left = 0;
|
||||||
|
for (let i = 0; i < idx; i++) {
|
||||||
|
const k = pinnedOrder[i];
|
||||||
|
left += columnWidths[k] || 100;
|
||||||
|
}
|
||||||
|
return left;
|
||||||
|
};
|
||||||
|
}, [pinnedColumns, columnOrder, columnWidths]);
|
||||||
|
|
||||||
|
const isPinned = (key: string) => pinnedColumns.has(key);
|
||||||
|
|
||||||
// ═ Unique values for dynamic pricing columns ═════════════════════════════
|
// ═ Unique values for dynamic pricing columns ═════════════════════════════
|
||||||
const dynamicColUniqueValues = useMemo(() => {
|
const dynamicColUniqueValues = useMemo(() => {
|
||||||
const result: Record<number, string[]> = {};
|
const result: Record<number, string[]> = {};
|
||||||
@@ -844,6 +905,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={`${row[COLUMNS.ARTICLE_NO]}-${idx}`}
|
key={`${row[COLUMNS.ARTICLE_NO]}-${idx}`}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -957,6 +1019,95 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ── Pin columns control ── */}
|
||||||
|
<div className="flex items-center gap-3 bg-slate-800/60 border border-slate-700/50 rounded-xl p-3">
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowPinPanel(!showPinPanel)}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all',
|
||||||
|
pinnedColumns.size > 0
|
||||||
|
? 'bg-blue-600 text-white shadow-lg shadow-blue-900/20'
|
||||||
|
: 'bg-slate-700 text-slate-300 hover:bg-slate-600'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Pin className="w-4 h-4" />
|
||||||
|
<span>Pin Columns</span>
|
||||||
|
{pinnedColumns.size > 0 && (
|
||||||
|
<span className="bg-white/20 px-1.5 py-0.5 rounded text-xs font-bold">{pinnedColumns.size}</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showPinPanel && (
|
||||||
|
<div className="absolute top-full left-0 mt-2 w-72 bg-slate-800 border border-slate-700 rounded-xl shadow-2xl z-50 overflow-hidden animate-in fade-in zoom-in-95 duration-100">
|
||||||
|
<div className="p-3 border-b border-slate-700 bg-slate-900/50">
|
||||||
|
<p className="text-xs font-bold text-slate-300">Select columns to pin (freeze)</p>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-64 overflow-y-auto p-2 grid grid-cols-2 gap-1">
|
||||||
|
{allPinnableColumns.map(col => (
|
||||||
|
<button
|
||||||
|
key={col.key}
|
||||||
|
onClick={() => togglePinnedColumn(col.key)}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 px-3 py-2 rounded-lg text-xs transition-all text-left',
|
||||||
|
pinnedColumns.has(col.key)
|
||||||
|
? 'bg-blue-600/20 text-blue-400 border border-blue-500/40'
|
||||||
|
: 'bg-slate-700/50 text-slate-300 hover:bg-slate-700 border border-transparent'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={cn(
|
||||||
|
'w-4 h-4 rounded border flex items-center justify-center shrink-0',
|
||||||
|
pinnedColumns.has(col.key)
|
||||||
|
? 'bg-blue-600 border-blue-600'
|
||||||
|
: 'border-slate-600'
|
||||||
|
)}>
|
||||||
|
{pinnedColumns.has(col.key) && (
|
||||||
|
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="truncate">{col.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="p-2 border-t border-slate-700 bg-slate-900/50 flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setPinnedColumns(new Set(['articleNo', 'articleName']))}
|
||||||
|
className="flex-1 px-3 py-1.5 text-xs text-slate-400 hover:text-white hover:bg-slate-700 rounded transition-colors"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setPinnedColumns(new Set())}
|
||||||
|
className="flex-1 px-3 py-1.5 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 rounded transition-colors"
|
||||||
|
>
|
||||||
|
Unpin All
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-slate-500">
|
||||||
|
<PinOff className="w-3.5 h-3.5" />
|
||||||
|
<span>Pinned:</span>
|
||||||
|
{pinnedColumns.size === 0 ? (
|
||||||
|
<span className="text-slate-600">None</span>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{Array.from(pinnedColumns).map(key => {
|
||||||
|
const col = allPinnableColumns.find(c => c.key === key);
|
||||||
|
return col ? (
|
||||||
|
<span key={key} className="bg-blue-500/10 text-blue-400 px-2 py-0.5 rounded border border-blue-500/20">
|
||||||
|
{col.label}
|
||||||
|
</span>
|
||||||
|
) : null;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* ── Table ── */}
|
{/* ── Table ── */}
|
||||||
<div className="flex-1 overflow-auto bg-slate-800 rounded-xl border border-slate-700 shadow-xl">
|
<div className="flex-1 overflow-auto bg-slate-800 rounded-xl border border-slate-700 shadow-xl">
|
||||||
{sortedRows.length === 0 ? (
|
{sortedRows.length === 0 ? (
|
||||||
@@ -969,9 +1120,15 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
<table ref={tableRef} className="w-full text-sm border-collapse table-fixed">
|
<table ref={tableRef} className="w-full text-sm border-collapse table-fixed">
|
||||||
<thead className="sticky top-0 z-10 bg-slate-900 border-b border-slate-700">
|
<thead className="sticky top-0 z-10 bg-slate-900 border-b border-slate-700">
|
||||||
<tr>
|
<tr>
|
||||||
<th style={{ width: columnWidths.articleNo }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort('articleNo')}>
|
<th style={{ width: columnWidths.articleNo, ...(isPinned('articleNo') ? { left: getStickyLeft('articleNo') ?? 0 } : {}) }} className={cn(
|
||||||
|
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
|
||||||
|
isPinned('articleNo') && "sticky bg-slate-900 z-20"
|
||||||
|
)} onClick={() => handleSort('articleNo')}>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="truncate flex items-center gap-1">Article No <SortIcon current={sortConfig.key === 'articleNo' ? sortConfig.direction : null} /></span>
|
<span className="truncate flex items-center gap-1">
|
||||||
|
{isPinned('articleNo') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||||
|
Article No <SortIcon current={sortConfig.key === 'articleNo' ? sortConfig.direction : null} />
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'sku' ? null : 'sku'); }}
|
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'sku' ? null : 'sku'); }}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -991,9 +1148,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
)}
|
)}
|
||||||
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'articleNo', columnWidths.articleNo); }} />
|
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'articleNo', columnWidths.articleNo); }} />
|
||||||
</th>
|
</th>
|
||||||
<th style={{ width: columnWidths.articleName }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort('articleName')}>
|
<th style={{ width: columnWidths.articleName, ...(isPinned('articleName') ? { left: getStickyLeft('articleName') ?? 0 } : {}) }} className={cn(
|
||||||
|
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
|
||||||
|
isPinned('articleName') && "sticky bg-slate-900 z-20"
|
||||||
|
)} onClick={() => handleSort('articleName')}>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="truncate flex items-center gap-1">Article Name <SortIcon current={sortConfig.key === 'articleName' ? sortConfig.direction : null} /></span>
|
<span className="truncate flex items-center gap-1">
|
||||||
|
{isPinned('articleName') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||||
|
Article Name <SortIcon current={sortConfig.key === 'articleName' ? sortConfig.direction : null} /></span>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'name' ? null : 'name'); }}
|
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'name' ? null : 'name'); }}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -1013,9 +1175,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
)}
|
)}
|
||||||
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'articleName', columnWidths.articleName); }} />
|
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'articleName', columnWidths.articleName); }} />
|
||||||
</th>
|
</th>
|
||||||
<th style={{ width: columnWidths.line }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort('line')}>
|
<th style={{ width: columnWidths.line, ...(isPinned('line') ? { left: getStickyLeft('line') ?? 0 } : {}) }} className={cn(
|
||||||
|
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
|
||||||
|
isPinned('line') && "sticky bg-slate-900 z-20"
|
||||||
|
)} onClick={() => handleSort('line')}>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="truncate flex items-center gap-1">Line <SortIcon current={sortConfig.key === 'line' ? sortConfig.direction : null} /></span>
|
<span className="truncate flex items-center gap-1">
|
||||||
|
{isPinned('line') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||||
|
Line <SortIcon current={sortConfig.key === 'line' ? sortConfig.direction : null} /></span>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'line' ? null : 'line'); }}
|
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'line' ? null : 'line'); }}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -1038,9 +1205,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
)}
|
)}
|
||||||
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'line', columnWidths.line); }} />
|
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'line', columnWidths.line); }} />
|
||||||
</th>
|
</th>
|
||||||
<th style={{ width: columnWidths.classification }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort('classification')}>
|
<th style={{ width: columnWidths.classification, ...(isPinned('classification') ? { left: getStickyLeft('classification') ?? 0 } : {}) }} className={cn(
|
||||||
|
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
|
||||||
|
isPinned('classification') && "sticky bg-slate-900 z-20"
|
||||||
|
)} onClick={() => handleSort('classification')}>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="truncate flex items-center gap-1">Classification <SortIcon current={sortConfig.key === 'classification' ? sortConfig.direction : null} /></span>
|
<span className="truncate flex items-center gap-1">
|
||||||
|
{isPinned('classification') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||||
|
Classification <SortIcon current={sortConfig.key === 'classification' ? sortConfig.direction : null} /></span>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'classification' ? null : 'classification'); }}
|
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'classification' ? null : 'classification'); }}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -1064,8 +1236,12 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'classification', columnWidths.classification); }} />
|
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'classification', columnWidths.classification); }} />
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th style={{ width: columnWidths.productType }} className="text-left px-3 py-3 text-xs font-semibold text-purple-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort('productType')}>
|
<th style={{ width: columnWidths.productType, ...(isPinned('productType') ? { left: getStickyLeft('productType') ?? 0 } : {}) }} className={cn(
|
||||||
|
"text-left px-3 py-3 text-xs font-semibold text-purple-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
|
||||||
|
isPinned('productType') && "sticky bg-slate-900 z-20"
|
||||||
|
)} onClick={() => handleSort('productType')}>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
|
{isPinned('productType') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||||
Type <SortIcon current={sortConfig.key === 'productType' ? sortConfig.direction : null} />
|
Type <SortIcon current={sortConfig.key === 'productType' ? sortConfig.direction : null} />
|
||||||
<button
|
<button
|
||||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); setOpenFilter(openFilter === 'productType' ? null : 'productType'); }}
|
onClick={(e: React.MouseEvent) => { e.stopPropagation(); setOpenFilter(openFilter === 'productType' ? null : 'productType'); }}
|
||||||
@@ -1087,8 +1263,13 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'productType', columnWidths.productType); }} />
|
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'productType', columnWidths.productType); }} />
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th style={{ width: columnWidths.itemToLogistic }} className="text-left px-3 py-3 text-xs font-semibold text-pink-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort('itemToLogistic')}>
|
<th style={{ width: columnWidths.itemToLogistic, ...(isPinned('itemToLogistic') ? { left: getStickyLeft('itemToLogistic') ?? 0 } : {}) }} className={cn(
|
||||||
<span className="flex items-center gap-1">Item to Logistic <SortIcon current={sortConfig.key === 'itemToLogistic' ? sortConfig.direction : null} /></span>
|
"text-left px-3 py-3 text-xs font-semibold text-pink-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
|
||||||
|
isPinned('itemToLogistic') && "sticky bg-slate-900 z-20"
|
||||||
|
)} onClick={() => handleSort('itemToLogistic')}>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
{isPinned('itemToLogistic') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||||
|
Item to Logistic <SortIcon current={sortConfig.key === 'itemToLogistic' ? sortConfig.direction : null} /></span>
|
||||||
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'itemToLogistic', columnWidths.itemToLogistic); }} />
|
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'itemToLogistic', columnWidths.itemToLogistic); }} />
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
@@ -1096,9 +1277,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
const colKey = `prc_${col.index}`;
|
const colKey = `prc_${col.index}`;
|
||||||
const width = columnWidths[colKey] ?? 100;
|
const width = columnWidths[colKey] ?? 100;
|
||||||
return (
|
return (
|
||||||
<th key={col.index} style={{ width }} className="text-left px-3 py-3 text-xs font-semibold text-blue-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort(col.index)}>
|
<th key={col.index} style={{ width, ...(isPinned(colKey) ? { left: getStickyLeft(colKey) ?? 0 } : {}) }} className={cn(
|
||||||
|
"text-left px-3 py-3 text-xs font-semibold text-blue-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
|
||||||
|
isPinned(colKey) && "sticky bg-slate-900 z-20"
|
||||||
|
)} onClick={() => handleSort(col.index)}>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="truncate flex items-center gap-1">{col.name} <SortIcon current={sortConfig.key === col.index ? sortConfig.direction : null} /></span>
|
<span className="truncate flex items-center gap-1">
|
||||||
|
{isPinned(colKey) && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||||
|
{col.name} <SortIcon current={sortConfig.key === col.index ? sortConfig.direction : null} /></span>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -1132,9 +1318,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<th style={{ width: columnWidths.unitsOuter }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort('unitsOuter')}>
|
<th style={{ width: columnWidths.unitsOuter, ...(isPinned('unitsOuter') ? { left: getStickyLeft('unitsOuter') ?? 0 } : {}) }} className={cn(
|
||||||
|
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
|
||||||
|
isPinned('unitsOuter') && "sticky bg-slate-900 z-20"
|
||||||
|
)} onClick={() => handleSort('unitsOuter')}>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="truncate flex items-center gap-1">Units/Outer <SortIcon current={sortConfig.key === 'unitsOuter' ? sortConfig.direction : null} /></span>
|
<span className="truncate flex items-center gap-1">
|
||||||
|
{isPinned('unitsOuter') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||||
|
Units/Outer <SortIcon current={sortConfig.key === 'unitsOuter' ? sortConfig.direction : null} /></span>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'unitsOuter' ? null : 'unitsOuter'); }}
|
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'unitsOuter' ? null : 'unitsOuter'); }}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -1158,9 +1349,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'unitsOuter', columnWidths.unitsOuter); }} />
|
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'unitsOuter', columnWidths.unitsOuter); }} />
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th style={{ width: columnWidths.outerW }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort('outerW')}>
|
<th style={{ width: columnWidths.outerW, ...(isPinned('outerW') ? { left: getStickyLeft('outerW') ?? 0 } : {}) }} className={cn(
|
||||||
|
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
|
||||||
|
isPinned('outerW') && "sticky bg-slate-900 z-20"
|
||||||
|
)} onClick={() => handleSort('outerW')}>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="truncate flex items-center gap-1">Outer W <SortIcon current={sortConfig.key === 'outerW' ? sortConfig.direction : null} /></span>
|
<span className="truncate flex items-center gap-1">
|
||||||
|
{isPinned('outerW') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||||
|
Outer W <SortIcon current={sortConfig.key === 'outerW' ? sortConfig.direction : null} /></span>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerW' ? null : 'outerW'); }}
|
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerW' ? null : 'outerW'); }}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -1184,9 +1380,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'outerW', columnWidths.outerW); }} />
|
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'outerW', columnWidths.outerW); }} />
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th style={{ width: columnWidths.outerL }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort('outerL')}>
|
<th style={{ width: columnWidths.outerL, ...(isPinned('outerL') ? { left: getStickyLeft('outerL') ?? 0 } : {}) }} className={cn(
|
||||||
|
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
|
||||||
|
isPinned('outerL') && "sticky bg-slate-900 z-20"
|
||||||
|
)} onClick={() => handleSort('outerL')}>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="truncate flex items-center gap-1">Outer L <SortIcon current={sortConfig.key === 'outerL' ? sortConfig.direction : null} /></span>
|
<span className="truncate flex items-center gap-1">
|
||||||
|
{isPinned('outerL') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||||
|
Outer L <SortIcon current={sortConfig.key === 'outerL' ? sortConfig.direction : null} /></span>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerL' ? null : 'outerL'); }}
|
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerL' ? null : 'outerL'); }}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -1210,9 +1411,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'outerL', columnWidths.outerL); }} />
|
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'outerL', columnWidths.outerL); }} />
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th style={{ width: columnWidths.outerH }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort('outerH')}>
|
<th style={{ width: columnWidths.outerH, ...(isPinned('outerH') ? { left: getStickyLeft('outerH') ?? 0 } : {}) }} className={cn(
|
||||||
|
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
|
||||||
|
isPinned('outerH') && "sticky bg-slate-900 z-20"
|
||||||
|
)} onClick={() => handleSort('outerH')}>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="truncate flex items-center gap-1">Outer H <SortIcon current={sortConfig.key === 'outerH' ? sortConfig.direction : null} /></span>
|
<span className="truncate flex items-center gap-1">
|
||||||
|
{isPinned('outerH') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||||
|
Outer H <SortIcon current={sortConfig.key === 'outerH' ? sortConfig.direction : null} /></span>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerH' ? null : 'outerH'); }}
|
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerH' ? null : 'outerH'); }}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -1240,9 +1446,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
const colKey = `con_${col.index}`;
|
const colKey = `con_${col.index}`;
|
||||||
const width = columnWidths[colKey] ?? 110;
|
const width = columnWidths[colKey] ?? 110;
|
||||||
return (
|
return (
|
||||||
<th key={col.index} style={{ width }} className="text-left px-3 py-3 text-xs font-semibold text-orange-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort(col.index)}>
|
<th key={col.index} style={{ width, ...(isPinned(colKey) ? { left: getStickyLeft(colKey) ?? 0 } : {}) }} className={cn(
|
||||||
|
"text-left px-3 py-3 text-xs font-semibold text-orange-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
|
||||||
|
isPinned(colKey) && "sticky bg-slate-900 z-20"
|
||||||
|
)} onClick={() => handleSort(col.index)}>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="truncate flex items-center gap-1">{col.name} <SortIcon current={sortConfig.key === col.index ? sortConfig.direction : null} /></span>
|
<span className="truncate flex items-center gap-1">
|
||||||
|
{isPinned(colKey) && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||||
|
{col.name} <SortIcon current={sortConfig.key === col.index ? sortConfig.direction : null} /></span>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -1358,24 +1569,24 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Art. No. */}
|
{/* Art. No. */}
|
||||||
<td className="px-3 py-2.5 font-mono text-xs text-slate-300 whitespace-nowrap overflow-hidden truncate">
|
<td className={cn("px-3 py-2.5 font-mono text-xs text-slate-300 whitespace-nowrap overflow-hidden truncate", isPinned('articleNo') && "sticky bg-slate-800 z-10")} style={isPinned('articleNo') ? { left: getStickyLeft('articleNo') ?? 0 } : {}}>
|
||||||
{row[COLUMNS.ARTICLE_NO]}
|
{row[COLUMNS.ARTICLE_NO]}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* Article Name */}
|
{/* Article Name */}
|
||||||
<td className="px-3 py-2.5 text-slate-200 max-w-[200px]">
|
<td className={cn("px-3 py-2.5 text-slate-200 max-w-[200px]", isPinned('articleName') && "sticky bg-slate-800 z-10")} style={isPinned('articleName') ? { left: getStickyLeft('articleName') ?? 0 } : {}}>
|
||||||
<span className="line-clamp-1" title={row[COLUMNS.ARTICLE_NAME]}>
|
<span className="line-clamp-1" title={row[COLUMNS.ARTICLE_NAME]}>
|
||||||
{row[COLUMNS.ARTICLE_NAME] || '—'}
|
{row[COLUMNS.ARTICLE_NAME] || '—'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* Line */}
|
{/* Line */}
|
||||||
<td className="px-3 py-2.5 text-slate-400 whitespace-nowrap text-xs overflow-hidden truncate">
|
<td className={cn("px-3 py-2.5 text-slate-400 whitespace-nowrap text-xs overflow-hidden truncate", isPinned('line') && "sticky bg-slate-800 z-10")} style={isPinned('line') ? { left: getStickyLeft('line') ?? 0 } : {}}>
|
||||||
{row[COLUMNS.LINE] || '—'}
|
{row[COLUMNS.LINE] || '—'}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* Classification */}
|
{/* Classification */}
|
||||||
<td className="px-3 py-2.5 overflow-hidden">
|
<td className={cn("px-3 py-2.5 overflow-hidden", isPinned('classification') && "sticky bg-slate-800 z-10")} style={isPinned('classification') ? { left: getStickyLeft('classification') ?? 0 } : {}}>
|
||||||
<span className={cn(
|
<span className={cn(
|
||||||
'px-2 py-0.5 rounded text-[10px] font-bold border whitespace-nowrap inline-block max-w-full truncate',
|
'px-2 py-0.5 rounded text-[10px] font-bold border whitespace-nowrap inline-block max-w-full truncate',
|
||||||
String(row[COLUMNS.CLASSIFICATION] || '').toUpperCase().includes('CORE')
|
String(row[COLUMNS.CLASSIFICATION] || '').toUpperCase().includes('CORE')
|
||||||
@@ -1389,7 +1600,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* TYPE cell */}
|
{/* TYPE cell */}
|
||||||
<td className="px-3 py-2 overflow-visible relative">
|
<td className={cn("px-3 py-2 overflow-visible relative", isPinned('productType') && "sticky bg-slate-800 z-10")} style={isPinned('productType') ? { left: getStickyLeft('productType') ?? 0 } : {}}>
|
||||||
{editingType?.rowIndex === dataIndex ? (
|
{editingType?.rowIndex === dataIndex ? (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
@@ -1435,7 +1646,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* ITEM TO LOGISTIC cell */}
|
{/* ITEM TO LOGISTIC cell */}
|
||||||
<td className="px-3 py-2 overflow-visible relative">
|
<td className={cn("px-3 py-2 overflow-visible relative", isPinned('itemToLogistic') && "sticky bg-slate-800 z-10")} style={isPinned('itemToLogistic') ? { left: getStickyLeft('itemToLogistic') ?? 0 } : {}}>
|
||||||
{editingLogistic?.rowIndex === dataIndex ? (
|
{editingLogistic?.rowIndex === dataIndex ? (
|
||||||
<input
|
<input
|
||||||
ref={logisticInputRef}
|
ref={logisticInputRef}
|
||||||
@@ -1471,9 +1682,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
const isSaving = savingCell?.rowIndex === dataIndex && savingCell?.colIndex === col.index;
|
const isSaving = savingCell?.rowIndex === dataIndex && savingCell?.colIndex === col.index;
|
||||||
const val = formatPrice(row[col.index]);
|
const val = formatPrice(row[col.index]);
|
||||||
const isEmpty = !row[col.index] || row[col.index] === '' || Number(row[col.index]) === 0;
|
const isEmpty = !row[col.index] || row[col.index] === '' || Number(row[col.index]) === 0;
|
||||||
|
const colKey = `prc_${col.index}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td key={col.index} className="px-3 py-2 overflow-hidden">
|
<td key={col.index} className={cn("px-3 py-2 overflow-hidden", isPinned(colKey) && "sticky bg-slate-800 z-10")} style={isPinned(colKey) ? { left: getStickyLeft(colKey) ?? 0 } : {}}>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<input
|
<input
|
||||||
@@ -1514,17 +1726,28 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Units columns */}
|
{/* Units columns */}
|
||||||
<td className="px-3 py-2.5">{unitOuterBadge(row[unitsOuterIdx])}</td>
|
<td className={cn("px-3 py-2.5", isPinned('unitsOuter') && "sticky bg-slate-800 z-10")} style={isPinned('unitsOuter') ? { left: getStickyLeft('unitsOuter') ?? 0 } : {}}>
|
||||||
<td className="px-3 py-2.5 text-slate-400 font-mono text-xs overflow-hidden truncate">{row[COLUMNS.OUTER_W] ?? '-'}</td>
|
{unitOuterBadge(row[unitsOuterIdx])}
|
||||||
<td className="px-3 py-2.5 text-slate-400 font-mono text-xs overflow-hidden truncate">{row[COLUMNS.OUTER_L] ?? '-'}</td>
|
</td>
|
||||||
<td className="px-3 py-2.5 text-slate-400 font-mono text-xs overflow-hidden truncate">{row[COLUMNS.OUTER_H] ?? '-'}</td>
|
<td className={cn("px-3 py-2.5 text-slate-400 font-mono text-xs overflow-hidden truncate", isPinned('outerW') && "sticky bg-slate-800 z-10")} style={isPinned('outerW') ? { left: getStickyLeft('outerW') ?? 0 } : {}}>
|
||||||
|
{row[COLUMNS.OUTER_W] ?? '-'}
|
||||||
|
</td>
|
||||||
|
<td className={cn("px-3 py-2.5 text-slate-400 font-mono text-xs overflow-hidden truncate", isPinned('outerL') && "sticky bg-slate-800 z-10")} style={isPinned('outerL') ? { left: getStickyLeft('outerL') ?? 0 } : {}}>
|
||||||
|
{row[COLUMNS.OUTER_L] ?? '-'}
|
||||||
|
</td>
|
||||||
|
<td className={cn("px-3 py-2.5 text-slate-400 font-mono text-xs overflow-hidden truncate", isPinned('outerH') && "sticky bg-slate-800 z-10")} style={isPinned('outerH') ? { left: getStickyLeft('outerH') ?? 0 } : {}}>
|
||||||
|
{row[COLUMNS.OUTER_H] ?? '-'}
|
||||||
|
</td>
|
||||||
|
|
||||||
{/* Container units columns */}
|
{/* Container units columns */}
|
||||||
{containerCols.map(col => (
|
{containerCols.map(col => {
|
||||||
<td key={col.index} className="px-3 py-2.5 overflow-hidden truncate">
|
const colKey = `con_${col.index}`;
|
||||||
|
return (
|
||||||
|
<td key={col.index} className={cn("px-3 py-2.5 overflow-hidden truncate", isPinned(colKey) && "sticky bg-slate-800 z-10")} style={isPinned(colKey) ? { left: getStickyLeft(colKey) ?? 0 } : {}}>
|
||||||
{unitBadge(row[col.index], col.name)}
|
{unitBadge(row[col.index], col.name)}
|
||||||
</td>
|
</td>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
{/* Issues column (weight only) */}
|
{/* Issues column (weight only) */}
|
||||||
<td className="px-3 py-2 overflow-hidden">
|
<td className="px-3 py-2 overflow-hidden">
|
||||||
|
|||||||
+13
-7
@@ -13,13 +13,12 @@ export async function generateGemini(prompt: string, systemPrompt: string): Prom
|
|||||||
throw new Error('Gemini API Key not found. Please set VITE_GEMINI_API_KEY in .env or GEMINI_API_KEY in localStorage.');
|
throw new Error('Gemini API Key not found. Please set VITE_GEMINI_API_KEY in .env or GEMINI_API_KEY in localStorage.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Model fallback list - updated for March 2026
|
// Model fallback list - using current stable and experimental versions
|
||||||
const modelOptions = [
|
const modelOptions = [
|
||||||
'gemini-2.5-flash',
|
'gemini-1.5-flash',
|
||||||
'gemini-2.5-flash-lite',
|
'gemini-1.5-pro',
|
||||||
'gemini-2.5-pro',
|
'gemini-2.0-flash-exp',
|
||||||
'gemini-2.0-flash',
|
'gemini-2.0-flash'
|
||||||
'gemini-1.5-flash'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let lastError: any = null;
|
let lastError: any = null;
|
||||||
@@ -61,5 +60,12 @@ export async function generateGemini(prompt: string, systemPrompt: string): Prom
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(lastError?.error?.message || lastError?.message || 'All Gemini models failed. Check console for details.');
|
let errorMessage = lastError?.error?.message || lastError?.message || 'All Gemini models failed.';
|
||||||
|
|
||||||
|
// Check if it's a quota error (429)
|
||||||
|
if (lastError?.error?.code === 429 || lastError?.status === 429 || errorMessage.toLowerCase().includes('quota') || errorMessage.toLowerCase().includes('429')) {
|
||||||
|
errorMessage = 'Gemini API quota exceeded. You may need to wait or switch to a paid billing plan in Google AI Studio.';
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(errorMessage + ' Check console for details.');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user