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');
|
const breakCell = await fillBreakCell(page, '0.5');
|
||||||
await breakCell.press('Enter');
|
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')).toBeVisible();
|
||||||
|
await expect(page.getByTestId('break_placement_summary')).toContainText(
|
||||||
|
'No Project · Split me'
|
||||||
|
);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
waitForBreakCreated(page),
|
waitForBreakCreated(page),
|
||||||
page.getByRole('button', { name: 'Add break' }).click(),
|
page.getByRole('button', { name: 'Add break' }).click(),
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ import {
|
|||||||
planMoveInsert,
|
planMoveInsert,
|
||||||
planSplitEntry,
|
planSplitEntry,
|
||||||
type BreakPlacementRequest,
|
type BreakPlacementRequest,
|
||||||
|
type Interval,
|
||||||
} from '@/utils/timesheet/breakPlacementMath';
|
} from '@/utils/timesheet/breakPlacementMath';
|
||||||
import { BREAK_GAP_TOLERANCE_MINUTES } from '@/packages/ui/src/utils/breakPlacement';
|
import { BREAK_GAP_TOLERANCE_MINUTES } from '@/packages/ui/src/utils/breakPlacement';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
request: BreakPlacementRequest | null;
|
request: BreakPlacementRequest | null;
|
||||||
apply: (breakStart: string, durationSeconds: number) => Promise<void>;
|
apply: (breakStart: string, durationSeconds: number) => Promise<void>;
|
||||||
|
entryLabel: (id: string) => string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{ cancel: [] }>();
|
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.";
|
: "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') {
|
if (mode.value === 'split') {
|
||||||
const plan = splitPlan.value;
|
const plan = splitPlan.value;
|
||||||
if (!plan) return [];
|
if (!plan) return [];
|
||||||
|
const workLabel = props.entryLabel(req.workEntries[0]!.id);
|
||||||
return [
|
return [
|
||||||
`${fmt(plan.firstHalf.start)}–${fmt(plan.firstHalf.end)} (work)`,
|
{ times: range(plan.firstHalf), label: workLabel },
|
||||||
`${fmt(plan.breakSlot.start)}–${fmt(plan.breakSlot.end)} (break)`,
|
{ times: range(plan.breakSlot), label: 'Break' },
|
||||||
`${fmt(plan.secondHalf.start)}–${fmt(plan.secondHalf.end)} (work)`,
|
{ times: range(plan.secondHalf), label: workLabel },
|
||||||
...plan.shifted.map((shift) => {
|
...plan.shifted.map((shift) => ({
|
||||||
const original = props.request!.otherEntries.find((e) => e.id === shift.id)!;
|
times: moved(req.otherEntries.find((e) => e.id === shift.id)!, shift),
|
||||||
return `${fmt(original.start)}–${fmt(original.end)} → ${fmt(shift.start)}–${fmt(shift.end)} (break)`;
|
label: props.entryLabel(shift.id),
|
||||||
}),
|
})),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
const plan = movePlan.value;
|
const plan = movePlan.value;
|
||||||
if (!plan) return [];
|
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) => {
|
return plan.shifted.map((shift) => {
|
||||||
const isBreak = props.request!.otherEntries.some((e) => e.id === shift.id);
|
|
||||||
const original =
|
const original =
|
||||||
props.request!.workEntries.find((e) => e.id === shift.id) ??
|
req.workEntries.find((e) => e.id === shift.id) ??
|
||||||
props.request!.otherEntries.find((e) => e.id === shift.id)!;
|
req.otherEntries.find((e) => e.id === shift.id)!;
|
||||||
const label = `${fmt(original.start)}–${fmt(original.end)} → ${fmt(shift.start)}–${fmt(shift.end)}`;
|
return { times: moved(original, shift), label: props.entryLabel(shift.id) };
|
||||||
return isBreak ? `${label} (break)` : label;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -189,8 +200,14 @@ async function submit() {
|
|||||||
<div class="text-xs uppercase tracking-wide text-text-tertiary">
|
<div class="text-xs uppercase tracking-wide text-text-tertiary">
|
||||||
{{ mode === 'split' ? 'Result' : 'Entries that move' }}
|
{{ mode === 'split' ? 'Result' : 'Entries that move' }}
|
||||||
</div>
|
</div>
|
||||||
<div v-for="(line, index) in changeSummary" :key="index" class="tabular-nums">
|
<div
|
||||||
{{ line }}
|
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>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -133,6 +133,17 @@ const {
|
|||||||
() => organization.value?.prevent_overlapping_time_entries ?? false
|
() => 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
|
// 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.
|
// row, so a flat set is enough — its cells show a warning for dates in the set.
|
||||||
const misplacedBreakDates = computed<Set<string>>(() => {
|
const misplacedBreakDates = computed<Set<string>>(() => {
|
||||||
@@ -257,6 +268,7 @@ async function createTag(name: string): Promise<Tag | undefined> {
|
|||||||
<BreakPlacementModal
|
<BreakPlacementModal
|
||||||
:request="breakPlacementRequest"
|
:request="breakPlacementRequest"
|
||||||
:apply="applyBreakPlacement"
|
:apply="applyBreakPlacement"
|
||||||
|
:entry-label="breakPlanEntryLabel"
|
||||||
@cancel="dismissBreakPlacement" />
|
@cancel="dismissBreakPlacement" />
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user