mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 00:02:15 +01:00
add description/project labels to break placement modal for existing
time entries
This commit is contained in:
@@ -697,8 +697,12 @@ test('test that adding a timesheet break to a full day splits the work entry via
|
||||
const breakCell = await fillBreakCell(page, '0.5');
|
||||
await breakCell.press('Enter');
|
||||
|
||||
// The placement modal opens with the split preview
|
||||
// The placement modal opens with the split preview, naming the entry that
|
||||
// will be split so the user can recognize it.
|
||||
await expect(page.getByTestId('break_placement_summary')).toBeVisible();
|
||||
await expect(page.getByTestId('break_placement_summary')).toContainText(
|
||||
'No Project · Split me'
|
||||
);
|
||||
await Promise.all([
|
||||
waitForBreakCreated(page),
|
||||
page.getByRole('button', { name: 'Add break' }).click(),
|
||||
|
||||
@@ -13,12 +13,14 @@ import {
|
||||
planMoveInsert,
|
||||
planSplitEntry,
|
||||
type BreakPlacementRequest,
|
||||
type Interval,
|
||||
} from '@/utils/timesheet/breakPlacementMath';
|
||||
import { BREAK_GAP_TOLERANCE_MINUTES } from '@/packages/ui/src/utils/breakPlacement';
|
||||
|
||||
const props = defineProps<{
|
||||
request: BreakPlacementRequest | null;
|
||||
apply: (breakStart: string, durationSeconds: number) => Promise<void>;
|
||||
entryLabel: (id: string) => string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{ cancel: [] }>();
|
||||
@@ -124,30 +126,39 @@ const explanation = computed(() => {
|
||||
: "There's no free gap that fits this break, so the surrounding entries will be shifted to make room.";
|
||||
});
|
||||
|
||||
const changeSummary = computed<string[]>(() => {
|
||||
interface PlanLine {
|
||||
times: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const changeSummary = computed<PlanLine[]>(() => {
|
||||
const req = props.request;
|
||||
if (!req) return [];
|
||||
const range = (interval: Interval) => `${fmt(interval.start)}–${fmt(interval.end)}`;
|
||||
const moved = (from: Interval, to: Interval) => `${range(from)} → ${range(to)}`;
|
||||
|
||||
if (mode.value === 'split') {
|
||||
const plan = splitPlan.value;
|
||||
if (!plan) return [];
|
||||
const workLabel = props.entryLabel(req.workEntries[0]!.id);
|
||||
return [
|
||||
`${fmt(plan.firstHalf.start)}–${fmt(plan.firstHalf.end)} (work)`,
|
||||
`${fmt(plan.breakSlot.start)}–${fmt(plan.breakSlot.end)} (break)`,
|
||||
`${fmt(plan.secondHalf.start)}–${fmt(plan.secondHalf.end)} (work)`,
|
||||
...plan.shifted.map((shift) => {
|
||||
const original = props.request!.otherEntries.find((e) => e.id === shift.id)!;
|
||||
return `${fmt(original.start)}–${fmt(original.end)} → ${fmt(shift.start)}–${fmt(shift.end)} (break)`;
|
||||
}),
|
||||
{ times: range(plan.firstHalf), label: workLabel },
|
||||
{ times: range(plan.breakSlot), label: 'Break' },
|
||||
{ times: range(plan.secondHalf), label: workLabel },
|
||||
...plan.shifted.map((shift) => ({
|
||||
times: moved(req.otherEntries.find((e) => e.id === shift.id)!, shift),
|
||||
label: props.entryLabel(shift.id),
|
||||
})),
|
||||
];
|
||||
}
|
||||
const plan = movePlan.value;
|
||||
if (!plan) return [];
|
||||
if (plan.shifted.length === 0) return ['No entries need to move.'];
|
||||
if (plan.shifted.length === 0) return [{ times: 'No entries need to move.', label: '' }];
|
||||
return plan.shifted.map((shift) => {
|
||||
const isBreak = props.request!.otherEntries.some((e) => e.id === shift.id);
|
||||
const original =
|
||||
props.request!.workEntries.find((e) => e.id === shift.id) ??
|
||||
props.request!.otherEntries.find((e) => e.id === shift.id)!;
|
||||
const label = `${fmt(original.start)}–${fmt(original.end)} → ${fmt(shift.start)}–${fmt(shift.end)}`;
|
||||
return isBreak ? `${label} (break)` : label;
|
||||
req.workEntries.find((e) => e.id === shift.id) ??
|
||||
req.otherEntries.find((e) => e.id === shift.id)!;
|
||||
return { times: moved(original, shift), label: props.entryLabel(shift.id) };
|
||||
});
|
||||
});
|
||||
|
||||
@@ -189,8 +200,14 @@ async function submit() {
|
||||
<div class="text-xs uppercase tracking-wide text-text-tertiary">
|
||||
{{ mode === 'split' ? 'Result' : 'Entries that move' }}
|
||||
</div>
|
||||
<div v-for="(line, index) in changeSummary" :key="index" class="tabular-nums">
|
||||
{{ line }}
|
||||
<div
|
||||
v-for="(line, index) in changeSummary"
|
||||
:key="index"
|
||||
class="flex items-baseline gap-2">
|
||||
<span class="tabular-nums whitespace-nowrap">{{ line.times }}</span>
|
||||
<span v-if="line.label" class="text-text-tertiary truncate">
|
||||
{{ line.label }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -133,6 +133,17 @@ const {
|
||||
() => organization.value?.prevent_overlapping_time_entries ?? false
|
||||
);
|
||||
|
||||
function breakPlanEntryLabel(id: string): string {
|
||||
const entry = allTimeEntries.value.find((e) => e.id === id);
|
||||
if (!entry) return '';
|
||||
if (entry.type === 'break') return 'Break';
|
||||
const project = projects.value.find((p) => p.id === entry.project_id);
|
||||
const task = tasks.value.find((t) => t.id === entry.task_id);
|
||||
return [project?.name ?? 'No Project', task?.name, entry.description]
|
||||
.filter((part): part is string => !!part)
|
||||
.join(' · ');
|
||||
}
|
||||
|
||||
// Local dates (YYYY-MM-DD) that have a misplaced break. There is only one break
|
||||
// row, so a flat set is enough — its cells show a warning for dates in the set.
|
||||
const misplacedBreakDates = computed<Set<string>>(() => {
|
||||
@@ -257,6 +268,7 @@ async function createTag(name: string): Promise<Tag | undefined> {
|
||||
<BreakPlacementModal
|
||||
:request="breakPlacementRequest"
|
||||
:apply="applyBreakPlacement"
|
||||
:entry-label="breakPlanEntryLabel"
|
||||
@cancel="dismissBreakPlacement" />
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user