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 <noreply@anthropic.com>
This commit is contained in:
Vadim Sobinin
2026-02-02 17:13:27 +03:00
parent e0e4f85ee1
commit d59d41f215
4 changed files with 202 additions and 16 deletions

38
.gitignore vendored Normal file
View File

@@ -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/

View File

@@ -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<unknown>;
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 (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-md mx-4">
<h2 className="text-xl font-semibold mb-4">Новая запись</h2>
<h2 className="text-xl font-semibold mb-4">
{editingLog ? 'Редактирование записи' : 'Новая запись'}
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>

View File

@@ -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<TimeLog | null>(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 (
<div className="min-h-screen bg-gray-50">
{/* Header */}
@@ -93,7 +116,13 @@ export function Dashboard({ username, onLogout }: Props) {
</div>
{/* Logs */}
<LogList logs={logs} onDelete={handleDelete} isLoading={isLoading} />
<LogList
logs={logs}
onDelete={handleDelete}
onEdit={handleEdit}
onUpdateTime={handleUpdateTime}
isLoading={isLoading}
/>
</main>
{/* Floating buttons */}
@@ -121,8 +150,9 @@ export function Dashboard({ username, onLogout }: Props) {
{/* Modals */}
<AddLogModal
isOpen={isAddModalOpen}
onClose={() => setIsAddModalOpen(false)}
onSave={createLog}
onClose={handleCloseModal}
onSave={handleSave}
editingLog={editingLog}
/>
<ReportModal
isOpen={isReportModalOpen}

View File

@@ -1,3 +1,4 @@
import { useState, useRef, useEffect } from 'react';
import { format, parseISO } from 'date-fns';
import { ru } from 'date-fns/locale';
import type { TimeLog } from '../hooks/useLogs';
@@ -5,6 +6,8 @@ import type { TimeLog } from '../hooks/useLogs';
interface Props {
logs: TimeLog[];
onDelete: (id: string) => void;
onEdit: (log: TimeLog) => void;
onUpdateTime: (id: string, time: string) => Promise<void>;
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<void>;
}
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<HTMLInputElement>(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 (
<input
ref={inputRef}
type="text"
value={value}
onChange={(e) => 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 (
<button
onClick={() => setIsEditing(true)}
className="text-blue-600 font-medium whitespace-nowrap hover:bg-blue-50 px-2 py-0.5 rounded transition-colors"
title="Изменить время"
>
{formatTime(log.hours, log.mins)}
</button>
);
}
export function LogList({ logs, onDelete, onEdit, onUpdateTime, isLoading }: Props) {
if (isLoading) {
return (
<div className="flex justify-center py-8">
@@ -68,10 +155,17 @@ export function LogList({ logs, onDelete, isLoading }: Props) {
<div className="flex-1 min-w-0">
<p className="text-gray-800 break-words">{log.description}</p>
</div>
<div className="flex items-center gap-3 flex-shrink-0">
<span className="text-blue-600 font-medium whitespace-nowrap">
{formatTime(log.hours, log.mins)}
</span>
<div className="flex items-center gap-2 flex-shrink-0">
<InlineTimeEdit log={log} onSave={onUpdateTime} />
<button
onClick={() => onEdit(log)}
className="text-gray-400 hover:text-blue-500 transition-colors"
title="Редактировать"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
</button>
<button
onClick={() => onDelete(log.id)}
className="text-gray-400 hover:text-red-500 transition-colors"