UI: Add 'Check Anna' column to Pricing Units view

Mirrors Check Ying with its own checkbox and note editor (pink theme).
Stored in virtual columns ANNA_CHECK (105) and ANNA_NOTE (106).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Vidal Wolf
2026-04-23 13:02:14 +02:00
co-authored by Claude Sonnet 4.6
parent aca37c4a99
commit f545776e15
2 changed files with 103 additions and 5 deletions
+97 -3
View File
@@ -592,6 +592,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
}, [headers, displayedIndices]); }, [headers, displayedIndices]);
const [noteEditor, setNoteEditor] = useState<{ rowIndex: number; text: string } | null>(null); const [noteEditor, setNoteEditor] = useState<{ rowIndex: number; text: string } | null>(null);
const [annaEditor, setAnnaEditor] = useState<{ rowIndex: number; text: string } | null>(null);
const handleToggleCheck = async (rowIndex: number, checked: boolean) => { const handleToggleCheck = async (rowIndex: number, checked: boolean) => {
const original = data[rowIndex]; const original = data[rowIndex];
@@ -612,6 +613,25 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
await onSaveRow(rowIndex, newRow); await onSaveRow(rowIndex, newRow);
}; };
const handleToggleAnnaCheck = async (rowIndex: number, checked: boolean) => {
const original = data[rowIndex];
const newRow = [...original];
newRow[COLUMNS.ANNA_CHECK] = checked;
onCaptureState(`${checked ? 'Anna checked' : 'Anna unchecked'} ${original[COLUMNS.ARTICLE_NO]}`);
await onSaveRow(rowIndex, newRow);
};
const saveAnnaNote = async () => {
if (!annaEditor) return;
const { rowIndex, text } = annaEditor;
const original = data[rowIndex];
const newRow = [...original];
newRow[COLUMNS.ANNA_NOTE] = text;
setAnnaEditor(null);
onCaptureState(`Updated Anna note for ${original[COLUMNS.ARTICLE_NO]}`);
await onSaveRow(rowIndex, newRow);
};
// ── Column detection notice ─────────────────────────────────────────────── // ── Column detection notice ───────────────────────────────────────────────
const missingCols: string[] = []; const missingCols: string[] = [];
if (uvpIdx < 0) missingCols.push('UVP'); if (uvpIdx < 0) missingCols.push('UVP');
@@ -627,6 +647,39 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
return ( return (
<div className="flex flex-col h-full gap-4 relative"> <div className="flex flex-col h-full gap-4 relative">
{/* ── Anna Note Editor Modal ── */}
{annaEditor && (
<div className="fixed inset-0 bg-slate-950/50 backdrop-blur-sm z-[100] flex items-center justify-center p-4">
<div className="bg-slate-800 border border-slate-700 rounded-xl shadow-2xl w-full max-w-md animate-in zoom-in-95 duration-200">
<div className="p-4 border-b border-slate-700 flex items-center justify-between">
<h3 className="font-semibold text-white flex items-center gap-2">
<MessageSquare className="w-4 h-4 text-pink-400" />
Anna Note
</h3>
<p className="text-[10px] text-slate-500 font-mono">{data[annaEditor.rowIndex][COLUMNS.ARTICLE_NO]}</p>
</div>
<div className="p-4">
<textarea
value={annaEditor.text}
onChange={e => setAnnaEditor(prev => prev ? { ...prev, text: e.target.value } : null)}
placeholder="Write a note about this product..."
className="w-full h-32 bg-slate-900 border border-slate-700 rounded-lg p-3 text-sm text-white focus:outline-none focus:border-pink-500 resize-none"
autoFocus
/>
</div>
<div className="p-4 bg-slate-900/50 rounded-b-xl flex justify-end gap-3">
<button onClick={() => setAnnaEditor(null)} className="px-4 py-2 text-sm text-slate-400 hover:text-white transition-colors">Cancel</button>
<button
onClick={saveAnnaNote}
className="px-4 py-2 bg-pink-600 hover:bg-pink-700 text-white text-sm font-bold rounded-lg shadow-lg active:scale-95 transition-all"
>
Save Note
</button>
</div>
</div>
</div>
)}
{/* ── Note Editor Modal ── */} {/* ── Note Editor Modal ── */}
{noteEditor && ( {noteEditor && (
<div className="fixed inset-0 bg-slate-950/50 backdrop-blur-sm z-[100] flex items-center justify-center p-4"> <div className="fixed inset-0 bg-slate-950/50 backdrop-blur-sm z-[100] flex items-center justify-center p-4">
@@ -1127,6 +1180,11 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'check', columnWidths.check); }} /> <ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'check', columnWidths.check); }} />
</th> </th>
<th style={{ width: columnWidths.checkAnna ?? 100 }} className="text-left px-3 py-3 text-xs font-semibold text-pink-400 uppercase tracking-wider group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort(COLUMNS.ANNA_CHECK)}>
<span className="flex items-center gap-1">Check Anna <SortIcon current={sortConfig.key === COLUMNS.ANNA_CHECK ? sortConfig.direction : null} /></span>
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'checkAnna', columnWidths.checkAnna ?? 100); }} />
</th>
{matrixCols.map(col => { {matrixCols.map(col => {
const colKey = `mat_${col.index}`; const colKey = `mat_${col.index}`;
const width = columnWidths[colKey] ?? 120; const width = columnWidths[colKey] ?? 120;
@@ -1350,7 +1408,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
})()} })()}
</td> </td>
{/* Check Column (Validated + Note) */} {/* Check Ying Column (Validated + Note) */}
<td className="px-3 py-2.5"> <td className="px-3 py-2.5">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
@@ -1364,8 +1422,8 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClick={() => setNoteEditor({ rowIndex: dataIndex, text: note })} onClick={() => setNoteEditor({ rowIndex: dataIndex, text: note })}
className={cn( className={cn(
"p-1.5 rounded-md transition-all shrink-0", "p-1.5 rounded-md transition-all shrink-0",
note note
? "bg-amber-500/10 text-amber-400 border border-amber-500/20 hover:bg-amber-500/20" ? "bg-amber-500/10 text-amber-400 border border-amber-500/20 hover:bg-amber-500/20"
: "text-slate-500 hover:text-slate-300 hover:bg-slate-700/50" : "text-slate-500 hover:text-slate-300 hover:bg-slate-700/50"
)} )}
> >
@@ -1380,6 +1438,42 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
</div> </div>
</td> </td>
{/* Check Anna Column */}
{(() => {
const annaChecked = !!row[COLUMNS.ANNA_CHECK];
const annaNote = String(row[COLUMNS.ANNA_NOTE] || '');
return (
<td className="px-3 py-2.5">
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={annaChecked}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleToggleAnnaCheck(dataIndex, e.target.checked)}
className="w-4 h-4 rounded border-slate-600 bg-slate-900 text-pink-600 focus:ring-pink-500 focus:ring-offset-slate-800 cursor-pointer"
/>
<div className="relative group">
<button
onClick={() => setAnnaEditor({ rowIndex: dataIndex, text: annaNote })}
className={cn(
"p-1.5 rounded-md transition-all shrink-0",
annaNote
? "bg-pink-500/10 text-pink-400 border border-pink-500/20 hover:bg-pink-500/20"
: "text-slate-500 hover:text-slate-300 hover:bg-slate-700/50"
)}
>
<MessageSquare className={cn("w-3.5 h-3.5", annaNote && "fill-pink-400/20")} />
</button>
{annaNote && (
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-48 p-2 bg-pink-900/90 border border-pink-500/50 rounded text-xs text-white shadow-lg z-50 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
{annaNote}
</div>
)}
</div>
</div>
</td>
);
})()}
{/* Matrix columns cells */} {/* Matrix columns cells */}
{matrixCols.map(col => ( {matrixCols.map(col => (
<td key={col.index} className="px-3 py-2.5 text-slate-500 text-xs max-w-[150px] truncate" title={String(row[col.index] || '')}> <td key={col.index} className="px-3 py-2.5 text-slate-500 text-xs max-w-[150px] truncate" title={String(row[col.index] || '')}>
+6 -2
View File
@@ -41,7 +41,9 @@ export const COLUMNS = {
VALIDATED_CHECK: 101, VALIDATED_CHECK: 101,
VALIDATED_NOTE: 102, VALIDATED_NOTE: 102,
PRODUCT_TYPE: 103, PRODUCT_TYPE: 103,
ITEM_TO_LOGISTIC: 104 ITEM_TO_LOGISTIC: 104,
ANNA_CHECK: 105,
ANNA_NOTE: 106
}; };
// Search patterns for dynamic detection // Search patterns for dynamic detection
@@ -77,7 +79,9 @@ export const COLUMN_PATTERNS: Record<keyof typeof COLUMNS, string[]> = {
VALIDATED_CHECK: ['validated'], VALIDATED_CHECK: ['validated'],
VALIDATED_NOTE: ['note'], VALIDATED_NOTE: ['note'],
PRODUCT_TYPE: ['product', 'type'], PRODUCT_TYPE: ['product', 'type'],
ITEM_TO_LOGISTIC: ['item', 'to', 'logistic'] ITEM_TO_LOGISTIC: ['item', 'to', 'logistic'],
ANNA_CHECK: ['anna', 'check'],
ANNA_NOTE: ['anna', 'note']
}; };
export function resolveColumnIndices(headers: string[]): typeof COLUMNS { export function resolveColumnIndices(headers: string[]): typeof COLUMNS {