Files
MindSift/frontend/src/features/triage/ManageColumnsModal.tsx
T
sandrodach 8b043c4809
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 57s
fix(ui): prevent modal overlays from inheriting margin-top from parent space-y containers
2026-10-25 21:20:57 +01:00

296 lines
10 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import {
X,
Plus,
Trash2,
ArrowUp,
ArrowDown,
RotateCcw,
Check,
Columns,
Layers,
} from 'lucide-react';
import { KanbanColumn } from '../../lib/types';
import { triggerHaptic } from '../../lib/api';
interface ManageColumnsModalProps {
isOpen: boolean;
onClose: () => void;
initialColumns: KanbanColumn[];
onSave: (columns: KanbanColumn[]) => Promise<void>;
listTitle?: string;
}
export const DEFAULT_KANBAN_COLUMNS: KanbanColumn[] = [
{ id: 'todo', name: 'Zu erledigen' },
{ id: 'in_progress', name: 'In Bearbeitung', color: 'accent' },
{ id: 'waiting', name: 'Wartet auf andere', color: 'amber' },
{ id: 'done', name: 'Erledigt', isDone: true, color: 'emerald' },
];
export const ManageColumnsModal: React.FC<ManageColumnsModalProps> = ({
isOpen,
onClose,
initialColumns,
onSave,
listTitle,
}) => {
if (!isOpen) return null;
const [columns, setColumns] = useState<KanbanColumn[]>(() => {
return initialColumns && initialColumns.length > 0 ? initialColumns : DEFAULT_KANBAN_COLUMNS;
});
const [newColName, setNewColName] = useState('');
const [newColIsDone, setNewColIsDone] = useState(false);
const [saving, setSaving] = useState(false);
useEffect(() => {
setColumns(initialColumns && initialColumns.length > 0 ? initialColumns : DEFAULT_KANBAN_COLUMNS);
}, [initialColumns, isOpen]);
const handleMove = (index: number, direction: 'up' | 'down') => {
triggerHaptic(10);
const targetIndex = direction === 'up' ? index - 1 : index + 1;
if (targetIndex < 0 || targetIndex >= columns.length) return;
const next = [...columns];
const temp = next[index];
next[index] = next[targetIndex];
next[targetIndex] = temp;
setColumns(next);
};
const handleRename = (id: string, newName: string) => {
setColumns((prev) =>
prev.map((c) => (c.id === id ? { ...c, name: newName } : c))
);
};
const handleToggleDone = (id: string) => {
triggerHaptic(10);
setColumns((prev) =>
prev.map((c) => (c.id === id ? { ...c, isDone: !c.isDone } : c))
);
};
const handleDelete = (id: string) => {
if (columns.length <= 1) {
alert('Mindestens eine Spalte muss vorhanden sein.');
return;
}
triggerHaptic(10);
setColumns((prev) => prev.filter((c) => c.id !== id));
};
const handleAddColumn = (e: React.FormEvent) => {
e.preventDefault();
const name = newColName.trim();
if (!name) return;
triggerHaptic(10);
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '_').slice(0, 20) || 'col';
const id = `${slug}_${Date.now().toString(36).slice(-4)}`;
setColumns((prev) => [
...prev,
{
id,
name,
isDone: newColIsDone,
},
]);
setNewColName('');
setNewColIsDone(false);
};
const handleResetToDefault = () => {
if (window.confirm('Spalten auf Standard zurücksetzen? (Zu erledigen, In Bearbeitung, Wartet auf andere, Erledigt)')) {
triggerHaptic(15);
setColumns(DEFAULT_KANBAN_COLUMNS);
}
};
const handleSave = async () => {
if (columns.length === 0) {
alert('Es muss mindestens eine Spalte vorhanden sein.');
return;
}
setSaving(true);
triggerHaptic(15);
try {
await onSave(columns);
onClose();
} catch (err: any) {
alert(err.message || 'Fehler beim Speichern der Spalten');
} finally {
setSaving(false);
}
};
return (
<div className="fixed inset-0 !m-0 modal-overlay z-50 flex items-center justify-center p-3 sm:p-4">
<div
className="w-full max-w-md panel-surface shadow-panel border border-border my-auto flex flex-col max-h-[90vh] overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-border bg-panel">
<div className="flex items-center gap-2">
<Columns className="w-4 h-4 text-accent" />
<div>
<h3 className="text-sm font-semibold text-main">Kanban-Spalten anpassen</h3>
{listTitle && (
<p className="text-[10px] text-muted truncate">Liste: {listTitle}</p>
)}
</div>
</div>
<button
type="button"
onClick={onClose}
className="text-muted hover:text-main p-1.5 transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Content */}
<div className="p-4 space-y-4 overflow-y-auto flex-1">
<p className="text-xs text-muted">
Passe die Spalten für das Kanban-Board an. Aufgaben können in jede beliebige Spalte verschoben werden.
</p>
{/* Columns List */}
<div className="space-y-2">
<label className="block text-xs font-semibold text-main">Aktive Spalten ({columns.length}):</label>
<div className="space-y-1.5 max-h-60 overflow-y-auto pr-1">
{columns.map((col, idx) => (
<div
key={col.id}
className="p-2 bg-canvas border border-border rounded-card flex items-center gap-2"
>
<div className="flex flex-col gap-0.5">
<button
type="button"
disabled={idx === 0}
onClick={() => handleMove(idx, 'up')}
className="text-muted hover:text-main disabled:opacity-20 p-0.5"
title="Nach oben"
>
<ArrowUp className="w-3 h-3" />
</button>
<button
type="button"
disabled={idx === columns.length - 1}
onClick={() => handleMove(idx, 'down')}
className="text-muted hover:text-main disabled:opacity-20 p-0.5"
title="Nach unten"
>
<ArrowDown className="w-3 h-3" />
</button>
</div>
<input
type="text"
value={col.name}
onChange={(e) => handleRename(col.id, e.target.value)}
className="flex-1 h-7 px-2 text-xs input-field"
placeholder="Spaltenname"
/>
{/* IsDone Toggle */}
<button
type="button"
onClick={() => handleToggleDone(col.id)}
className={`h-7 px-2 text-[10px] rounded-btn border transition-colors flex items-center gap-1 ${
col.isDone
? 'bg-status-ok/15 border-status-ok text-status-ok font-semibold'
: 'bg-panel border-border text-faint hover:text-muted'
}`}
title={col.isDone ? 'Gilt als Erledigt' : 'Gilt als Offen'}
>
<Check className={`w-2.5 h-2.5 ${col.isDone ? 'stroke-[3]' : 'opacity-40'}`} />
<span>Erledigt</span>
</button>
<button
type="button"
onClick={() => handleDelete(col.id)}
disabled={columns.length <= 1}
className="text-muted hover:text-status-err disabled:opacity-20 p-1 transition-colors"
title="Spalte löschen"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
))}
</div>
</div>
{/* Add New Column Form */}
<form onSubmit={handleAddColumn} className="space-y-2 pt-2 border-t border-border">
<label className="block text-xs font-semibold text-main">Neue Spalte hinzufügen:</label>
<div className="flex items-center gap-1.5">
<input
type="text"
value={newColName}
onChange={(e) => setNewColName(e.target.value)}
placeholder="z.B. Wartet auf Feedback, Review…"
className="flex-1 h-8 px-2.5 text-xs input-field"
/>
<button
type="button"
onClick={() => setNewColIsDone((prev) => !prev)}
className={`h-8 px-2.5 text-xs rounded-btn border transition-colors flex items-center gap-1 ${
newColIsDone
? 'bg-status-ok/15 border-status-ok text-status-ok font-semibold'
: 'bg-panel border-border text-muted hover:text-main'
}`}
title="Sollen Aufgaben in dieser Spalte als erledigt gelten?"
>
<Check className="w-3 h-3" />
<span>Erledigt</span>
</button>
<button
type="submit"
disabled={!newColName.trim()}
className="btn-primary h-8 px-3 text-xs flex items-center gap-1 disabled:opacity-40"
>
<Plus className="w-3.5 h-3.5" />
<span>Hinzufügen</span>
</button>
</div>
</form>
{/* Reset button */}
<div className="pt-2 flex justify-start">
<button
type="button"
onClick={handleResetToDefault}
className="text-xs text-muted hover:text-main flex items-center gap-1"
>
<RotateCcw className="w-3 h-3 text-accent" />
<span>Auf Standard-Spalten zurücksetzen</span>
</button>
</div>
</div>
{/* Footer */}
<div className="flex items-center justify-end gap-2 px-4 py-2.5 border-t border-border bg-panel">
<button
type="button"
onClick={onClose}
className="btn-action h-7 px-3 text-xs"
>
Abbrechen
</button>
<button
type="button"
onClick={handleSave}
disabled={saving}
className="btn-primary h-7 px-3 text-xs disabled:opacity-50"
>
Spalten speichern
</button>
</div>
</div>
</div>
);
};