472 lines
17 KiB
JavaScript
472 lines
17 KiB
JavaScript
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;
|