mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 16:55:24 +02:00
558 lines
26 KiB
TypeScript
558 lines
26 KiB
TypeScript
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
||
import { ExcelRow } from '../types';
|
||
import { useColumns } from '../contexts/ColumnsContext';
|
||
import { X, Sparkles, Save, Loader2, Languages, Package, CheckCircle2, Mic, MicOff } from 'lucide-react';
|
||
import { generateGemini } from '../services/gemini';
|
||
import { cn } from '../lib/utils';
|
||
import { useSpeechRecognition } from '../lib/useSpeechRecognition';
|
||
import { ConfirmModal } from './ConfirmModal';
|
||
|
||
interface EditPanelProps {
|
||
row: ExcelRow;
|
||
rowIndex: number;
|
||
onSave: (rowIndex: number, updatedRow: ExcelRow) => void;
|
||
onClose: () => void;
|
||
onCaptureState: (message: string) => void;
|
||
}
|
||
|
||
interface DimensionInputProps {
|
||
label: string;
|
||
field: string;
|
||
value: string;
|
||
isModified: boolean;
|
||
onChange: (val: string) => void;
|
||
placeholder?: string;
|
||
}
|
||
|
||
const DimensionInput = ({ label, value, isModified, onChange, placeholder }: DimensionInputProps) => (
|
||
<div className="flex flex-col gap-1.5">
|
||
<label className="text-[10px] font-medium text-slate-500 uppercase tracking-wider">{label}</label>
|
||
<input
|
||
type="text"
|
||
value={value}
|
||
onChange={e => onChange(e.target.value)}
|
||
placeholder={placeholder}
|
||
className={cn(
|
||
"w-full bg-slate-900 border rounded p-2 text-sm text-white focus:outline-none focus:ring-1 transition-colors",
|
||
isModified ? "border-blue-500 focus:ring-blue-500" : "border-slate-700 focus:border-slate-500"
|
||
)}
|
||
/>
|
||
</div>
|
||
);
|
||
|
||
const AutoResizeTextarea = ({
|
||
id,
|
||
value,
|
||
onChange,
|
||
onKeyDown,
|
||
className,
|
||
placeholder
|
||
}: {
|
||
id?: string;
|
||
value: string;
|
||
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||
onKeyDown?: (e: React.KeyboardEvent) => void;
|
||
className?: string;
|
||
placeholder?: string;
|
||
}) => {
|
||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||
|
||
const adjustHeight = useCallback(() => {
|
||
const textarea = textareaRef.current;
|
||
if (textarea) {
|
||
textarea.style.height = 'auto';
|
||
textarea.style.height = `${textarea.scrollHeight}px`;
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
adjustHeight();
|
||
}, [value, adjustHeight]);
|
||
|
||
return (
|
||
<textarea
|
||
id={id}
|
||
ref={textareaRef}
|
||
value={value}
|
||
onChange={onChange}
|
||
onKeyDown={onKeyDown}
|
||
className={cn(className, "overflow-hidden resize-none")}
|
||
placeholder={placeholder}
|
||
/>
|
||
);
|
||
};
|
||
|
||
interface FieldEditorProps {
|
||
title: string;
|
||
field: string;
|
||
value: string;
|
||
isModified: boolean;
|
||
canTranslate: boolean;
|
||
isGenerated: boolean;
|
||
isLoading: boolean;
|
||
onGenerate: () => void;
|
||
onChange: (val: string) => void;
|
||
onKeyDown: (e: React.KeyboardEvent) => void;
|
||
isListening?: boolean;
|
||
onToggleVoice?: () => void;
|
||
voiceSupported?: boolean;
|
||
}
|
||
|
||
const FieldEditor = ({
|
||
title,
|
||
field,
|
||
value,
|
||
isModified,
|
||
canTranslate,
|
||
isGenerated,
|
||
isLoading,
|
||
onGenerate,
|
||
onChange,
|
||
onKeyDown,
|
||
isListening,
|
||
onToggleVoice,
|
||
voiceSupported
|
||
}: FieldEditorProps) => (
|
||
<div className="flex flex-col gap-2">
|
||
<div className="flex items-center justify-between">
|
||
<label className="text-sm font-medium text-slate-300">{title}</label>
|
||
<div className="flex items-center gap-2">
|
||
{canTranslate && (
|
||
<div className="flex items-center gap-1 text-[10px] text-slate-500 italic">
|
||
<Languages className="w-3 h-3" />
|
||
Can translate from existing
|
||
</div>
|
||
)}
|
||
{voiceSupported && (
|
||
<button
|
||
onClick={onToggleVoice}
|
||
className={cn(
|
||
"flex items-center gap-1.5 text-xs font-medium px-2 py-1 rounded transition-colors",
|
||
isListening
|
||
? "bg-red-600/20 text-red-400 animate-pulse"
|
||
: "bg-slate-700/50 text-slate-400 hover:bg-slate-600 hover:text-white"
|
||
)}
|
||
title={isListening ? "Stop recording" : "Voice input"}
|
||
>
|
||
{isListening ? <MicOff className="w-3 h-3" /> : <Mic className="w-3 h-3" />}
|
||
{isListening ? 'Stop' : 'Voice'}
|
||
</button>
|
||
)}
|
||
<button
|
||
onClick={onGenerate}
|
||
disabled={isLoading}
|
||
className="flex items-center gap-1.5 text-xs font-medium bg-blue-600/20 text-blue-400 hover:bg-blue-600 hover:text-white px-2 py-1 rounded transition-colors disabled:opacity-50"
|
||
>
|
||
{isLoading ? <Loader2 className="w-3 h-3 animate-spin" /> : <Sparkles className="w-3 h-3" />}
|
||
{canTranslate ? 'Translate with Gemini' : 'Generate with Gemini'}
|
||
</button>
|
||
</div>
|
||
{isGenerated && (
|
||
<div className="flex items-center gap-1.5 text-[10px] text-emerald-400 bg-emerald-400/10 px-2 py-0.5 rounded w-fit animate-in fade-in slide-in-from-top-1 duration-300">
|
||
<CheckCircle2 className="w-3 h-3" />
|
||
AI Generated - You can still edit manually
|
||
</div>
|
||
)}
|
||
</div>
|
||
<AutoResizeTextarea
|
||
id={`field-${field}`}
|
||
value={value}
|
||
onChange={e => onChange(e.target.value)}
|
||
onKeyDown={onKeyDown}
|
||
className={cn(
|
||
"w-full min-h-[128px] bg-slate-900 border rounded-md p-3 text-sm text-white focus:outline-none focus:ring-1 transition-colors",
|
||
isModified ? "border-blue-500 focus:ring-blue-500" : "border-slate-700 focus:border-slate-500 focus:ring-slate-500"
|
||
)}
|
||
placeholder={`Enter ${title}...`}
|
||
/>
|
||
</div>
|
||
);
|
||
|
||
export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: EditPanelProps) {
|
||
const COLUMNS = useColumns();
|
||
const [formData, setFormData] = useState({
|
||
longDe: row[COLUMNS.LONG_DE] || '',
|
||
longEn: row[COLUMNS.LONG_EN] || '',
|
||
shortDe: row[COLUMNS.SHORT_DE] || '',
|
||
shortEn: row[COLUMNS.SHORT_EN] || '',
|
||
detailsDe: row[COLUMNS.DETAILS_DE] || '',
|
||
detailsEn: row[COLUMNS.DETAILS_EN] || '',
|
||
innerW: row[COLUMNS.INNER_W] !== undefined && row[COLUMNS.INNER_W] !== null ? String(row[COLUMNS.INNER_W]) : '',
|
||
innerL: row[COLUMNS.INNER_L] !== undefined && row[COLUMNS.INNER_L] !== null ? String(row[COLUMNS.INNER_L]) : '',
|
||
innerH: row[COLUMNS.INNER_H] !== undefined && row[COLUMNS.INNER_H] !== null ? String(row[COLUMNS.INNER_H]) : '',
|
||
outerW: row[COLUMNS.OUTER_W] !== undefined && row[COLUMNS.OUTER_W] !== null ? String(row[COLUMNS.OUTER_W]) : '',
|
||
outerL: row[COLUMNS.OUTER_L] !== undefined && row[COLUMNS.OUTER_L] !== null ? String(row[COLUMNS.OUTER_L]) : '',
|
||
outerH: row[COLUMNS.OUTER_H] !== undefined && row[COLUMNS.OUTER_H] !== null ? String(row[COLUMNS.OUTER_H]) : '',
|
||
unitsOuter: row[COLUMNS.UNITS_OUTER] !== undefined && row[COLUMNS.UNITS_OUTER] !== null ? String(row[COLUMNS.UNITS_OUTER]) : '',
|
||
moq: row[COLUMNS.MOQ] !== undefined && row[COLUMNS.MOQ] !== null ? String(row[COLUMNS.MOQ]) : '',
|
||
productType: row[COLUMNS.CATEGORIZATION_CODE] !== undefined && row[COLUMNS.CATEGORIZATION_CODE] !== null ? String(row[COLUMNS.CATEGORIZATION_CODE]) : '',
|
||
itemToLogistic: row[COLUMNS.ITEM_TO_LOGISTIC] !== undefined && row[COLUMNS.ITEM_TO_LOGISTIC] !== null ? String(row[COLUMNS.ITEM_TO_LOGISTIC]) : '',
|
||
});
|
||
|
||
const [loadingField, setLoadingField] = useState<string | null>(null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
||
const [pendingGeminiField, setPendingGeminiField] = useState<keyof typeof formData | null>(null);
|
||
const [generatedFields, setGeneratedFields] = useState<Set<string>>(new Set());
|
||
|
||
const handleSpeechResult = useCallback((transcript: string) => {
|
||
setFormData(prev => ({ ...prev, longDe: prev.longDe + ' ' + transcript }));
|
||
}, []);
|
||
|
||
const { isListening: isListeningDe, start: startListeningDe, stop: stopListeningDe, isSupported: speechSupported } = useSpeechRecognition({ onResult: handleSpeechResult });
|
||
|
||
const handleSpeechResultEn = useCallback((transcript: string) => {
|
||
setFormData(prev => ({ ...prev, longEn: prev.longEn + ' ' + transcript }));
|
||
}, []);
|
||
const { isListening: isListeningEn, start: startListeningEn, stop: stopListeningEn } = useSpeechRecognition({ onResult: handleSpeechResultEn, lang: 'en-US' });
|
||
|
||
const isModified = (field: keyof typeof formData) => {
|
||
const colMap: Record<string, number> = {
|
||
longDe: COLUMNS.LONG_DE,
|
||
longEn: COLUMNS.LONG_EN,
|
||
shortDe: COLUMNS.SHORT_DE,
|
||
shortEn: COLUMNS.SHORT_EN,
|
||
innerW: COLUMNS.INNER_W,
|
||
innerL: COLUMNS.INNER_L,
|
||
innerH: COLUMNS.INNER_H,
|
||
outerW: COLUMNS.OUTER_W,
|
||
outerL: COLUMNS.OUTER_L,
|
||
outerH: COLUMNS.OUTER_H,
|
||
unitsOuter: COLUMNS.UNITS_OUTER,
|
||
moq: COLUMNS.MOQ,
|
||
detailsDe: COLUMNS.DETAILS_DE,
|
||
detailsEn: COLUMNS.DETAILS_EN,
|
||
productType: COLUMNS.CATEGORIZATION_CODE,
|
||
itemToLogistic: COLUMNS.ITEM_TO_LOGISTIC,
|
||
};
|
||
const colIndex = (colMap as Record<string, number>)[field as string];
|
||
if (colIndex === undefined) return false;
|
||
return formData[field] !== (row[colIndex] !== undefined && row[colIndex] !== null ? String(row[colIndex]) : '');
|
||
};
|
||
|
||
const handleGenerate = (field: keyof typeof formData) => {
|
||
console.log('[EditPanel] handleGenerate called for field:', field);
|
||
setError(null); // Clear any previous error
|
||
setPendingGeminiField(field);
|
||
};
|
||
|
||
const executeGenerate = async () => {
|
||
if (!pendingGeminiField) {
|
||
console.warn('[EditPanel] executeGenerate called but no field pending');
|
||
return;
|
||
}
|
||
const field = pendingGeminiField;
|
||
console.log('[EditPanel] Starting generation for field:', field);
|
||
setLoadingField(field);
|
||
setError(null);
|
||
|
||
try {
|
||
let prompt = '';
|
||
const baseContext = `Article Name: ${row[COLUMNS.ARTICLE_NAME]}\nArticle Details (EN): ${formData.detailsEn || 'N/A'}\nArticle Details (DE): ${formData.detailsDe || 'N/A'}`;
|
||
|
||
const systemPrompt = "You are a professional copywriter and expert translator for CRAZE GmbH, a German toy company. CRITICAL: Output ONLY the translated or generated content. Do not include any introductions, conclusions, or conversational text. Translate EVERYTHING, including phrases in ALL CAPS (maintain the all-caps casing for those phrases in the translation). Your response must be complete, with all sentences finished and logically concluded. Never end mid-sentence.";
|
||
|
||
if (field === 'longDe') {
|
||
if (formData.longEn) {
|
||
prompt = `Translate the EVERYTHING below from English into professional German.
|
||
CRITICAL INSTRUCTIONS:
|
||
1. Do NOT summarize.
|
||
2. Maintain the EXACT same length and detailed information.
|
||
3. Keep all technical specs intact.
|
||
4. Translate every single paragraph into natural, commercial German for toy buyers.
|
||
5. Translate ALL text, including titles or phrases in ALL CAPS, and keep them in ALL CAPS in the translation.
|
||
6. ENSURE the translation is complete and does not cut off. The final sentence must be fully finished.
|
||
|
||
English Text to Translate:
|
||
${formData.longEn}`;
|
||
} else {
|
||
prompt = `Based on the following product details, generate a long commercial description in German. It should be fluent, oriented towards B2B toy buyers, 3-5 paragraphs long. Include features, benefits, and material if available.
|
||
IMPORTANT: Ensure the description is complete and ends with a finished sentence.\n\n${baseContext}`;
|
||
}
|
||
} else if (field === 'longEn') {
|
||
if (formData.longDe) {
|
||
prompt = `Translate the EVERYTHING below from German into professional English.
|
||
CRITICAL INSTRUCTIONS:
|
||
1. Do NOT summarize.
|
||
2. Maintain the EXACT same length and detailed information.
|
||
3. Keep all technical specs intact.
|
||
4. Translate every single paragraph into natural, commercial English for toy buyers.
|
||
5. Translate ALL text, including titles or phrases in ALL CAPS, and keep them in ALL CAPS in the translation.
|
||
6. ENSURE the translation is complete and does not cut off. The final sentence must be fully finished.
|
||
|
||
German Text to Translate:
|
||
${formData.longDe}`;
|
||
} else {
|
||
prompt = `Based on the following product details, generate a long commercial description in English. It should be fluent, oriented towards B2B toy buyers, 3-5 paragraphs long.
|
||
IMPORTANT: Ensure the description is complete and ends with a finished sentence.\n\n${baseContext}`;
|
||
}
|
||
} else if (field === 'shortDe') {
|
||
if (formData.shortEn) {
|
||
prompt = `Translate exactly this short English product description into professional German for the toy market.
|
||
IMPORTANT:
|
||
1. Translate everything, including any ALL CAPS phrases, maintaining the all-caps formatting.
|
||
2. ENSURE the translation is complete and does not cut off mid-sentence.\n\n${formData.shortEn}`;
|
||
} else if (formData.longDe) {
|
||
prompt = `Create a concise summary of the following German product description.
|
||
CRITICAL: The summary MUST be between 250 and 450 characters long.
|
||
Preserve the most important commercial highlights and features in natural, professional German for B2B toy buyers.
|
||
Do not use bullet points — write flowing prose.
|
||
IMPORTANT: Ensure the summary is a complete thought and ends with a finished sentence.\n\nOriginal description:\n${formData.longDe}`;
|
||
} else {
|
||
prompt = `Based on the following product details, generate a short commercial description in German.
|
||
CRITICAL: Length MUST be between 250 and 450 characters.
|
||
Ensure the text is complete and ends with a finished sentence.\n\n${baseContext}`;
|
||
}
|
||
} else if (field === 'shortEn') {
|
||
if (formData.shortDe) {
|
||
prompt = `Translate exactly this short German product description into professional English for the toy market.
|
||
IMPORTANT:
|
||
1. Translate everything, including any ALL CAPS phrases, maintaining the all-caps formatting.
|
||
2. ENSURE the translation is complete and does not cut off mid-sentence.\n\n${formData.shortDe}`;
|
||
} else if (formData.longEn) {
|
||
prompt = `Create a concise summary of the following English product description.
|
||
CRITICAL: The summary MUST be between 250 and 450 characters long.
|
||
Preserve the most important commercial highlights and features in natural, professional English for B2B toy buyers.
|
||
Do not use bullet points — write flowing prose.
|
||
IMPORTANT: Ensure the summary is a complete thought and ends with a finished sentence.\n\nOriginal description:\n${formData.longEn}`;
|
||
} else {
|
||
prompt = `Based on the following product details, generate a short commercial description in English.
|
||
CRITICAL: Length MUST be between 250 and 450 characters.
|
||
Ensure the text is complete and ends with a finished sentence.\n\n${baseContext}`;
|
||
}
|
||
}
|
||
|
||
const generatedText = await generateGemini(prompt, systemPrompt);
|
||
setFormData(prev => ({ ...prev, [field]: generatedText.trim() }));
|
||
setGeneratedFields(prev => new Set(prev).add(field));
|
||
// Focus the textarea so user can edit immediately after AI generation
|
||
setTimeout(() => {
|
||
const textarea = document.getElementById(`field-${field}`);
|
||
if (textarea) textarea.focus();
|
||
}, 100);
|
||
setPendingGeminiField(null);
|
||
} catch (err: any) {
|
||
setError(err.message || 'An error occurred during generation.');
|
||
setPendingGeminiField(null); // Close modal on error so error message is visible in panel
|
||
} finally {
|
||
setLoadingField(null);
|
||
}
|
||
};
|
||
|
||
const handleSave = () => {
|
||
setIsConfirmOpen(false);
|
||
const hasModifications = Object.keys(formData).some(k => isModified(k as keyof typeof formData));
|
||
if (hasModifications) {
|
||
onCaptureState(`Updated product ${row[COLUMNS.ARTICLE_NO]}`);
|
||
}
|
||
const newRow = [...row];
|
||
newRow[COLUMNS.LONG_DE] = formData.longDe;
|
||
newRow[COLUMNS.LONG_EN] = formData.longEn;
|
||
newRow[COLUMNS.SHORT_DE] = formData.shortDe;
|
||
newRow[COLUMNS.SHORT_EN] = formData.shortEn;
|
||
newRow[COLUMNS.INNER_W] = formData.innerW;
|
||
newRow[COLUMNS.INNER_L] = formData.innerL;
|
||
newRow[COLUMNS.INNER_H] = formData.innerH;
|
||
newRow[COLUMNS.OUTER_W] = formData.outerW;
|
||
newRow[COLUMNS.OUTER_L] = formData.outerL;
|
||
newRow[COLUMNS.OUTER_H] = formData.outerH;
|
||
newRow[COLUMNS.UNITS_OUTER] = formData.unitsOuter;
|
||
newRow[COLUMNS.MOQ] = formData.moq;
|
||
newRow[COLUMNS.DETAILS_DE] = formData.detailsDe;
|
||
newRow[COLUMNS.DETAILS_EN] = formData.detailsEn;
|
||
newRow[COLUMNS.CATEGORIZATION_CODE] = formData.productType;
|
||
newRow[COLUMNS.ITEM_TO_LOGISTIC] = formData.itemToLogistic;
|
||
onSave(rowIndex, newRow);
|
||
};
|
||
|
||
const handleInputKeyDown = (e: React.KeyboardEvent) => {
|
||
e.stopPropagation();
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<div className="fixed inset-0 bg-slate-950/50 backdrop-blur-sm z-40" onClick={onClose} />
|
||
<div className="fixed right-0 top-0 bottom-0 w-[600px] bg-slate-800 border-l border-slate-700 shadow-2xl z-50 flex flex-col animate-in slide-in-from-right duration-200">
|
||
<div className="flex items-center justify-between p-6 border-b border-slate-700 bg-slate-800/50">
|
||
<div>
|
||
<h2 className="text-xl font-bold text-white">Edit Product</h2>
|
||
<p className="text-sm text-slate-400 mt-1">{row[COLUMNS.ARTICLE_NO]} - {row[COLUMNS.ARTICLE_NAME]}</p>
|
||
</div>
|
||
<button onClick={onClose} className="p-2 text-slate-400 hover:text-white hover:bg-slate-700 rounded-full transition-colors">
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="flex-1 overflow-auto p-6 space-y-6">
|
||
{error && (
|
||
<div className="bg-red-500/10 border border-red-500/50 text-red-400 p-4 rounded-md text-sm">
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
<div className="grid grid-cols-2 gap-4 bg-slate-900/50 p-4 rounded-lg border border-slate-700/50">
|
||
<div>
|
||
<span className="block text-xs text-slate-500 mb-1">Line</span>
|
||
<span className="text-sm text-slate-300 font-medium">{row[COLUMNS.LINE] || '-'}</span>
|
||
</div>
|
||
<div>
|
||
<span className="block text-xs text-slate-500 mb-1">License</span>
|
||
<span className="text-sm text-slate-300 font-medium">{row[COLUMNS.LICENSE] || '-'}</span>
|
||
</div>
|
||
<div className="col-span-2">
|
||
<span className="block text-xs text-slate-500 mb-1">Details (DE)</span>
|
||
<AutoResizeTextarea
|
||
value={formData.detailsDe}
|
||
onChange={e => setFormData(prev => ({ ...prev, detailsDe: e.target.value }))}
|
||
className={cn(
|
||
"w-full min-h-[80px] bg-slate-900 border rounded p-2 text-sm text-white focus:outline-none focus:ring-1 transition-colors",
|
||
isModified('detailsDe') ? "border-blue-500 focus:ring-blue-500" : "border-slate-700/50"
|
||
)}
|
||
placeholder="Enter details in German..."
|
||
/>
|
||
</div>
|
||
<div className="col-span-2">
|
||
<span className="block text-xs text-slate-500 mb-1">Details (EN)</span>
|
||
<AutoResizeTextarea
|
||
value={formData.detailsEn}
|
||
onChange={e => setFormData(prev => ({ ...prev, detailsEn: e.target.value }))}
|
||
className={cn(
|
||
"w-full min-h-[80px] bg-slate-900 border rounded p-2 text-sm text-white focus:outline-none focus:ring-1 transition-colors",
|
||
isModified('detailsEn') ? "border-blue-500 focus:ring-blue-500" : "border-slate-700/50"
|
||
)}
|
||
placeholder="Enter details in English..."
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-4 bg-slate-900/30 p-4 rounded-lg border border-slate-700/50">
|
||
<h3 className="text-xs font-semibold text-slate-400 uppercase tracking-widest flex items-center gap-2">
|
||
<Package className="w-3 h-3" /> Dimensions & Packaging
|
||
</h3>
|
||
|
||
<div className="grid grid-cols-3 gap-3">
|
||
<div className="col-span-3 text-[10px] text-slate-500 font-medium">INNER BOX (L × W × H) cm</div>
|
||
<DimensionInput label="Length" value={formData.innerL} field="innerL" isModified={isModified('innerL')} onChange={(val) => setFormData(p => ({...p, innerL: val}))} placeholder="L" />
|
||
<DimensionInput label="Width" value={formData.innerW} field="innerW" isModified={isModified('innerW')} onChange={(val) => setFormData(p => ({...p, innerW: val}))} placeholder="W" />
|
||
<DimensionInput label="Height" value={formData.innerH} field="innerH" isModified={isModified('innerH')} onChange={(val) => setFormData(p => ({...p, innerH: val}))} placeholder="H" />
|
||
</div>
|
||
|
||
<div className="grid grid-cols-3 gap-3">
|
||
<div className="col-span-3 text-[10px] text-slate-500 font-medium">OUTER BOX (L × W × H) cm</div>
|
||
<DimensionInput label="Length" value={formData.outerL} field="outerL" isModified={isModified('outerL')} onChange={(val) => setFormData(p => ({...p, outerL: val}))} placeholder="L" />
|
||
<DimensionInput label="Width" value={formData.outerW} field="outerW" isModified={isModified('outerW')} onChange={(val) => setFormData(p => ({...p, outerW: val}))} placeholder="W" />
|
||
<DimensionInput label="Height" value={formData.outerH} field="outerH" isModified={isModified('outerH')} onChange={(val) => setFormData(p => ({...p, outerH: val}))} placeholder="H" />
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<DimensionInput label="Units per Outer" value={formData.unitsOuter} field="unitsOuter" isModified={isModified('unitsOuter')} onChange={(val) => setFormData(p => ({...p, unitsOuter: val}))} />
|
||
<DimensionInput label="MOQ" value={formData.moq} field="moq" isModified={isModified('moq')} onChange={(val) => setFormData(p => ({...p, moq: val}))} />
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-3 pt-2 border-t border-slate-700/30">
|
||
<DimensionInput label="CategorizationCode" value={formData.productType} field="productType" isModified={isModified('productType')} onChange={(val) => setFormData(p => ({...p, productType: val}))} />
|
||
<DimensionInput label="Item to Logistic" value={formData.itemToLogistic} field="itemToLogistic" isModified={isModified('itemToLogistic')} onChange={(val) => setFormData(p => ({...p, itemToLogistic: val}))} />
|
||
</div>
|
||
</div>
|
||
|
||
<FieldEditor
|
||
title="Long Description (DE)"
|
||
field="longDe"
|
||
value={formData.longDe}
|
||
isModified={isModified('longDe')}
|
||
canTranslate={!!formData.longEn}
|
||
isGenerated={generatedFields.has('longDe')}
|
||
isLoading={loadingField === 'longDe'}
|
||
onGenerate={() => handleGenerate('longDe')}
|
||
onChange={(val) => setFormData(p => ({...p, longDe: val}))}
|
||
onKeyDown={handleInputKeyDown}
|
||
isListening={isListeningDe}
|
||
onToggleVoice={isListeningDe ? stopListeningDe : startListeningDe}
|
||
voiceSupported={speechSupported}
|
||
/>
|
||
<FieldEditor
|
||
title="Long Description (EN)"
|
||
field="longEn"
|
||
value={formData.longEn}
|
||
isModified={isModified('longEn')}
|
||
canTranslate={!!formData.longDe}
|
||
isGenerated={generatedFields.has('longEn')}
|
||
isLoading={loadingField === 'longEn'}
|
||
onGenerate={() => handleGenerate('longEn')}
|
||
onChange={(val) => setFormData(p => ({...p, longEn: val}))}
|
||
onKeyDown={handleInputKeyDown}
|
||
isListening={isListeningEn}
|
||
onToggleVoice={isListeningEn ? stopListeningEn : startListeningEn}
|
||
voiceSupported={speechSupported}
|
||
/>
|
||
<FieldEditor
|
||
title="Short Description (DE)"
|
||
field="shortDe"
|
||
value={formData.shortDe}
|
||
isModified={isModified('shortDe')}
|
||
canTranslate={!!formData.shortEn}
|
||
isGenerated={generatedFields.has('shortDe')}
|
||
isLoading={loadingField === 'shortDe'}
|
||
onGenerate={() => handleGenerate('shortDe')}
|
||
onChange={(val) => setFormData(p => ({...p, shortDe: val}))}
|
||
onKeyDown={handleInputKeyDown}
|
||
/>
|
||
<FieldEditor
|
||
title="Short Description (EN)"
|
||
field="shortEn"
|
||
value={formData.shortEn}
|
||
isModified={isModified('shortEn')}
|
||
canTranslate={!!formData.shortDe}
|
||
isGenerated={generatedFields.has('shortEn')}
|
||
isLoading={loadingField === 'shortEn'}
|
||
onGenerate={() => handleGenerate('shortEn')}
|
||
onChange={(val) => setFormData(p => ({...p, shortEn: val}))}
|
||
onKeyDown={handleInputKeyDown}
|
||
/>
|
||
</div>
|
||
|
||
<div className="p-6 border-t border-slate-700 bg-slate-800/50 flex justify-end gap-3">
|
||
<button
|
||
onClick={onClose}
|
||
className="px-4 py-2 text-sm font-medium text-slate-300 hover:text-white hover:bg-slate-700 rounded-md transition-colors"
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
onClick={() => setIsConfirmOpen(true)}
|
||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md shadow-lg shadow-blue-900/20 transition-colors"
|
||
>
|
||
<Save className="w-4 h-4" />
|
||
Queue Changes
|
||
</button>
|
||
</div>
|
||
<ConfirmModal
|
||
isOpen={isConfirmOpen}
|
||
onConfirm={handleSave}
|
||
onCancel={() => setIsConfirmOpen(false)}
|
||
title="Confirm Changes"
|
||
message={`Are you sure you want to save the modifications for product ${row[COLUMNS.ARTICLE_NO]}?`}
|
||
type="info"
|
||
confirmText="Save changes"
|
||
/>
|
||
|
||
<ConfirmModal
|
||
isOpen={!!pendingGeminiField}
|
||
isLoading={loadingField !== null}
|
||
onConfirm={executeGenerate}
|
||
onCancel={() => setPendingGeminiField(null)}
|
||
title="Confirm AI Action"
|
||
message={`Are you sure you want to use Gemini to ${
|
||
((pendingGeminiField === 'shortEn' && formData.shortDe) ||
|
||
(pendingGeminiField === 'shortDe' && formData.shortEn) ||
|
||
(pendingGeminiField === 'longEn' && formData.longDe) ||
|
||
(pendingGeminiField === 'longDe' && formData.longEn))
|
||
? 'translate' : 'generate'} the ${pendingGeminiField} field?`}
|
||
type="info"
|
||
confirmText="Start Generation"
|
||
/>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|