refactor: distribute types from crm-types.ts to owning composables
Each composable now owns its types and exports them. Other composables import types from the owning composable. Deleted centralized crm-types.ts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,21 +5,189 @@ import {
|
||||
CreateCalendarEventMutationDocument,
|
||||
ArchiveCalendarEventMutationDocument,
|
||||
} from "~~/graphql/generated";
|
||||
import type { CalendarEvent, CalendarView } from "~/composables/crm-types";
|
||||
import {
|
||||
dayKey,
|
||||
formatDay,
|
||||
formatTime,
|
||||
toInputDate,
|
||||
toInputTime,
|
||||
roundToNextQuarter,
|
||||
roundToPrevQuarter,
|
||||
isEventFinalStatus,
|
||||
} from "~/composables/crm-types";
|
||||
|
||||
type CalendarHierarchyView = "year" | "month" | "week" | "day";
|
||||
type CalendarRect = { left: number; top: number; width: number; height: number };
|
||||
|
||||
|
||||
export type CalendarView = "day" | "week" | "month" | "year" | "agenda";
|
||||
|
||||
export type CalendarEvent = {
|
||||
id: string;
|
||||
title: string;
|
||||
start: string;
|
||||
end: string;
|
||||
contact: string;
|
||||
note: string;
|
||||
isArchived: boolean;
|
||||
createdAt: string;
|
||||
archiveNote: string;
|
||||
archivedAt: string;
|
||||
};
|
||||
|
||||
export type EventLifecyclePhase = "scheduled" | "due_soon" | "awaiting_outcome" | "closed";
|
||||
|
||||
export function dayKey(date: Date) {
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const d = String(date.getDate()).padStart(2, "0");
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
export function formatDay(iso: string) {
|
||||
return new Intl.DateTimeFormat("en-GB", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
}).format(new Date(iso));
|
||||
}
|
||||
|
||||
export function formatTime(iso: string) {
|
||||
return new Intl.DateTimeFormat("en-GB", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}).format(new Date(iso));
|
||||
}
|
||||
|
||||
export function formatThreadTime(iso: string) {
|
||||
return new Intl.DateTimeFormat("en-GB", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
})
|
||||
.format(new Date(iso))
|
||||
.replace(":", ".");
|
||||
}
|
||||
|
||||
export function formatStamp(iso: string) {
|
||||
return new Intl.DateTimeFormat("en-GB", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}).format(new Date(iso));
|
||||
}
|
||||
|
||||
export function toInputDate(date: Date) {
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const d = String(date.getDate()).padStart(2, "0");
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
export function toInputTime(date: Date) {
|
||||
const hh = String(date.getHours()).padStart(2, "0");
|
||||
const mm = String(date.getMinutes()).padStart(2, "0");
|
||||
return `${hh}:${mm}`;
|
||||
}
|
||||
|
||||
export function roundToNextQuarter(date = new Date()) {
|
||||
const d = new Date(date);
|
||||
d.setSeconds(0, 0);
|
||||
const minutes = d.getMinutes();
|
||||
const rounded = Math.ceil(minutes / 15) * 15;
|
||||
if (rounded >= 60) {
|
||||
d.setHours(d.getHours() + 1, 0, 0, 0);
|
||||
} else {
|
||||
d.setMinutes(rounded, 0, 0);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
export function roundToPrevQuarter(date = new Date()) {
|
||||
const d = new Date(date);
|
||||
d.setSeconds(0, 0);
|
||||
const minutes = d.getMinutes();
|
||||
const rounded = Math.floor(minutes / 15) * 15;
|
||||
d.setMinutes(rounded, 0, 0);
|
||||
return d;
|
||||
}
|
||||
|
||||
export function atOffset(days: number, hour: number, minute: number) {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() + days);
|
||||
d.setHours(hour, minute, 0, 0);
|
||||
return d.toISOString();
|
||||
}
|
||||
|
||||
export function inMinutes(minutes: number) {
|
||||
const d = new Date();
|
||||
d.setMinutes(d.getMinutes() + minutes, 0, 0);
|
||||
return d.toISOString();
|
||||
}
|
||||
|
||||
export function endAfter(startIso: string, minutes: number) {
|
||||
const d = new Date(startIso);
|
||||
d.setMinutes(d.getMinutes() + minutes);
|
||||
return d.toISOString();
|
||||
}
|
||||
|
||||
export function isEventFinalStatus(isArchived: boolean) {
|
||||
return Boolean(isArchived);
|
||||
}
|
||||
|
||||
export function eventPreDueAt(event: CalendarEvent) {
|
||||
return new Date(new Date(event.start).getTime() - 30 * 60 * 1000).toISOString();
|
||||
}
|
||||
|
||||
export function eventDueAt(event: CalendarEvent) {
|
||||
return event.start;
|
||||
}
|
||||
|
||||
export function eventLifecyclePhase(event: CalendarEvent, nowMs: number): EventLifecyclePhase {
|
||||
if (event.isArchived) return "closed";
|
||||
const dueMs = new Date(eventDueAt(event)).getTime();
|
||||
const preDueMs = new Date(eventPreDueAt(event)).getTime();
|
||||
if (nowMs >= dueMs) return "awaiting_outcome";
|
||||
if (nowMs >= preDueMs) return "due_soon";
|
||||
return "scheduled";
|
||||
}
|
||||
|
||||
export function eventTimelineAt(event: CalendarEvent, phase: EventLifecyclePhase) {
|
||||
if (phase === "scheduled") return event.createdAt || event.start;
|
||||
if (phase === "due_soon") return eventPreDueAt(event);
|
||||
return eventDueAt(event);
|
||||
}
|
||||
|
||||
export function eventRelativeLabel(event: CalendarEvent, nowMs: number) {
|
||||
if (event.isArchived) return "Archived";
|
||||
const diffMs = new Date(event.start).getTime() - nowMs;
|
||||
const minuteMs = 60 * 1000;
|
||||
const hourMs = 60 * minuteMs;
|
||||
const dayMs = 24 * hourMs;
|
||||
const abs = Math.abs(diffMs);
|
||||
|
||||
if (diffMs >= 0) {
|
||||
if (abs >= dayMs) {
|
||||
const days = Math.round(abs / dayMs);
|
||||
return `Event in ${days} day${days === 1 ? "" : "s"}`;
|
||||
}
|
||||
if (abs >= hourMs) {
|
||||
const hours = Math.round(abs / hourMs);
|
||||
return `Event in ${hours} hour${hours === 1 ? "" : "s"}`;
|
||||
}
|
||||
const minutes = Math.max(1, Math.round(abs / minuteMs));
|
||||
return `Event in ${minutes} minute${minutes === 1 ? "" : "s"}`;
|
||||
}
|
||||
|
||||
if (abs >= dayMs) {
|
||||
const days = Math.round(abs / dayMs);
|
||||
return `Overdue by ${days} day${days === 1 ? "" : "s"}`;
|
||||
}
|
||||
if (abs >= hourMs) {
|
||||
const hours = Math.round(abs / hourMs);
|
||||
return `Overdue by ${hours} hour${hours === 1 ? "" : "s"}`;
|
||||
}
|
||||
const minutes = Math.max(1, Math.round(abs / minuteMs));
|
||||
return `Overdue by ${minutes} minute${minutes === 1 ? "" : "s"}`;
|
||||
}
|
||||
|
||||
export function eventPhaseToneClass(phase: EventLifecyclePhase) {
|
||||
if (phase === "awaiting_outcome") return "border-warning/50 bg-warning/10";
|
||||
if (phase === "due_soon") return "border-info/50 bg-info/10";
|
||||
if (phase === "closed") return "border-success/40 bg-success/10";
|
||||
return "border-base-300 bg-base-100";
|
||||
}
|
||||
|
||||
export function useCalendar(opts: { apolloAuthReady: ComputedRef<boolean> }) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Apollo query & mutation
|
||||
|
||||
Reference in New Issue
Block a user