feat(timeline): add pulse animation for in-flight drag mutations
Allocation bars that have active optimistic overrides (post-drag, awaiting server confirmation) now pulse subtly via animate-pulse. The pending set is derived from the existing optimisticAllocations map keys, requiring no additional state. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,10 @@ type StoredAliasMap = Record<string, StoredAliasEntry>;
|
||||
const ALIAS_NAME_RE = /^[A-Za-z]+(?: [A-Za-z]+)*$/;
|
||||
const ALIAS_SLUG_RE = /^[a-z]+(?:\.[a-z]+)*$/;
|
||||
|
||||
// Module-level TTL cache for the anonymization directory (60 s)
|
||||
let _directoryCache: { value: AnonymizationDirectory | null; expiresAt: number } | null = null;
|
||||
const DIRECTORY_TTL_MS = 60_000;
|
||||
|
||||
const ICONIC_ALIAS_NAMES = [
|
||||
"Iron Man",
|
||||
"Spider Man",
|
||||
@@ -650,6 +654,10 @@ export async function getAnonymizationConfig(
|
||||
};
|
||||
}
|
||||
|
||||
export function invalidateAnonymizationDirectoryCache() {
|
||||
_directoryCache = null;
|
||||
}
|
||||
|
||||
export async function getAnonymizationDirectory(
|
||||
db: Pick<PrismaClient, "systemSettings" | "resource">,
|
||||
): Promise<AnonymizationDirectory | null> {
|
||||
@@ -657,6 +665,11 @@ export async function getAnonymizationDirectory(
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (_directoryCache && _directoryCache.expiresAt > now) {
|
||||
return _directoryCache.value;
|
||||
}
|
||||
|
||||
const settings = await db.systemSettings.findUnique({
|
||||
where: { id: "singleton" },
|
||||
select: {
|
||||
@@ -736,13 +749,22 @@ export async function getAnonymizationDirectory(
|
||||
where: { id: "singleton" },
|
||||
data: { anonymizationAliases: storedAliases },
|
||||
});
|
||||
// Invalidate stale cache after a DB write so the next call re-fetches
|
||||
_directoryCache = null;
|
||||
}
|
||||
|
||||
return {
|
||||
const directory: AnonymizationDirectory = {
|
||||
config,
|
||||
byResourceId,
|
||||
byAliasEid,
|
||||
};
|
||||
|
||||
// Only cache stable directories (no alias changes = steady state)
|
||||
if (!aliasesChanged) {
|
||||
_directoryCache = { value: directory, expiresAt: Date.now() + DIRECTORY_TTL_MS };
|
||||
}
|
||||
|
||||
return directory;
|
||||
}
|
||||
|
||||
export function anonymizeResource<T extends ResourceIdentity>(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getPublicHolidays, type AbsenceDay } from "@capakraken/shared";
|
||||
import { getPublicHolidays, toIsoDate, normalizeCityName, normalizeStateCode, type AbsenceDay } from "@capakraken/shared";
|
||||
export { toIsoDate } from "@capakraken/shared";
|
||||
|
||||
type VacationLike = {
|
||||
startDate: Date;
|
||||
@@ -69,10 +70,6 @@ export function asHolidayResolverDb(db: unknown): HolidayResolverDb {
|
||||
return db as HolidayResolverDb;
|
||||
}
|
||||
|
||||
export function toIsoDate(value: Date): string {
|
||||
return value.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
type CityHolidayRule = {
|
||||
countryCode: string;
|
||||
cityName: string;
|
||||
@@ -93,16 +90,6 @@ const SCOPE_WEIGHT: Record<CalendarScope, number> = {
|
||||
CITY: 3,
|
||||
};
|
||||
|
||||
function normalizeCityName(cityName?: string | null): string | null {
|
||||
const normalized = cityName?.trim().toLowerCase();
|
||||
return normalized && normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
function normalizeStateCode(stateCode?: string | null): string | null {
|
||||
const normalized = stateCode?.trim().toUpperCase();
|
||||
return normalized && normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
function resolveCalendarEntries(
|
||||
calendars: HolidayCalendarRecord[],
|
||||
periodStart: Date,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MILLISECONDS_PER_DAY } from "@capakraken/shared";
|
||||
import { getCalendarHolidayStrings, toIsoDate } from "./holiday-availability.js";
|
||||
|
||||
type VacationSpan = {
|
||||
@@ -59,7 +60,7 @@ export function countCalendarDaysInPeriod(
|
||||
}
|
||||
|
||||
const ms = overlap.end.getTime() - overlap.start.getTime();
|
||||
return Math.round(ms / 86_400_000) + 1;
|
||||
return Math.round(ms / MILLISECONDS_PER_DAY) + 1;
|
||||
}
|
||||
|
||||
export function countVacationChargeableDays(
|
||||
|
||||
Reference in New Issue
Block a user