feat: include article details (DE/EN) in completeness check and make them editable

This commit is contained in:
Christian Vidal Wolf
2026-04-07 13:38:21 +02:00
parent 32c392d470
commit ec1b506341
3 changed files with 59 additions and 15 deletions
+3 -1
View File
@@ -21,6 +21,8 @@ export function DataCompleteness({ data, headers }: DataCompletenessProps) {
COLUMNS.TARIFF_CODE, COLUMNS.TARIFF_CODE,
COLUMNS.COUNTRY_ORIGIN, COLUMNS.COUNTRY_ORIGIN,
COLUMNS.RECOMMENDED_AGE, COLUMNS.RECOMMENDED_AGE,
COLUMNS.DETAILS_DE,
COLUMNS.DETAILS_EN,
COLUMNS.LONG_DE, COLUMNS.LONG_DE,
COLUMNS.LONG_EN, COLUMNS.LONG_EN,
COLUMNS.SHORT_DE, COLUMNS.SHORT_DE,
@@ -70,7 +72,7 @@ export function DataCompleteness({ data, headers }: DataCompletenessProps) {
<div className="flex flex-col h-full bg-slate-800 rounded-xl border border-slate-700 shadow-xl overflow-hidden"> <div className="flex flex-col h-full bg-slate-800 rounded-xl border border-slate-700 shadow-xl overflow-hidden">
<div className="p-4 border-b border-slate-700 bg-slate-800/50"> <div className="p-4 border-b border-slate-700 bg-slate-800/50">
<h2 className="text-lg font-semibold text-white">Data Completeness</h2> <h2 className="text-lg font-semibold text-white">Data Completeness</h2>
<p className="text-sm text-slate-400">Evaluating 10 key fields per product.</p> <p className="text-sm text-slate-400">Evaluating 12 key fields per product.</p>
</div> </div>
<div className="overflow-x-auto flex-1"> <div className="overflow-x-auto flex-1">
+28 -4
View File
@@ -19,6 +19,8 @@ export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: Ed
longEn: row[COLUMNS.LONG_EN] || '', longEn: row[COLUMNS.LONG_EN] || '',
shortDe: row[COLUMNS.SHORT_DE] || '', shortDe: row[COLUMNS.SHORT_DE] || '',
shortEn: row[COLUMNS.SHORT_EN] || '', shortEn: row[COLUMNS.SHORT_EN] || '',
detailsDe: row[COLUMNS.DETAILS_DE] || '',
detailsEn: row[COLUMNS.DETAILS_EN] || '',
innerW: row[COLUMNS.INNER_W] || '', innerW: row[COLUMNS.INNER_W] || '',
innerL: row[COLUMNS.INNER_L] || '', innerL: row[COLUMNS.INNER_L] || '',
innerH: row[COLUMNS.INNER_H] || '', innerH: row[COLUMNS.INNER_H] || '',
@@ -48,8 +50,12 @@ export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: Ed
outerH: COLUMNS.OUTER_H, outerH: COLUMNS.OUTER_H,
unitsOuter: COLUMNS.UNITS_OUTER, unitsOuter: COLUMNS.UNITS_OUTER,
moq: COLUMNS.MOQ, moq: COLUMNS.MOQ,
detailsDe: COLUMNS.DETAILS_DE,
detailsEn: COLUMNS.DETAILS_EN,
}; };
return formData[field] !== (row[colMap[field]] || ''); const colIndex = (colMap as Record<string, number>)[field as string];
if (colIndex === undefined) return false;
return formData[field] !== (row[colIndex] || '');
}; };
const handleGenerate = async (field: keyof typeof formData) => { const handleGenerate = async (field: keyof typeof formData) => {
@@ -65,7 +71,7 @@ export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: Ed
try { try {
let prompt = ''; let prompt = '';
const baseContext = `Article Name: ${row[COLUMNS.ARTICLE_NAME]}\nArticle Details (EN): ${row[COLUMNS.DETAILS_EN] || 'N/A'}\nArticle Details (DE): ${row[COLUMNS.DETAILS_DE] || 'N/A'}`; 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. You excel at translating product descriptions between German and English, maintaining the commercial and professional tone while ensuring all technical toy details are accurate. Use clear, engaging language."; const systemPrompt = "You are a professional copywriter and expert translator for CRAZE GmbH, a German toy company. You excel at translating product descriptions between German and English, maintaining the commercial and professional tone while ensuring all technical toy details are accurate. Use clear, engaging language.";
@@ -143,6 +149,8 @@ export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: Ed
newRow[COLUMNS.OUTER_H] = formData.outerH; newRow[COLUMNS.OUTER_H] = formData.outerH;
newRow[COLUMNS.UNITS_OUTER] = formData.unitsOuter; newRow[COLUMNS.UNITS_OUTER] = formData.unitsOuter;
newRow[COLUMNS.MOQ] = formData.moq; newRow[COLUMNS.MOQ] = formData.moq;
newRow[COLUMNS.DETAILS_DE] = formData.detailsDe;
newRow[COLUMNS.DETAILS_EN] = formData.detailsEn;
onSave(rowIndex, newRow); onSave(rowIndex, newRow);
}; };
@@ -230,11 +238,27 @@ export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: Ed
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<span className="block text-xs text-slate-500 mb-1">Details (DE)</span> <span className="block text-xs text-slate-500 mb-1">Details (DE)</span>
<p className="text-sm text-slate-300 line-clamp-2" title={row[COLUMNS.DETAILS_DE]}>{row[COLUMNS.DETAILS_DE] || '-'}</p> <textarea
value={formData.detailsDe}
onChange={e => setFormData(prev => ({ ...prev, detailsDe: e.target.value }))}
className={cn(
"w-full h-20 bg-slate-900 border rounded p-2 text-sm text-white focus:outline-none focus:ring-1 transition-colors resize-none",
isModified('detailsDe') ? "border-blue-500 focus:ring-blue-500" : "border-slate-700/50"
)}
placeholder="Enter details in German..."
/>
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<span className="block text-xs text-slate-500 mb-1">Details (EN)</span> <span className="block text-xs text-slate-500 mb-1">Details (EN)</span>
<p className="text-sm text-slate-300 line-clamp-2" title={row[COLUMNS.DETAILS_EN]}>{row[COLUMNS.DETAILS_EN] || '-'}</p> <textarea
value={formData.detailsEn}
onChange={e => setFormData(prev => ({ ...prev, detailsEn: e.target.value }))}
className={cn(
"w-full h-20 bg-slate-900 border rounded p-2 text-sm text-white focus:outline-none focus:ring-1 transition-colors resize-none",
isModified('detailsEn') ? "border-blue-500 focus:ring-blue-500" : "border-slate-700/50"
)}
placeholder="Enter details in English..."
/>
</div> </div>
</div> </div>
+28 -10
View File
@@ -8,7 +8,7 @@ interface ProductDescriptionsProps {
onEdit: (index: number) => void; onEdit: (index: number) => void;
} }
type TabType = 'all' | 'missingDeLong' | 'missingEnLong' | 'missingDeShort' | 'missingEnShort' | 'complete' | 'incomplete'; type TabType = 'all' | 'missingDeLong' | 'missingEnLong' | 'missingDeShort' | 'missingEnShort' | 'missingDeDetails' | 'missingEnDetails' | 'complete' | 'incomplete';
export function ProductDescriptions({ data, onEdit }: ProductDescriptionsProps) { export function ProductDescriptions({ data, onEdit }: ProductDescriptionsProps) {
const [activeTab, setActiveTab] = useState<TabType>('all'); const [activeTab, setActiveTab] = useState<TabType>('all');
@@ -31,8 +31,18 @@ export function ProductDescriptions({ data, onEdit }: ProductDescriptionsProps)
if (activeTab === 'missingEnLong') result = result.filter(r => !r.row[COLUMNS.LONG_EN]); if (activeTab === 'missingEnLong') result = result.filter(r => !r.row[COLUMNS.LONG_EN]);
if (activeTab === 'missingDeShort') result = result.filter(r => !r.row[COLUMNS.SHORT_DE]); if (activeTab === 'missingDeShort') result = result.filter(r => !r.row[COLUMNS.SHORT_DE]);
if (activeTab === 'missingEnShort') result = result.filter(r => !r.row[COLUMNS.SHORT_EN]); if (activeTab === 'missingEnShort') result = result.filter(r => !r.row[COLUMNS.SHORT_EN]);
if (activeTab === 'complete') result = result.filter(r => r.row[COLUMNS.LONG_DE] && r.row[COLUMNS.LONG_EN] && r.row[COLUMNS.SHORT_DE] && r.row[COLUMNS.SHORT_EN]); if (activeTab === 'missingDeDetails') result = result.filter(r => !r.row[COLUMNS.DETAILS_DE]);
if (activeTab === 'incomplete') result = result.filter(r => !r.row[COLUMNS.LONG_DE] || !r.row[COLUMNS.LONG_EN] || !r.row[COLUMNS.SHORT_DE] || !r.row[COLUMNS.SHORT_EN]); if (activeTab === 'missingEnDetails') result = result.filter(r => !r.row[COLUMNS.DETAILS_EN]);
if (activeTab === 'complete') result = result.filter(r =>
r.row[COLUMNS.LONG_DE] && r.row[COLUMNS.LONG_EN] &&
r.row[COLUMNS.SHORT_DE] && r.row[COLUMNS.SHORT_EN] &&
r.row[COLUMNS.DETAILS_DE] && r.row[COLUMNS.DETAILS_EN]
);
if (activeTab === 'incomplete') result = result.filter(r =>
!r.row[COLUMNS.LONG_DE] || !r.row[COLUMNS.LONG_EN] ||
!r.row[COLUMNS.SHORT_DE] || !r.row[COLUMNS.SHORT_EN] ||
!r.row[COLUMNS.DETAILS_DE] || !r.row[COLUMNS.DETAILS_EN]
);
// Search filter // Search filter
if (search) { if (search) {
@@ -84,9 +94,13 @@ export function ProductDescriptions({ data, onEdit }: ProductDescriptionsProps)
return 'bg-yellow-500/10 hover:bg-yellow-500/20'; // EOL Highlight return 'bg-yellow-500/10 hover:bg-yellow-500/20'; // EOL Highlight
} }
const fields = [row[COLUMNS.LONG_DE], row[COLUMNS.LONG_EN], row[COLUMNS.SHORT_DE], row[COLUMNS.SHORT_EN]]; const fields = [
row[COLUMNS.LONG_DE], row[COLUMNS.LONG_EN],
row[COLUMNS.SHORT_DE], row[COLUMNS.SHORT_EN],
row[COLUMNS.DETAILS_DE], row[COLUMNS.DETAILS_EN]
];
const filled = fields.filter(Boolean).length; const filled = fields.filter(Boolean).length;
if (filled === 4) return 'bg-green-900/10 hover:bg-green-900/20'; if (filled === 6) return 'bg-green-900/10 hover:bg-green-900/20';
if (filled === 0) return 'bg-red-900/10 hover:bg-red-900/20'; if (filled === 0) return 'bg-red-900/10 hover:bg-red-900/20';
return 'bg-yellow-900/10 hover:bg-yellow-900/20'; return 'bg-yellow-900/10 hover:bg-yellow-900/20';
}; };
@@ -111,6 +125,8 @@ export function ProductDescriptions({ data, onEdit }: ProductDescriptionsProps)
{ id: 'missingEnLong', label: 'Missing EN Long' }, { id: 'missingEnLong', label: 'Missing EN Long' },
{ id: 'missingDeShort', label: 'Missing DE Short' }, { id: 'missingDeShort', label: 'Missing DE Short' },
{ id: 'missingEnShort', label: 'Missing EN Short' }, { id: 'missingEnShort', label: 'Missing EN Short' },
{ id: 'missingDeDetails', label: 'Missing DE Details' },
{ id: 'missingEnDetails', label: 'Missing EN Details' },
{ id: 'complete', label: 'Complete' }, { id: 'complete', label: 'Complete' },
{ id: 'incomplete', label: 'Incomplete' }, { id: 'incomplete', label: 'Incomplete' },
]; ];
@@ -172,7 +188,8 @@ export function ProductDescriptions({ data, onEdit }: ProductDescriptionsProps)
{ col: COLUMNS.ARTICLE_NO, label: 'Article No.' }, { col: COLUMNS.ARTICLE_NO, label: 'Article No.' },
{ col: COLUMNS.ARTICLE_NAME, label: 'Article Name' }, { col: COLUMNS.ARTICLE_NAME, label: 'Article Name' },
{ col: COLUMNS.LINE, label: 'Line' }, { col: COLUMNS.LINE, label: 'Line' },
{ col: COLUMNS.LICENSE, label: 'License' }, { col: COLUMNS.DETAILS_DE, label: 'Details DE' },
{ col: COLUMNS.DETAILS_EN, label: 'Details EN' },
{ col: COLUMNS.LONG_DE, label: 'Long DE' }, { col: COLUMNS.LONG_DE, label: 'Long DE' },
{ col: COLUMNS.LONG_EN, label: 'Long EN' }, { col: COLUMNS.LONG_EN, label: 'Long EN' },
{ col: COLUMNS.SHORT_DE, label: 'Short DE' }, { col: COLUMNS.SHORT_DE, label: 'Short DE' },
@@ -197,10 +214,11 @@ export function ProductDescriptions({ data, onEdit }: ProductDescriptionsProps)
<tbody className="divide-y divide-slate-700/50"> <tbody className="divide-y divide-slate-700/50">
{paginatedData.map(({ row, index }) => ( {paginatedData.map(({ row, index }) => (
<tr key={index} className={cn("transition-colors", getRowColor(row))}> <tr key={index} className={cn("transition-colors", getRowColor(row))}>
<td className="px-4 py-3 font-mono text-slate-300">{row[COLUMNS.ARTICLE_NO]}</td> <td className="px-4 py-3 font-mono text-slate-300 text-xs">{row[COLUMNS.ARTICLE_NO]}</td>
<td className="px-4 py-3 font-medium text-white max-w-[300px] truncate" title={row[COLUMNS.ARTICLE_NAME]}>{row[COLUMNS.ARTICLE_NAME]}</td> <td className="px-4 py-3 font-medium text-white max-w-[200px] truncate" title={row[COLUMNS.ARTICLE_NAME]}>{row[COLUMNS.ARTICLE_NAME]}</td>
<td className="px-4 py-3 text-slate-300">{row[COLUMNS.LINE]}</td> <td className="px-4 py-3 text-slate-300 text-xs">{row[COLUMNS.LINE]}</td>
<td className="px-4 py-3 text-slate-300">{row[COLUMNS.LICENSE]}</td> <td className="px-4 py-3"><Badge content={row[COLUMNS.DETAILS_DE]} row={row} /></td>
<td className="px-4 py-3"><Badge content={row[COLUMNS.DETAILS_EN]} row={row} /></td>
<td className="px-4 py-3"><Badge content={row[COLUMNS.LONG_DE]} row={row} /></td> <td className="px-4 py-3"><Badge content={row[COLUMNS.LONG_DE]} row={row} /></td>
<td className="px-4 py-3"><Badge content={row[COLUMNS.LONG_EN]} row={row} /></td> <td className="px-4 py-3"><Badge content={row[COLUMNS.LONG_EN]} row={row} /></td>
<td className="px-4 py-3"><Badge content={row[COLUMNS.SHORT_DE]} row={row} /></td> <td className="px-4 py-3"><Badge content={row[COLUMNS.SHORT_DE]} row={row} /></td>