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:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user