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:
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal 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/
|
||||||
@@ -1,19 +1,43 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { format } from 'date-fns';
|
import { format, parseISO } from 'date-fns';
|
||||||
|
import type { TimeLog } from '../hooks/useLogs';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSave: (data: { date: string; time: string; description: string }) => Promise<unknown>;
|
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 [date, setDate] = useState(format(new Date(), 'yyyy-MM-dd'));
|
||||||
const [time, setTime] = useState('8');
|
const [time, setTime] = useState('8');
|
||||||
const [description, setDescription] = useState('');
|
const [description, setDescription] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
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;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
@@ -23,8 +47,6 @@ export function AddLogModal({ isOpen, onClose, onSave }: Props) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await onSave({ date, time, description });
|
await onSave({ date, time, description });
|
||||||
setTime('8');
|
|
||||||
setDescription('');
|
|
||||||
onClose();
|
onClose();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Ошибка');
|
setError(err instanceof Error ? err.message : 'Ошибка');
|
||||||
@@ -40,7 +62,9 @@ export function AddLogModal({ isOpen, onClose, onSave }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<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">
|
<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">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { format, startOfMonth, endOfMonth, addMonths, subMonths } from 'date-fns';
|
import { format, startOfMonth, endOfMonth, addMonths, subMonths } from 'date-fns';
|
||||||
import { ru } from 'date-fns/locale';
|
import { ru } from 'date-fns/locale';
|
||||||
import { useLogs } from '../hooks/useLogs';
|
import { useLogs, type TimeLog } from '../hooks/useLogs';
|
||||||
import { LogList } from './LogList';
|
import { LogList } from './LogList';
|
||||||
import { AddLogModal } from './AddLogModal';
|
import { AddLogModal } from './AddLogModal';
|
||||||
import { ReportModal } from './ReportModal';
|
import { ReportModal } from './ReportModal';
|
||||||
@@ -15,11 +15,12 @@ export function Dashboard({ username, onLogout }: Props) {
|
|||||||
const [currentDate, setCurrentDate] = useState(new Date());
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||||
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
|
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
|
||||||
|
const [editingLog, setEditingLog] = useState<TimeLog | null>(null);
|
||||||
|
|
||||||
const startDate = format(startOfMonth(currentDate), 'yyyy-MM-dd');
|
const startDate = format(startOfMonth(currentDate), 'yyyy-MM-dd');
|
||||||
const endDate = format(endOfMonth(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 totalMinutes = logs.reduce((sum, log) => sum + log.minutes, 0);
|
||||||
const totalHours = Math.floor(totalMinutes / 60);
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -93,7 +116,13 @@ export function Dashboard({ username, onLogout }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Logs */}
|
{/* Logs */}
|
||||||
<LogList logs={logs} onDelete={handleDelete} isLoading={isLoading} />
|
<LogList
|
||||||
|
logs={logs}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
onEdit={handleEdit}
|
||||||
|
onUpdateTime={handleUpdateTime}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Floating buttons */}
|
{/* Floating buttons */}
|
||||||
@@ -121,8 +150,9 @@ export function Dashboard({ username, onLogout }: Props) {
|
|||||||
{/* Modals */}
|
{/* Modals */}
|
||||||
<AddLogModal
|
<AddLogModal
|
||||||
isOpen={isAddModalOpen}
|
isOpen={isAddModalOpen}
|
||||||
onClose={() => setIsAddModalOpen(false)}
|
onClose={handleCloseModal}
|
||||||
onSave={createLog}
|
onSave={handleSave}
|
||||||
|
editingLog={editingLog}
|
||||||
/>
|
/>
|
||||||
<ReportModal
|
<ReportModal
|
||||||
isOpen={isReportModalOpen}
|
isOpen={isReportModalOpen}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { format, parseISO } from 'date-fns';
|
import { format, parseISO } from 'date-fns';
|
||||||
import { ru } from 'date-fns/locale';
|
import { ru } from 'date-fns/locale';
|
||||||
import type { TimeLog } from '../hooks/useLogs';
|
import type { TimeLog } from '../hooks/useLogs';
|
||||||
@@ -5,6 +6,8 @@ import type { TimeLog } from '../hooks/useLogs';
|
|||||||
interface Props {
|
interface Props {
|
||||||
logs: TimeLog[];
|
logs: TimeLog[];
|
||||||
onDelete: (id: string) => void;
|
onDelete: (id: string) => void;
|
||||||
|
onEdit: (log: TimeLog) => void;
|
||||||
|
onUpdateTime: (id: string, time: string) => Promise<void>;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,7 +18,91 @@ function formatTime(hours: number, mins: number): string {
|
|||||||
return `${hours}:${String(mins).padStart(2, '0')}`;
|
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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center py-8">
|
<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">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-gray-800 break-words">{log.description}</p>
|
<p className="text-gray-800 break-words">{log.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 flex-shrink-0">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
<span className="text-blue-600 font-medium whitespace-nowrap">
|
<InlineTimeEdit log={log} onSave={onUpdateTime} />
|
||||||
{formatTime(log.hours, log.mins)}
|
<button
|
||||||
</span>
|
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
|
<button
|
||||||
onClick={() => onDelete(log.id)}
|
onClick={() => onDelete(log.id)}
|
||||||
className="text-gray-400 hover:text-red-500 transition-colors"
|
className="text-gray-400 hover:text-red-500 transition-colors"
|
||||||
|
|||||||
Reference in New Issue
Block a user