time-tracking-eltex/frontend/src/components/AddTimeEntryModal.jsx

472 lines
17 KiB
JavaScript
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, useEffect } from "react";
const AddTimeEntryModal = ({
show,
onHide,
onAdd,
isEdit = false,
existingEntry = null,
onUpdate,
defaultAutoMode = false,
}) => {
const [entry, setEntry] = useState({
date: "",
reason: "",
hours: "",
startDate: "",
startTime: "",
endDate: "",
endTime: "",
isAuto: false,
});
const [previewHours, setPreviewHours] = useState(0);
useEffect(() => {
if (isEdit && existingEntry) {
console.log("=== EDITING ENTRY ===");
console.log("Existing entry data:", existingEntry);
console.log("Status:", existingEntry.status);
console.log("startDate:", existingEntry.startDate);
console.log("startTime:", existingEntry.startTime);
const isAutoEntry = !!(
existingEntry.startDate ||
existingEntry.startTime ||
existingEntry.status === "active"
);
console.log("Calculated isAutoEntry:", isAutoEntry);
setEntry({
date: existingEntry.date
? new Date(existingEntry.date).toISOString().split("T")[0]
: "",
reason: existingEntry.reason || "",
hours: existingEntry.hours || "",
startDate: existingEntry.startDate
? new Date(existingEntry.startDate).toISOString().split("T")[0]
: "",
startTime: existingEntry.startTime || "",
endDate: existingEntry.endDate
? new Date(existingEntry.endDate).toISOString().split("T")[0]
: "",
endTime: existingEntry.endTime || "",
isAuto: isAutoEntry,
});
setPreviewHours(existingEntry.hours || 0);
console.log("Final entry state set:", {
isAuto: isAutoEntry,
startDate: existingEntry.startDate,
startTime: existingEntry.startTime,
status: existingEntry.status,
});
} else if (!isEdit) {
setEntry({
date: "",
reason: "",
hours: "",
startDate: "",
startTime: "",
endDate: "",
endTime: "",
isAuto: defaultAutoMode,
});
setPreviewHours(0);
}
}, [isEdit, existingEntry, show, defaultAutoMode]);
const handleSubmit = (e) => {
e.preventDefault();
const submitData = { ...entry };
// Очищаем пустые поля
Object.keys(submitData).forEach((key) => {
if (submitData[key] === "" && key !== "reason") {
submitData[key] = null;
}
});
// Для ручного режима используем date и hours
if (!entry.isAuto) {
submitData.startDate = null;
submitData.startTime = null;
submitData.endDate = null;
submitData.endTime = null;
}
console.log("Final submit data:", submitData);
if (isEdit && onUpdate) {
onUpdate(existingEntry.id, submitData);
} else {
onAdd(submitData);
}
if (!isEdit) {
setEntry({
date: "",
reason: "",
hours: "",
startDate: "",
startTime: "",
endDate: "",
endTime: "",
isAuto: defaultAutoMode,
});
setPreviewHours(0);
}
};
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
const newValue = type === "checkbox" ? checked : value;
setEntry({ ...entry, [name]: newValue });
};
// Функция предварительного расчета часов (соответствует бэкендовому алгоритму)
const calculatePreviewHours = (startDate, startTime, endDate, endTime) => {
if (!startDate || !startTime) return 0;
try {
const startDateTime = new Date(`${startDate}T${startTime || "09:00"}`);
const endDateTime =
endDate && endTime ? new Date(`${endDate}T${endTime}`) : null;
// Если нет даты окончания - возвращаем 0 (открытая запись)
if (!endDateTime) {
return 0;
}
// Если дата окончания раньше даты начала - ошибка
if (endDateTime < startDateTime) {
throw new Error("Дата окончания не может быть раньше даты начала");
}
const WORK_START = 9 * 60; // 09:00 в минутах
const WORK_END = 18 * 60; // 18:00 в минутах
const LUNCH_START = 12 * 60; // 12:00 в минутах
const LUNCH_END = 13 * 60; // 13:00 в минутах
let totalMinutes = 0;
let currentDate = new Date(startDate);
// Проходим по каждому дню от начала до окончания
while (
currentDate <= (endDateTime ? new Date(endDate) : new Date(startDate))
) {
const day = currentDate.getDay(); // 0 = воскресенье, 1 = понедельник, ..., 6 = суббота
if (day >= 1 && day <= 5) {
// понедельник-пятница
const dayStart = new Date(currentDate);
dayStart.setHours(0, 0, 0, 0);
const dayEnd = new Date(currentDate);
dayEnd.setHours(23, 59, 59, 999);
// Определяем время начала работы в этот день
let workStartMinutes = WORK_START;
if (
currentDate.toDateString() === new Date(startDate).toDateString()
) {
// Первый день - берем время начала работы
const startMinutes =
startDateTime.getHours() * 60 + startDateTime.getMinutes();
workStartMinutes = Math.max(startMinutes, WORK_START);
}
// Определяем время окончания работы в этот день
let workEndMinutes = WORK_END;
if (
endDateTime &&
currentDate.toDateString() === new Date(endDate).toDateString()
) {
// Последний день - берем время окончания работы
const endMinutes =
endDateTime.getHours() * 60 + endDateTime.getMinutes();
workEndMinutes = Math.min(endMinutes, WORK_END);
}
// Если время окончания раньше времени начала в этот день - пропускаем
if (workEndMinutes > workStartMinutes) {
let dayMinutes = workEndMinutes - workStartMinutes;
// Вычитаем обед, если он попадает в рабочий интервал
if (workStartMinutes < LUNCH_END && workEndMinutes > LUNCH_START) {
const lunchOverlapStart = Math.max(workStartMinutes, LUNCH_START);
const lunchOverlapEnd = Math.min(workEndMinutes, LUNCH_END);
if (lunchOverlapEnd > lunchOverlapStart) {
dayMinutes -= lunchOverlapEnd - lunchOverlapStart;
}
}
totalMinutes += Math.max(0, dayMinutes);
}
}
// Переходим к следующему дню
currentDate.setDate(currentDate.getDate() + 1);
}
return Math.round((totalMinutes / 60) * 10) / 10; // Округляем до 0.1 часа
} catch (error) {
console.error("Calculation error:", error);
return 0;
}
};
// Обновляем предварительный расчет при изменении полей
useEffect(() => {
if (entry.isAuto) {
const hours = calculatePreviewHours(
entry.startDate,
entry.startTime,
entry.endDate,
entry.endTime
);
setPreviewHours(hours);
}
}, [
entry.startDate,
entry.startTime,
entry.endDate,
entry.endTime,
entry.isAuto,
]);
return (
<div
className={`modal ${show ? "show d-block" : ""}`}
tabIndex="-1"
role="dialog"
style={{
display: show ? "block" : "none",
zIndex: show ? "1055" : "auto",
}}
>
<div className="modal-dialog modal-dialog-centered" role="document">
<div
className="modal-content"
style={{ zIndex: show ? "1056" : "auto" }}
>
<div className="modal-header">
<h5 className="modal-title">
{isEdit
? "Редактировать запись"
: "Добавить запись о неучтенном времени"}
</h5>
<button
type="button"
className="btn-close"
onClick={onHide}
aria-label="Close"
></button>
</div>
<form onSubmit={handleSubmit}>
<div className="modal-body">
{/* Переключатель режима */}
<div className="mb-3">
<div className="form-check form-switch">
<input
className="form-check-input"
type="checkbox"
id="isAuto"
name="isAuto"
checked={entry.isAuto}
onChange={handleChange}
/>
<label className="form-check-label" htmlFor="isAuto">
Автоматический расчет времени
</label>
</div>
</div>
{/* Ручной режим */}
{!entry.isAuto && (
<>
<div className="mb-3">
<label htmlFor="date" className="form-label">
Дата
</label>
<input
type="date"
className="form-control"
id="date"
name="date"
value={entry.date}
onChange={handleChange}
required={!entry.isAuto}
/>
</div>
<div className="mb-3">
<label htmlFor="hours" className="form-label">
Количество часов
</label>
<input
type="number"
step="0.1"
className="form-control"
id="hours"
name="hours"
value={entry.hours}
onChange={handleChange}
required={!entry.isAuto}
/>
</div>
</>
)}
{/* Автоматический режим */}
{entry.isAuto && (
<>
<div className="row">
<div className="col-md-6 mb-3">
<label htmlFor="startDate" className="form-label">
Дата начала
</label>
<input
type="date"
className={`form-control ${
!entry.startDate ? "is-invalid" : ""
}`}
id="startDate"
name="startDate"
value={entry.startDate}
onChange={handleChange}
required={entry.isAuto}
/>
{!entry.startDate && (
<div className="invalid-feedback">
Обязательное поле для автоматического режима
</div>
)}
</div>
<div className="col-md-6 mb-3">
<label htmlFor="startTime" className="form-label">
Время начала
</label>
<input
type="time"
className={`form-control ${
!entry.startTime ? "is-invalid" : ""
}`}
id="startTime"
name="startTime"
value={entry.startTime}
onChange={handleChange}
required={entry.isAuto}
/>
{!entry.startTime && (
<div className="invalid-feedback">
Обязательное поле для автоматического режима
</div>
)}
</div>
</div>
<div className="row">
<div className="col-md-6 mb-3">
<label htmlFor="endDate" className="form-label">
Дата окончания{" "}
<small className="text-muted">(опционально)</small>
</label>
<input
type="date"
className={`form-control ${
entry.endDate &&
entry.startDate &&
new Date(entry.endDate) < new Date(entry.startDate)
? "is-invalid"
: ""
}`}
id="endDate"
name="endDate"
value={entry.endDate}
onChange={handleChange}
/>
{entry.endDate &&
entry.startDate &&
new Date(entry.endDate) < new Date(entry.startDate) && (
<div className="invalid-feedback">
Дата окончания не может быть раньше даты начала
</div>
)}
</div>
<div className="col-md-6 mb-3">
<label htmlFor="endTime" className="form-label">
Время окончания{" "}
<small className="text-muted">(опционально)</small>
</label>
<input
type="time"
className="form-control"
id="endTime"
name="endTime"
value={entry.endTime}
onChange={handleChange}
/>
</div>
</div>
{previewHours > 0 && (
<div className="alert alert-info">
<strong>
Предварительный расчет: {previewHours} часов
</strong>
<br />
<small>
Рабочий день: 09:00-18:00, обед: 12:00-13:00, только
будни
</small>
</div>
)}
{(!entry.endDate || !entry.endTime) &&
entry.startDate &&
entry.startTime && (
<div className="alert alert-warning">
<strong>Открытая запись:</strong> Время окончания не
указано. Запись будет активной до тех пор, пока вы не
добавите время окончания.
</div>
)}
</>
)}
<div className="mb-3">
<label htmlFor="reason" className="form-label">
Причина
</label>
<textarea
className="form-control"
id="reason"
name="reason"
value={entry.reason}
onChange={handleChange}
required
/>
</div>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-secondary"
onClick={onHide}
>
Отмена
</button>
<button type="submit" className="btn btn-primary">
{isEdit ? "Обновить" : "Добавить"}
</button>
</div>
</form>
</div>
</div>
{show && (
<div
className="modal-backdrop fade show"
style={{ zIndex: "1054" }}
onClick={onHide}
></div>
)}
</div>
);
};
export default AddTimeEntryModal;