mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-14 19:22:14 +01:00
insert breaks into work entries instead of carving them out; rollback on
failure system
This commit is contained in:
@@ -18,7 +18,7 @@ import {
|
||||
workDayStartOn,
|
||||
type FreeWindow,
|
||||
} from './cellMath';
|
||||
import { useBreakPlacement, BreakPlacementDeferred } from './useBreakPlacement';
|
||||
import { useBreakPlacement, type PlaceBreakResult } from './useBreakPlacement';
|
||||
|
||||
export type CellSaveStatus = 'saving' | 'saved' | 'error';
|
||||
|
||||
@@ -54,7 +54,8 @@ export function useTimesheetCellMutations(
|
||||
weekDays: Ref<string[]>,
|
||||
timeEntries: Ref<TimeEntry[]>,
|
||||
rows: Ref<TimesheetRow[]>,
|
||||
removeSlot: (key: TimesheetRowKey) => void
|
||||
removeSlot: (key: TimesheetRowKey) => void,
|
||||
preventOverlappingTimeEntries: () => boolean = () => false
|
||||
) {
|
||||
const dayjs = getDayJsInstance();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -70,7 +71,15 @@ export function useTimesheetCellMutations(
|
||||
// modal flow) is its own subsystem — it borrows the generic entry primitives
|
||||
// below (hoisted function declarations, so referenceable here).
|
||||
const { breakPlacementRequest, placeBreak, dismissBreakPlacement, applyBreakPlacement } =
|
||||
useBreakPlacement({ weekDays, timeEntries, requireOrgId, createCell, updateEntry });
|
||||
useBreakPlacement({
|
||||
weekDays,
|
||||
timeEntries,
|
||||
requireOrgId,
|
||||
createCell,
|
||||
updateEntry,
|
||||
deleteEntry,
|
||||
preventOverlappingTimeEntries,
|
||||
});
|
||||
|
||||
function clearStatusTimer(key: string): void {
|
||||
clearTimeout(statusClearTimers[key]);
|
||||
@@ -125,7 +134,14 @@ export function useTimesheetCellMutations(
|
||||
const wasEmpty = row.totalSeconds === 0;
|
||||
|
||||
try {
|
||||
await dispatchCellUpdate(row, dayIndex, newTotalSeconds);
|
||||
const result = await dispatchCellUpdate(row, dayIndex, newTotalSeconds);
|
||||
if (result === 'needs-input') {
|
||||
// The placement modal owns the actual save, so return the cell to idle.
|
||||
clearStatusTimer(statusKey);
|
||||
delete cellStatus.value[statusKey];
|
||||
delete cellPendingSeconds.value[statusKey];
|
||||
return;
|
||||
}
|
||||
|
||||
if (wasEmpty && newTotalSeconds > 0 && hasDuplicateIdentitySlot(row)) {
|
||||
removeSlot(row.key);
|
||||
@@ -137,14 +153,6 @@ export function useTimesheetCellMutations(
|
||||
}
|
||||
markSaved(statusKey);
|
||||
} catch (err) {
|
||||
if (err instanceof BreakPlacementDeferred) {
|
||||
// The break needs manual placement — revert the cell to idle (the
|
||||
// modal drives the actual save) instead of showing an error.
|
||||
clearStatusTimer(statusKey);
|
||||
delete cellStatus.value[statusKey];
|
||||
delete cellPendingSeconds.value[statusKey];
|
||||
return;
|
||||
}
|
||||
markError(statusKey);
|
||||
if (err instanceof NoFreeWindowError) {
|
||||
const friendlyDuration = formatHumanReadableDuration(
|
||||
@@ -182,25 +190,24 @@ export function useTimesheetCellMutations(
|
||||
row: TimesheetRow,
|
||||
dayIndex: number,
|
||||
newTotalSeconds: number
|
||||
): Promise<void> {
|
||||
): Promise<PlaceBreakResult> {
|
||||
const cell = row.cells.get(dayIndex);
|
||||
const existingSeconds = cell?.totalSeconds ?? 0;
|
||||
const diff = newTotalSeconds - existingSeconds;
|
||||
|
||||
if (newTotalSeconds === 0 && cell) {
|
||||
await deleteCell(cell);
|
||||
return;
|
||||
return 'committed';
|
||||
}
|
||||
|
||||
if (!cell || existingSeconds === 0) {
|
||||
// Breaks are placed relative to work (within tolerance), not just in the
|
||||
// first free slot, and may need the placement modal to resolve.
|
||||
if (row.type === 'break' && newTotalSeconds > 0) {
|
||||
await placeBreak(row, dayIndex, newTotalSeconds);
|
||||
return;
|
||||
return placeBreak(row, dayIndex, newTotalSeconds);
|
||||
}
|
||||
await createCell(row, dayIndex, newTotalSeconds);
|
||||
return;
|
||||
return 'committed';
|
||||
}
|
||||
|
||||
// Re-place breaks rather than extend/shrink them, which would fragment a break into
|
||||
@@ -209,24 +216,23 @@ export function useTimesheetCellMutations(
|
||||
// become obstacles); shrinking just trims the tail, which can't fragment.
|
||||
if (row.type === 'break') {
|
||||
if (cell.entries.length === 1) {
|
||||
await placeBreak(row, dayIndex, newTotalSeconds, cell.entries[0]!.id);
|
||||
return;
|
||||
return placeBreak(row, dayIndex, newTotalSeconds, cell.entries[0]!.id);
|
||||
}
|
||||
const tail = pickLatestEndedEntry(cell);
|
||||
if (diff > 0 && tail?.end) {
|
||||
await placeBreak(row, dayIndex, (tail.duration ?? 0) + diff, tail.id);
|
||||
return;
|
||||
return placeBreak(row, dayIndex, (tail.duration ?? 0) + diff, tail.id);
|
||||
}
|
||||
await shrinkFromEnd(cell, -diff);
|
||||
return;
|
||||
return 'committed';
|
||||
}
|
||||
|
||||
if (diff > 0) {
|
||||
await extendCell(row, dayIndex, cell, diff);
|
||||
return;
|
||||
return 'committed';
|
||||
}
|
||||
|
||||
await shrinkFromEnd(cell, -diff);
|
||||
return 'committed';
|
||||
}
|
||||
|
||||
async function deleteCell(cell: TimesheetCell): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user