diff --git a/frontend/src/components/DayCard.jsx b/frontend/src/components/DayCard.jsx
index a2d033c..04ed16d 100644
--- a/frontend/src/components/DayCard.jsx
+++ b/frontend/src/components/DayCard.jsx
@@ -65,7 +65,7 @@ export default function DayCard({ date, entries, homeowners, onEntryChange, onAd
{(entries || []).map((entry) => (
realId (after create resolves)
+ // creating: tempId -> Promise (while create is in flight)
+ const idMap = useRef({});
+ const creating = useRef({});
+
+ function makeNewEntry(date) {
+ const id = `new-${date}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
+ return { id, clientKey: id, date, homeownerId: '', hoursWorked: '', workDescription: '', _isNew: true };
+ }
const weekDates = getWeekDates(selectedDate);
const weekParam = formatWeekParam(selectedDate);
@@ -85,10 +95,12 @@ export default function Timesheet() {
// Ensure at least one empty entry per day
weekDates.forEach((d) => {
if (byDate[d].length === 0) {
- byDate[d] = [{ id: `new-${d}-0`, date: d, homeownerId: '', hoursWorked: '', workDescription: '', _isNew: true }];
+ byDate[d] = [makeNewEntry(d)];
}
});
+ idMap.current = {};
+ creating.current = {};
setEntries(byDate);
setTimesheet(timesheetRes.data?.timesheet || timesheetRes.data);
setHomeowners(homeownersRes.data.homeowners || homeownersRes.data || []);
@@ -124,18 +136,51 @@ export default function Timesheet() {
};
try {
- if (data._isNew || entryId.startsWith('new-')) {
- const res = await api.post('/entries', payload);
- // Replace temp ID with real ID
- setEntries((prev) => {
- const dk = data.date;
- return {
- ...prev,
- [dk]: prev[dk].map((e) => (e.id === entryId ? { ...res.data.entry || res.data, date: dk } : e)),
- };
+ const isTemp = entryId.startsWith('new-');
+ let realId = isTemp ? idMap.current[entryId] : entryId;
+
+ // A create for this temp id is already in flight — wait for it, then update.
+ if (isTemp && !realId && creating.current[entryId]) {
+ realId = await creating.current[entryId];
+ }
+
+ if (isTemp && !realId) {
+ const createPromise = api.post('/entries', payload).then((res) => {
+ const created = res.data.entry || res.data;
+ idMap.current[entryId] = created.id;
+ // Merge the server id into the existing local entry. Keep the
+ // clientKey (so React does not remount the form) and keep whatever
+ // the user has typed since the request was sent.
+ setEntries((prev) => {
+ const dk = data.date;
+ return {
+ ...prev,
+ [dk]: (prev[dk] || []).map((e) =>
+ e.id === entryId
+ ? {
+ ...e,
+ id: created.id,
+ clientKey: e.clientKey,
+ date: dk,
+ homeownerId: e.homeownerId,
+ hoursWorked: e.hoursWorked,
+ workDescription: e.workDescription,
+ _isNew: false,
+ }
+ : e
+ ),
+ };
+ });
+ return created.id;
});
+ creating.current[entryId] = createPromise;
+ try {
+ await createPromise;
+ } finally {
+ delete creating.current[entryId];
+ }
} else {
- await api.put(`/entries/${entryId}`, payload);
+ await api.put(`/entries/${realId}`, payload);
}
setSaveStatus('saved');
} catch (err) {
@@ -164,27 +209,27 @@ export default function Timesheet() {
}
function handleAddEntry(date) {
- const newEntry = {
- id: `new-${date}-${Date.now()}`,
- date,
- homeownerId: '',
- hoursWorked: '',
- workDescription: '',
- _isNew: true,
- };
+ const newEntry = makeNewEntry(date);
setEntries((prev) => ({ ...prev, [date]: [...(prev[date] || []), newEntry] }));
}
async function handleDeleteEntry(entryId) {
try {
- if (!entryId.startsWith('new-')) {
- await api.delete(`/entries/${entryId}`);
+ // Resolve a temp id to its server id if the create already happened
+ // (or is still in flight) so we do not orphan a row on the server.
+ let realId = entryId;
+ if (entryId.startsWith('new-')) {
+ realId = idMap.current[entryId] || (creating.current[entryId] ? await creating.current[entryId] : null);
}
+ if (realId) {
+ await api.delete(`/entries/${realId}`);
+ }
+ delete pendingChanges.current[entryId];
setEntries((prev) => {
const updated = {};
Object.entries(prev).forEach(([dk, dayEntries]) => {
- const filtered = dayEntries.filter((e) => e.id !== entryId);
- updated[dk] = filtered.length > 0 ? filtered : [{ id: `new-${dk}-0`, date: dk, homeownerId: '', hoursWorked: '', workDescription: '', _isNew: true }];
+ const filtered = dayEntries.filter((e) => e.id !== entryId && e.id !== realId);
+ updated[dk] = filtered.length > 0 ? filtered : [makeNewEntry(dk)];
});
return updated;
});