Files
timetracker/frontend/src/components/Dashboard.tsx
Vadim Sobinin d59d41f215 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>
2026-02-02 17:13:27 +03:00

164 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import { format, startOfMonth, endOfMonth, addMonths, subMonths } from 'date-fns';
import { ru } from 'date-fns/locale';
import { useLogs, type TimeLog } from '../hooks/useLogs';
import { LogList } from './LogList';
import { AddLogModal } from './AddLogModal';
import { ReportModal } from './ReportModal';
interface Props {
username: string;
onLogout: () => void;
}
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, updateLog, deleteLog } = useLogs(startDate, endDate);
const totalMinutes = logs.reduce((sum, log) => sum + log.minutes, 0);
const totalHours = Math.floor(totalMinutes / 60);
const totalMins = totalMinutes % 60;
const handlePrevMonth = () => setCurrentDate(subMonths(currentDate, 1));
const handleNextMonth = () => setCurrentDate(addMonths(currentDate, 1));
const handleDelete = async (id: string) => {
if (window.confirm('Удалить эту запись?')) {
await deleteLog(id);
}
};
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 */}
<header className="bg-white shadow-sm">
<div className="max-w-4xl mx-auto px-4 py-4 flex items-center justify-between">
<h1 className="text-xl font-bold text-gray-800">TimeTracker</h1>
<div className="flex items-center gap-4">
<span className="text-gray-600 text-sm">{username}</span>
<button
onClick={onLogout}
className="text-gray-500 hover:text-gray-700 text-sm"
>
Выйти
</button>
</div>
</div>
</header>
<main className="max-w-4xl mx-auto px-4 py-6">
{/* Month navigation */}
<div className="flex items-center justify-between mb-6">
<button
onClick={handlePrevMonth}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-600" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</button>
<h2 className="text-lg font-semibold text-gray-700 capitalize">
{format(currentDate, 'LLLL yyyy', { locale: ru })}
</h2>
<button
onClick={handleNextMonth}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-600" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
</svg>
</button>
</div>
{/* Stats */}
<div className="bg-white rounded-lg shadow-sm border p-4 mb-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500">Всего за месяц</p>
<p className="text-2xl font-bold text-blue-600">
{totalHours}:{String(totalMins).padStart(2, '0')}
</p>
</div>
<div className="text-right">
<p className="text-sm text-gray-500">Записей</p>
<p className="text-2xl font-bold text-gray-700">{logs.length}</p>
</div>
</div>
</div>
{/* Logs */}
<LogList
logs={logs}
onDelete={handleDelete}
onEdit={handleEdit}
onUpdateTime={handleUpdateTime}
isLoading={isLoading}
/>
</main>
{/* Floating buttons */}
<div className="fixed bottom-6 right-6 flex flex-col gap-3">
<button
onClick={() => setIsReportModalOpen(true)}
className="w-14 h-14 bg-green-600 text-white rounded-full shadow-lg hover:bg-green-700 transition-colors flex items-center justify-center"
title="Создать отчёт"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</button>
<button
onClick={() => setIsAddModalOpen(true)}
className="w-14 h-14 bg-blue-600 text-white rounded-full shadow-lg hover:bg-blue-700 transition-colors flex items-center justify-center"
title="Добавить запись"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
</button>
</div>
{/* Modals */}
<AddLogModal
isOpen={isAddModalOpen}
onClose={handleCloseModal}
onSave={handleSave}
editingLog={editingLog}
/>
<ReportModal
isOpen={isReportModalOpen}
onClose={() => setIsReportModalOpen(false)}
/>
</div>
);
}