diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 5d639c8..35d3abe 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -20,7 +20,8 @@ "resizable": true, "fullscreen": false, "center": true, - "backgroundColor": "#0f1117" + "backgroundColor": "#0f1117", + "dragDropEnabled": false } ], "security": { diff --git a/src/components/NotificationBell.tsx b/src/components/NotificationBell.tsx index 5093d81..af902ec 100644 --- a/src/components/NotificationBell.tsx +++ b/src/components/NotificationBell.tsx @@ -5,11 +5,13 @@ import { formatDistanceToNow } from 'date-fns' import { fr } from 'date-fns/locale' import { supa } from '../lib/supabase' import { useApp } from '../lib/AppContext' +import { useFeedback } from '../lib/feedback' import { AppNotification } from '../lib/types' import type { Page } from './Layout' export default function NotificationBell({ setPage }: { setPage: (p: Page) => void }): JSX.Element { - const { profile } = useApp() + const { profile, setFocusEntity } = useApp() + const { toast } = useFeedback() const [items, setItems] = useState([]) const [open, setOpen] = useState(false) const ref = useRef(null) @@ -39,13 +41,20 @@ export default function NotificationBell({ setPage }: { setPage: (p: Page) => vo .on( 'postgres_changes', { event: '*', schema: 'public', table: 'notifications', filter: `user_id=eq.${uid}` }, - () => load() + (payload) => { + // Toast à la réception d'une nouvelle notification. + if (payload.eventType === 'INSERT') { + const n = payload.new as AppNotification + toast(n.body ? `${n.title} : ${n.body}` : n.title, 'info') + } + load() + } ) .subscribe() return () => { supa().removeChannel(ch) } - }, [uid, load]) + }, [uid, load, toast]) // Fermeture au clic extérieur. useEffect(() => { @@ -70,6 +79,8 @@ export default function NotificationBell({ setPage }: { setPage: (p: Page) => vo setItems((list) => list.map((x) => (x.id === n.id ? { ...x, read: true } : x))) await supa().from('notifications').update({ read: true }).eq('id', n.id) } + // Ouvre directement la tâche / le bug concerné(e) sur la page cible. + if (n.entity_id) setFocusEntity({ kind: n.kind, id: n.entity_id }) setPage(n.kind === 'bug' ? 'bugs' : 'kanban') } diff --git a/src/lib/AppContext.tsx b/src/lib/AppContext.tsx index e7d3aeb..eb43782 100644 --- a/src/lib/AppContext.tsx +++ b/src/lib/AppContext.tsx @@ -6,6 +6,11 @@ import { Profile, Project } from './types' type Stage = 'loading' | 'setup' | 'login' | 'ready' +export interface FocusEntity { + kind: 'task' | 'bug' + id: string +} + interface AppState { stage: Stage session: Session | null @@ -17,6 +22,9 @@ interface AppState { refreshProjects: () => Promise refreshProfile: () => Promise signOut: () => Promise + // Élément à ouvrir (depuis une notification) : la page cible s'en saisit. + focusEntity: FocusEntity | null + setFocusEntity: (f: FocusEntity | null) => void } const Ctx = createContext(null) @@ -33,6 +41,7 @@ export function AppProvider({ children }: { children: ReactNode }): JSX.Element const [profile, setProfile] = useState(null) const [projects, setProjects] = useState([]) const [currentProject, setCurrentProject] = useState(null) + const [focusEntity, setFocusEntity] = useState(null) const refreshProfile = useCallback(async () => { const uid = (await supa().auth.getUser()).data.user?.id @@ -109,7 +118,9 @@ export function AppProvider({ children }: { children: ReactNode }): JSX.Element saveConfig, refreshProjects, refreshProfile, - signOut + signOut, + focusEntity, + setFocusEntity }} > {children} diff --git a/src/pages/Bugs.tsx b/src/pages/Bugs.tsx index 4e2f3c2..9252a3e 100644 --- a/src/pages/Bugs.tsx +++ b/src/pages/Bugs.tsx @@ -33,7 +33,7 @@ const STATUS_STYLES: Record = { } export default function Bugs(): JSX.Element { - const { currentProject, profile } = useApp() + const { currentProject, profile, focusEntity, setFocusEntity } = useApp() const { members, byId } = useMembers() const [bugs, setBugs] = useState([]) const [filter, setFilter] = useState('all') @@ -55,6 +55,16 @@ export default function Bugs(): JSX.Element { load() }, [load]) + // Ouverture d'un bug demandé par une notification. + useEffect(() => { + if (focusEntity?.kind !== 'bug') return + const b = bugs.find((x) => x.id === focusEntity.id) + if (b) { + setEditing(b) + setFocusEntity(null) + } + }, [focusEntity, bugs, setFocusEntity]) + const shown = bugs.filter((b) => filter === 'all' || b.status === filter) const openCount = bugs.filter((b) => b.status === 'open' || b.status === 'in_progress').length diff --git a/src/pages/Kanban.tsx b/src/pages/Kanban.tsx index 38818a4..1725a55 100644 --- a/src/pages/Kanban.tsx +++ b/src/pages/Kanban.tsx @@ -29,7 +29,7 @@ const STATUS_ACCENT: Record([]) const [times, setTimes] = useState>({}) @@ -76,6 +76,16 @@ export default function Kanban(): JSX.Element { load() }, [load]) + // Ouverture d'une tâche demandée par une notification. + useEffect(() => { + if (focusEntity?.kind !== 'task') return + const t = tasks.find((x) => x.id === focusEntity.id) + if (t) { + setEditing(t) + setFocusEntity(null) + } + }, [focusEntity, tasks, setFocusEntity]) + // Synchro temps réel useEffect(() => { if (!projectId) return @@ -198,7 +208,7 @@ export default function Kanban(): JSX.Element { {colTasks.length} -
+
{colTasks.map((t) => { const mins = times[t.id] ?? 0 return ( @@ -215,7 +225,7 @@ export default function Kanban(): JSX.Element { setOverStatus(null) }} onClick={() => setEditing(t)} - className={`card cursor-pointer p-3 transition-all hover:-translate-y-0.5 hover:border-accent ${ + className={`card cursor-pointer p-3 transition-shadow hover:border-accent hover:shadow-md ${ dragId === t.id ? 'opacity-40' : '' }`} > diff --git a/src/pages/Planning.tsx b/src/pages/Planning.tsx index fc4f72e..e98bc1a 100644 --- a/src/pages/Planning.tsx +++ b/src/pages/Planning.tsx @@ -81,6 +81,16 @@ export default function Planning(): JSX.Element { const totalOn = (day: Date): number => logsOn(day).reduce((s, l) => s + l.minutes, 0) const weekTotal = visibleLogs.reduce((s, l) => s + l.minutes, 0) + // Total de la semaine par membre (sur toute l'équipe, non filtré). + const perMember = useMemo(() => { + const totals: Record = {} + logs.forEach((l) => { + const k = l.user_id ?? 'none' + totals[k] = (totals[k] ?? 0) + l.minutes + }) + return Object.entries(totals).sort((a, b) => b[1] - a[1]) + }, [logs]) + const title = `Semaine du ${format(weekStart, 'd MMM', { locale: fr })} au ${format( weekEnd, 'd MMM yyyy', @@ -213,6 +223,26 @@ export default function Planning(): JSX.Element { })}
+ {/* Récap de la semaine par membre */} + {perMember.length > 0 && ( +
+ Cette semaine + {perMember.map(([uid, mins]) => ( + + {uid === 'none' ? ( + Sans membre + ) : ( + <> + + {byId(uid)?.full_name ?? 'Inconnu'} + + )} + {formatMinutes(mins)} + + ))} +
+ )} + {modal && (