feat(tasks): customizable kanban columns, task detail page, hashtag filtering and clean task list
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m0s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m0s
This commit is contained in:
1 parent
0afa2f9c2d
commit
b5df4a34b4
8 files changed
+1847
-689
No files matched your search
@@ -0,0 +1,295 @@
|
||||
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 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>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user