Fixed a few minor bugs
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import Dexie from 'dexie';
|
||||
import { BackupError, buildBackup, EXPORT_FORMAT, EXPORT_VERSION, parseBackup } from './backup.js';
|
||||
import { DAY, localDateKey } from './time.js';
|
||||
import { DAY, HOUR, localDateKey } from './time.js';
|
||||
|
||||
/**
|
||||
* A single working day. `plannedStartTs` / `plannedEndTs` are snapshotted from the
|
||||
@@ -88,6 +88,59 @@ export function patchDay(dateKey, patch) {
|
||||
return db.days.update(dateKey, patch);
|
||||
}
|
||||
|
||||
/** Create once, together with the first entry, even from competing tabs. */
|
||||
export function createDay(day, first) {
|
||||
return db.transaction('rw', db.days, db.entries, async () => {
|
||||
if (await getDay(day.date) || await getActiveDay()) return false;
|
||||
await db.days.add(day);
|
||||
await closeAndStart(day.date, day.startedTs, first);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/** Close the day and its last entry at the same authoritative timestamp. */
|
||||
export function closeDay(date, ts, stale = false) {
|
||||
return db.transaction('rw', db.days, db.entries, async () => {
|
||||
const day = await getDay(date);
|
||||
if (!day || day.status !== 'active') return false;
|
||||
const entries = await getEntries(date);
|
||||
const lastActivity = entries.reduce((max, e) => Math.max(max, e.startTs, e.endTs ?? e.startTs), day.startedTs);
|
||||
const endedTs = Math.max(stale ? day.plannedEndTs : ts, lastActivity);
|
||||
await closeAndStart(date, endedTs, null);
|
||||
await patchDay(date, { status: 'closed', endedTs });
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/** Undo a premature close; no new entry or unaccounted interval is introduced. */
|
||||
export function reopenDay(date, now) {
|
||||
return db.transaction('rw', db.days, db.entries, async () => {
|
||||
const day = await getDay(date);
|
||||
if (!day || day.status !== 'closed' || await getActiveDay()) return false;
|
||||
const entries = await getEntries(date);
|
||||
const last = entries.at(-1);
|
||||
if (!last) return false;
|
||||
await db.entries.update(last.id, { endTs: null });
|
||||
await patchDay(date, {
|
||||
status: 'active', endedTs: null, closePromptedTs: null,
|
||||
plannedEndTs: Math.max(day.plannedEndTs, now + HOUR),
|
||||
});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/** Conditional day updates cannot resurrect or modify a concurrently closed day. */
|
||||
export function patchActiveDay(date, makePatch) {
|
||||
return db.transaction('rw', db.days, async () => {
|
||||
const day = await getDay(date);
|
||||
if (day?.status !== 'active') return false;
|
||||
const patch = makePatch(day);
|
||||
if (!patch) return false;
|
||||
await patchDay(date, patch);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/** The one day still running, if any — including a stale one from a past date. */
|
||||
export function getActiveDay() {
|
||||
return db.days.where('status').equals('active').first();
|
||||
@@ -116,12 +169,16 @@ export function getEntries(dateKey) {
|
||||
* @param {number} ts
|
||||
* @param {Pick<EntryRecord, 'kind'|'task'|'client'>|null} next
|
||||
* Pass null to only close the running entry (end of day).
|
||||
* @param {number|null} [expectedId] running entry observed by the action's caller
|
||||
* @returns {Promise<number|null>} id of the new entry
|
||||
*/
|
||||
export function closeAndStart(dateKey, ts, next) {
|
||||
return db.transaction('rw', db.entries, async () => {
|
||||
export function closeAndStart(dateKey, ts, next, expectedId) {
|
||||
return db.transaction('rw', db.days, db.entries, async () => {
|
||||
if ((await getDay(dateKey))?.status !== 'active') return null;
|
||||
const list = await db.entries.where('date').equals(dateKey).sortBy('seq');
|
||||
const running = list.find((e) => e.endTs == null);
|
||||
// Two taps/tabs acting on the same entry may advance it only once.
|
||||
if (expectedId !== undefined && (running?.id ?? null) !== expectedId) return null;
|
||||
|
||||
let boundary = ts;
|
||||
if (running) {
|
||||
@@ -211,33 +268,30 @@ export function deleteEntry(id) {
|
||||
|
||||
/** Everything the prediction model is built from. */
|
||||
export async function getHistory() {
|
||||
const [entries, days] = await Promise.all([
|
||||
db.entries.toArray(),
|
||||
db.days.toArray(),
|
||||
]);
|
||||
return { entries, days };
|
||||
return db.transaction('r', db.entries, db.days, async () => {
|
||||
const [entries, days] = await Promise.all([db.entries.toArray(), db.days.toArray()]);
|
||||
return { entries, days };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop days older than the retention window. The most recent closed day is always
|
||||
* kept — it is the app's primary output and must survive any setting.
|
||||
*/
|
||||
export async function cleanupOldDays(retentionDays) {
|
||||
export async function cleanupOldDays(retentionDays, now = Date.now()) {
|
||||
if (!retentionDays || retentionDays <= 0) return 0;
|
||||
const cutoff = localDateKey(Date.now() - retentionDays * DAY);
|
||||
const keep = await getLastClosedDay();
|
||||
|
||||
const stale = (await db.days.toArray()).filter(
|
||||
(d) => d.date < cutoff && d.date !== keep?.date,
|
||||
);
|
||||
if (!stale.length) return 0;
|
||||
|
||||
const keys = stale.map((d) => d.date);
|
||||
await db.transaction('rw', db.days, db.entries, async () => {
|
||||
const cutoff = localDateKey(now - retentionDays * DAY);
|
||||
return db.transaction('rw', db.days, db.entries, async () => {
|
||||
const keep = await getLastClosedDay();
|
||||
const stale = (await db.days.toArray()).filter(
|
||||
(d) => d.status === 'closed' && d.date < cutoff && d.date !== keep?.date,
|
||||
);
|
||||
const keys = stale.map((d) => d.date);
|
||||
if (!keys.length) return 0;
|
||||
await db.days.bulkDelete(keys);
|
||||
await db.entries.where('date').anyOf(keys).delete();
|
||||
return keys.length;
|
||||
});
|
||||
return keys.length;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user