From d59d41f215b9a83ff88efb2bac5c0fa77c732e38 Mon Sep 17 00:00:00 2001 From: Vadim Sobinin Date: Mon, 2 Feb 2026 17:13:27 +0300 Subject: [PATCH] feat: add inline time edit and log editing modal - Add inline time editing in log list (click on time to edit) - Add edit button with pencil icon for each log entry - Modify AddLogModal to support both create and edit modes - Wire up updateLog from useLogs hook in Dashboard Co-Authored-By: Claude --- .gitignore | 38 +++++++++ frontend/src/components/AddLogModal.tsx | 36 ++++++-- frontend/src/components/Dashboard.tsx | 40 +++++++-- frontend/src/components/LogList.tsx | 104 ++++++++++++++++++++++-- 4 files changed, 202 insertions(+), 16 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac9a55d --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Dependencies +node_modules/ + +# Build output +dist/ +build/ + +# Environment files +.env +.env.local +.env.*.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Database +*.db +*.sqlite + +# Documentation +docs/ + +# Cache +.cache/ +.parcel-cache/ diff --git a/frontend/src/components/AddLogModal.tsx b/frontend/src/components/AddLogModal.tsx index daf7134..d3d6a84 100644 --- a/frontend/src/components/AddLogModal.tsx +++ b/frontend/src/components/AddLogModal.tsx @@ -1,19 +1,43 @@ -import { useState } from 'react'; -import { format } from 'date-fns'; +import { useState, useEffect } from 'react'; +import { format, parseISO } from 'date-fns'; +import type { TimeLog } from '../hooks/useLogs'; interface Props { isOpen: boolean; onClose: () => void; onSave: (data: { date: string; time: string; description: string }) => Promise; + editingLog?: TimeLog | null; } -export function AddLogModal({ isOpen, onClose, onSave }: Props) { +function formatTimeForInput(hours: number, mins: number): string { + if (mins === 0) { + return String(hours); + } + return `${hours}:${String(mins).padStart(2, '0')}`; +} + +export function AddLogModal({ isOpen, onClose, onSave, editingLog }: Props) { const [date, setDate] = useState(format(new Date(), 'yyyy-MM-dd')); const [time, setTime] = useState('8'); const [description, setDescription] = useState(''); const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); + useEffect(() => { + if (isOpen) { + if (editingLog) { + setDate(format(parseISO(editingLog.date), 'yyyy-MM-dd')); + setTime(formatTimeForInput(editingLog.hours, editingLog.mins)); + setDescription(editingLog.description); + } else { + setDate(format(new Date(), 'yyyy-MM-dd')); + setTime('8'); + setDescription(''); + } + setError(''); + } + }, [isOpen, editingLog]); + if (!isOpen) return null; const handleSubmit = async (e: React.FormEvent) => { @@ -23,8 +47,6 @@ export function AddLogModal({ isOpen, onClose, onSave }: Props) { try { await onSave({ date, time, description }); - setTime('8'); - setDescription(''); onClose(); } catch (err) { setError(err instanceof Error ? err.message : 'Ошибка'); @@ -40,7 +62,9 @@ export function AddLogModal({ isOpen, onClose, onSave }: Props) { return (
-

Новая запись

+

+ {editingLog ? 'Редактирование записи' : 'Новая запись'} +

diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index 185629f..2f46c3f 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { format, startOfMonth, endOfMonth, addMonths, subMonths } from 'date-fns'; import { ru } from 'date-fns/locale'; -import { useLogs } from '../hooks/useLogs'; +import { useLogs, type TimeLog } from '../hooks/useLogs'; import { LogList } from './LogList'; import { AddLogModal } from './AddLogModal'; import { ReportModal } from './ReportModal'; @@ -15,11 +15,12 @@ export function Dashboard({ username, onLogout }: Props) { const [currentDate, setCurrentDate] = useState(new Date()); const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isReportModalOpen, setIsReportModalOpen] = useState(false); + const [editingLog, setEditingLog] = useState(null); const startDate = format(startOfMonth(currentDate), 'yyyy-MM-dd'); const endDate = format(endOfMonth(currentDate), 'yyyy-MM-dd'); - const { logs, isLoading, createLog, deleteLog } = useLogs(startDate, endDate); + const { logs, isLoading, createLog, updateLog, deleteLog } = useLogs(startDate, endDate); const totalMinutes = logs.reduce((sum, log) => sum + log.minutes, 0); const totalHours = Math.floor(totalMinutes / 60); @@ -34,6 +35,28 @@ export function Dashboard({ username, onLogout }: Props) { } }; + const handleEdit = (log: TimeLog) => { + setEditingLog(log); + setIsAddModalOpen(true); + }; + + const handleUpdateTime = async (id: string, time: string) => { + await updateLog({ id, data: { time } }); + }; + + const handleSave = async (data: { date: string; time: string; description: string }) => { + if (editingLog) { + await updateLog({ id: editingLog.id, data }); + } else { + await createLog(data); + } + }; + + const handleCloseModal = () => { + setIsAddModalOpen(false); + setEditingLog(null); + }; + return (
{/* Header */} @@ -93,7 +116,13 @@ export function Dashboard({ username, onLogout }: Props) {
{/* Logs */} - + {/* Floating buttons */} @@ -121,8 +150,9 @@ export function Dashboard({ username, onLogout }: Props) { {/* Modals */} setIsAddModalOpen(false)} - onSave={createLog} + onClose={handleCloseModal} + onSave={handleSave} + editingLog={editingLog} /> void; + onEdit: (log: TimeLog) => void; + onUpdateTime: (id: string, time: string) => Promise; isLoading: boolean; } @@ -15,7 +18,91 @@ function formatTime(hours: number, mins: number): string { return `${hours}:${String(mins).padStart(2, '0')}`; } -export function LogList({ logs, onDelete, isLoading }: Props) { +function formatTimeForInput(hours: number, mins: number): string { + if (mins === 0) { + return String(hours); + } + return `${hours}:${String(mins).padStart(2, '0')}`; +} + +interface InlineTimeEditProps { + log: TimeLog; + onSave: (id: string, time: string) => Promise; +} + +function InlineTimeEdit({ log, onSave }: InlineTimeEditProps) { + const [isEditing, setIsEditing] = useState(false); + const [value, setValue] = useState(formatTimeForInput(log.hours, log.mins)); + const [isLoading, setIsLoading] = useState(false); + const inputRef = useRef(null); + + useEffect(() => { + if (isEditing && inputRef.current) { + inputRef.current.focus(); + inputRef.current.select(); + } + }, [isEditing]); + + useEffect(() => { + setValue(formatTimeForInput(log.hours, log.mins)); + }, [log.hours, log.mins]); + + const handleSave = async () => { + const originalValue = formatTimeForInput(log.hours, log.mins); + if (value.trim() === '' || value === originalValue) { + setValue(originalValue); + setIsEditing(false); + return; + } + + setIsLoading(true); + try { + await onSave(log.id, value); + setIsEditing(false); + } catch { + setValue(originalValue); + } finally { + setIsLoading(false); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleSave(); + } else if (e.key === 'Escape') { + setValue(formatTimeForInput(log.hours, log.mins)); + setIsEditing(false); + } + }; + + if (isEditing) { + return ( + setValue(e.target.value)} + onBlur={handleSave} + onKeyDown={handleKeyDown} + disabled={isLoading} + className="w-16 px-1 py-0.5 text-blue-600 font-medium text-center border border-blue-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500" + /> + ); + } + + return ( + + ); +} + +export function LogList({ logs, onDelete, onEdit, onUpdateTime, isLoading }: Props) { if (isLoading) { return (
@@ -68,10 +155,17 @@ export function LogList({ logs, onDelete, isLoading }: Props) {

{log.description}

-
- - {formatTime(log.hours, log.mins)} - +
+ +