MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 57s
296 lines
10 KiB
TypeScript
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>
|
|
);
|
|
};
|