diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b64bc22..e3ab213 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -132,11 +132,12 @@ export const App: React.FC = () => { // Mobile Navigation & Quick-Action State const [showMobileCapture, setShowMobileCapture] = useState(false); const [showMobileProfile, setShowMobileProfile] = useState(false); - const { isMenuOpen, setIsMenuOpen, closeMenu, setGroups } = useMobileActions(); + const { isMenuOpen, setIsMenuOpen, closeMenu, setGroups, primaryAction, setPrimaryAction } = useMobileActions(); useEffect(() => { closeMenu(); if (activeTab === 'triage') { + setPrimaryAction(() => setShowMobileCapture(true)); setGroups([ { id: 'triage-actions', @@ -966,7 +967,11 @@ export const App: React.FC = () => { type="button" onClick={() => { triggerHaptic(15); - setShowMobileCapture(true); + if (primaryAction) { + primaryAction(); + } else { + setShowMobileCapture(true); + } }} className="w-14 h-14 -mt-6 rounded-full bg-accent text-on-accent shadow-lg flex items-center justify-center active:scale-90 transition-transform border-4 border-panel cursor-pointer" title="Schnell erfassen (+)" diff --git a/frontend/src/context/MobileActionContext.tsx b/frontend/src/context/MobileActionContext.tsx index ef661a4..1ea376d 100644 --- a/frontend/src/context/MobileActionContext.tsx +++ b/frontend/src/context/MobileActionContext.tsx @@ -22,6 +22,8 @@ interface MobileActionContextValue { isMenuOpen: boolean; setIsMenuOpen: React.Dispatch>; closeMenu: () => void; + primaryAction?: () => void; + setPrimaryAction: (fn?: () => void) => void; } const MobileActionContext = createContext({ @@ -30,11 +32,14 @@ const MobileActionContext = createContext({ isMenuOpen: false, setIsMenuOpen: () => {}, closeMenu: () => {}, + primaryAction: undefined, + setPrimaryAction: () => {}, }); export const MobileActionProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [groups, setGroupsState] = useState([]); const [isMenuOpen, setIsMenuOpen] = useState(false); + const [primaryAction, setPrimaryActionState] = useState<(() => void) | undefined>(undefined); const setGroups = useCallback((newGroups: MobileMenuGroup[]) => { setGroupsState(newGroups); @@ -44,6 +49,10 @@ export const MobileActionProvider: React.FC<{ children: React.ReactNode }> = ({ setIsMenuOpen(false); }, []); + const setPrimaryAction = useCallback((fn?: () => void) => { + setPrimaryActionState(() => fn); + }, []); + return ( = ({ isMenuOpen, setIsMenuOpen, closeMenu, + primaryAction, + setPrimaryAction, }} > {children} diff --git a/frontend/src/features/lists/SharedListsView.tsx b/frontend/src/features/lists/SharedListsView.tsx index 7605cc7..2fd00e0 100644 --- a/frontend/src/features/lists/SharedListsView.tsx +++ b/frontend/src/features/lists/SharedListsView.tsx @@ -77,7 +77,7 @@ export function deduplicateItems(items: ListItem[]): ListItem[] { } export const SharedListsView: React.FC = ({ onRefreshGlobal, currentUser }) => { - const { setGroups } = useMobileActions(); + const { setGroups, setPrimaryAction } = useMobileActions(); const [lists, setLists] = useState([]); const [activeListId, setActiveListId] = useState(''); @@ -714,6 +714,13 @@ export const SharedListsView: React.FC = ({ onRefreshGloba // Register context-sensitive mobile menu groups behind the MindSift logo useEffect(() => { + setPrimaryAction(() => { + setTimeout(() => { + inputRef.current?.focus(); + inputRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }); + }, 60); + }); + setGroups([ { id: 'lists-actions', @@ -786,7 +793,7 @@ export const SharedListsView: React.FC = ({ onRefreshGloba }), }, ]); - }, [lists, activeList, completedItems.length, setGroups]); + }, [lists, activeList, completedItems.length, setGroups, setPrimaryAction]); return (
diff --git a/frontend/src/features/triage/TasksView.tsx b/frontend/src/features/triage/TasksView.tsx index 41465f4..5c0d6d2 100644 --- a/frontend/src/features/triage/TasksView.tsx +++ b/frontend/src/features/triage/TasksView.tsx @@ -23,8 +23,9 @@ import { Settings, ChevronDown, Folder, + Filter, } from 'lucide-react'; -import { TaskList, Thought, SubTask, Team, User, TaskListShare, KanbanColumn } from '../../lib/types'; +import { TaskList, Thought, SubTask, Team, User, TaskListShare, KanbanColumn, PriorityLevel } from '../../lib/types'; import { api, triggerHaptic } from '../../lib/api'; import { UnifiedShareModal } from '../../components/UnifiedShareModal'; import { TaskDetailModal } from './TaskDetailModal'; @@ -73,7 +74,15 @@ export const TasksView: React.FC = ({ const [quickTitle, setQuickTitle] = useState(''); const [quickDueDate, setQuickDueDate] = useState(''); const quickInputRef = useRef(null); - const { setGroups } = useMobileActions(); + const { setGroups, setPrimaryAction } = useMobileActions(); + + // Mobile task creation & filter modal state + const [showMobileCreateModal, setShowMobileCreateModal] = useState(false); + const [mobileCreateTitle, setMobileCreateTitle] = useState(''); + const [mobileCreateDueDate, setMobileCreateDueDate] = useState(''); + const [mobileCreatePriority, setMobileCreatePriority] = useState('normal'); + const [mobileCreateListId, setMobileCreateListId] = useState(''); + const [showFilterModal, setShowFilterModal] = useState(false); // New TaskList modal state const [showNewListModal, setShowNewListModal] = useState(false); @@ -530,6 +539,31 @@ export const TasksView: React.FC = ({ } }; + const handleMobileCreateTask = async (e: React.FormEvent) => { + e.preventDefault(); + const title = mobileCreateTitle.trim(); + if (!title) return; + triggerHaptic(12); + + try { + const created = await api.captureThought(title, mobileCreatePriority, 'manual', mobileCreateDueDate || undefined, { + target: 'task', + taskListId: mobileCreateListId || (selectedListId === 'all' ? '' : selectedListId), + }); + setTasks((prev) => { + if (prev.some((t) => t.id === created.id)) return prev; + return [created, ...prev]; + }); + setMobileCreateTitle(''); + setMobileCreateDueDate(''); + setMobileCreatePriority('normal'); + setShowMobileCreateModal(false); + onRefreshStats(); + } catch { + loadData(); + } + }; + const handleQuickAddTaskToColumn = async (columnId: string) => { const title = prompt('Neue Aufgabe für diese Spalte:'); if (!title || !title.trim()) return; @@ -815,6 +849,11 @@ export const TasksView: React.FC = ({ }; useEffect(() => { + setPrimaryAction(() => { + setMobileCreateListId(selectedListId === 'all' ? '' : selectedListId); + setShowMobileCreateModal(true); + }); + const primaryItems: any[] = [ { id: 'new-task', @@ -822,12 +861,26 @@ export const TasksView: React.FC = ({ icon: , variant: 'accent', onClick: () => { - window.scrollTo({ top: 0, behavior: 'smooth' }); - setTimeout(() => quickInputRef.current?.focus(), 100); + setMobileCreateListId(selectedListId === 'all' ? '' : selectedListId); + setShowMobileCreateModal(true); }, }, ]; + const filterSortActiveCount = + (selectedTag ? 1 : 0) + (groupBy !== 'none' ? 1 : 0) + (sortBy !== 'custom' ? 1 : 0); + + const filterItems: any[] = [ + { + id: 'tasks-filter-modal', + label: 'Filter, Hashtags & Sortierung', + icon: , + badge: filterSortActiveCount > 0 ? `${filterSortActiveCount} aktiv` : undefined, + active: filterSortActiveCount > 0, + onClick: () => setShowFilterModal(true), + }, + ]; + const listManagementItems: any[] = [ { id: 'new-task-list', @@ -928,6 +981,11 @@ export const TasksView: React.FC = ({ id: 'tasks-primary', items: primaryItems, }, + { + id: 'tasks-filters', + title: 'Filter & Sortierung', + items: filterItems, + }, { id: 'tasks-list-management', title: 'Listen-Verwaltung', @@ -948,12 +1006,17 @@ export const TasksView: React.FC = ({ viewMode, filter, selectedListId, + selectedTag, + groupBy, + sortBy, openTasks.length, doneTasks.length, safeDiscarded.length, kanbanTasks.length, safeTaskLists, currentSelectedList, + setPrimaryAction, + setGroups, ]); const renderTaskProgressBar = (task: Thought) => { @@ -1071,11 +1134,11 @@ export const TasksView: React.FC = ({ className="flex flex-wrap items-center gap-1.5 pt-0.5 empty:hidden" onClick={(e) => e.stopPropagation()} > - {/* Kanban status selector pill */} + {/* Kanban status selector pill (Desktop only - on mobile done via TaskDetailModal) */} = ({
- {/* Hashtag Filter & Grouping / Sorting Toolbar */} -
+ {/* Mobile Active Filter Badge Banner (minimal and clean) */} + {(selectedTag || groupBy !== 'none' || sortBy !== 'custom') && ( +
+
+ + {selectedTag && ( + + #{selectedTag} + + + )} + {groupBy !== 'none' && ( + + Gruppe: {groupBy} + + )} + {sortBy !== 'custom' && ( + + Sortiert: {sortBy} + + )} +
+ +
+ )} + + {/* Hashtag Filter & Grouping / Sorting Toolbar (Desktop only - on mobile accessible via Logo Menu Filter & Sort) */} +
{/* Hashtag filter chips */}
@@ -1497,6 +1602,264 @@ export const TasksView: React.FC = ({
+ {/* MOBILE CREATE TASK BOTTOM SHEET MODAL */} + {showMobileCreateModal && ( +
setShowMobileCreateModal(false)} + > +
e.stopPropagation()} + > +
+
+
+ +
+

Neue Aufgabe anlegen

+
+ +
+ +
+
+ + setMobileCreateTitle(e.target.value)} + placeholder="Was ist zu tun?…" + className="w-full input-field px-3 py-2 text-sm" + /> +
+ + {/* List Selector (if lists exist) */} + {safeTaskLists.length > 0 && ( +
+ + +
+ )} + + {/* Priority & Due Date */} +
+
+ + +
+ +
+ + setMobileCreateDueDate(e.target.value)} + className="w-full input-field px-2 py-2 text-xs" + /> +
+
+ +
+ + +
+
+
+
+ )} + + {/* MOBILE FILTER & SORT MODAL */} + {showFilterModal && ( +
setShowFilterModal(false)} + > +
e.stopPropagation()} + > +
+
+
+ +
+

Filter & Sortierung

+
+ +
+ + {/* Hashtag Filters */} +
+ + + Nach Hashtags filtern + +
+ + {allAvailableTags.map((tag) => { + const count = safeTasks.filter((t) => t.tags?.includes(tag)).length; + const isSelected = selectedTag === tag; + return ( + + ); + })} +
+
+ + {/* Grouping */} + {viewMode !== 'kanban' && ( +
+ + Gruppierung + +
+ {[ + { id: 'none', label: 'Keine' }, + { id: 'tag', label: 'Nach Hashtag' }, + { id: 'status', label: 'Nach Status' }, + { id: 'priority', label: 'Nach Priorität' }, + { id: 'due', label: 'Nach Fälligkeit' }, + ].map((g) => ( + + ))} +
+
+ )} + + {/* Sorting */} + {viewMode !== 'kanban' && ( +
+ + Sortierung + +
+ {[ + { id: 'custom', label: 'Manuell (Drag & Drop)' }, + { id: 'created', label: 'Erstelldatum' }, + { id: 'due', label: 'Fälligkeitsdatum' }, + { id: 'priority', label: 'Priorität' }, + { id: 'alpha', label: 'Alphabetisch' }, + ].map((s) => ( + + ))} +
+
+ )} + +
+ + +
+
+
+ )} + {/* NEW LIST MODAL */} {showNewListModal && (
diff --git a/frontend/src/features/workspace/WorkspaceView.tsx b/frontend/src/features/workspace/WorkspaceView.tsx index 37d61f2..25d2196 100644 --- a/frontend/src/features/workspace/WorkspaceView.tsx +++ b/frontend/src/features/workspace/WorkspaceView.tsx @@ -100,7 +100,7 @@ export const WorkspaceView: React.FC = ({ initialDocId, onStatsChange, }) => { - const { setGroups } = useMobileActions(); + const { setGroups, setPrimaryAction } = useMobileActions(); const [showMobileFileDrawer, setShowMobileFileDrawer] = useState(false); // Top Workspace Tab Mode: 'editor' vs 'todos' vs 'tags' @@ -2836,6 +2836,10 @@ export const WorkspaceView: React.FC = ({ // Register context-sensitive mobile menu groups behind the MindSift logo useEffect(() => { + setPrimaryAction(() => { + handleCreateDoc(null); + }); + const openTodosCount = allWorkspaceTodos.filter((t) => !t.block.checked).length; setGroups([ { @@ -2958,6 +2962,7 @@ export const WorkspaceView: React.FC = ({ allWorkspaceTodos, selectedDoc, setGroups, + setPrimaryAction, ]); const renderDocItem = (doc: Document, depth: number) => { @@ -3186,49 +3191,6 @@ export const WorkspaceView: React.FC = ({ return (
- {/* Mobile Compact Workspace Bar (Options & mode switching live in MindSift Logo Menu) */} -
- - -
- {workspaceMode === 'editor' && selectedDoc && ( - - - {saveState === 'saving' ? 'Speichert…' : 'Gespeichert'} - - )} - -
-
{/* Top Navigation Mode Switcher: Notizen & Outliner vs. Globales To-Do Dashboard (Desktop) */}
@@ -4342,8 +4304,30 @@ export const WorkspaceView: React.FC = ({
) : ( -
- Wähle links eine Datei im Datei-Explorer aus oder erstelle eine neue Datei. +
+ +

Keine Notiz geöffnet.

+
+ + +
)}