first commit

This commit is contained in:
2026-07-17 21:48:37 +05:30
commit f85599dac5
124 changed files with 10775 additions and 0 deletions
+218
View File
@@ -0,0 +1,218 @@
import type {
AgentAvailability,
Bootstrap,
Channel,
InboxItem,
Message,
Notification,
SidebarSection,
Ticket,
User,
Workspace,
} from "@lynkd/messaging-inbox-sdk";
const now = Date.now();
const min = 60_000;
const hr = 60 * min;
const day = 24 * hr;
/* ------------------------------- users -------------------------------- */
export const users: User[] = [
{ id: "u_me", name: "James Carter", handle: "james", avatarText: "JC", avatarColor: "#FDA913", title: "Property Owner", presence: "active", statusEmoji: "🏗️", statusText: "On site — Territory 4", timezone: "America/Chicago", localTime: "9:41 AM", email: "james@lynkedup.pro", pronouns: "he/him" },
{ id: "u_ava", name: "Ava Mitchell", handle: "ava", avatarText: "AM", avatarColor: "#5b9dff", title: "Ops Lead", presence: "active", statusText: "In a meeting", timezone: "America/New_York" },
{ id: "u_noah", name: "Noah Bennett", handle: "noah", avatarText: "NB", avatarColor: "#3ecf8e", title: "Field Supervisor", presence: "away", timezone: "America/Chicago" },
{ id: "u_mia", name: "Mia Torres", handle: "mia", avatarText: "MT", avatarColor: "#bb98ff", title: "Estimator", presence: "active", statusEmoji: "📐", timezone: "America/Denver" },
{ id: "u_liam", name: "Liam Foster", handle: "liam", avatarText: "LF", avatarColor: "#ff9f66", title: "Dispatch", presence: "dnd", statusText: "Heads-down", timezone: "America/Chicago" },
{ id: "u_zoe", name: "Zoe Nguyen", handle: "zoe", avatarText: "ZN", avatarColor: "#f2555a", title: "Customer Success", presence: "active", timezone: "America/Los_Angeles" },
{ id: "u_ethan", name: "Ethan Cole", handle: "ethan", avatarText: "EC", avatarColor: "#54e7ff", title: "Subcontractor", presence: "offline", timezone: "America/Chicago" },
{ id: "u_sara", name: "Sara Khan", handle: "sara", avatarText: "SK", avatarColor: "#f5b544", title: "Account Manager", presence: "active", timezone: "America/New_York" },
{ id: "u_bot", name: "LynkBot", handle: "lynkbot", avatarText: "LB", avatarColor: "#8c8c94", title: "Automation", presence: "active", isBot: true },
];
export const me = users[0];
export const workspace: Workspace = {
id: "ws_lynkd",
name: "LynkedUp Pro",
initials: "LP",
accent: "#FDA913",
plan: "Business+",
};
export const workspaces: Workspace[] = [
workspace,
{ id: "ws_storm", name: "Storm Response", initials: "SR", accent: "#5b9dff", plan: "Pro" },
{ id: "ws_hq", name: "LynkedUp HQ", initials: "HQ", accent: "#3ecf8e", plan: "Enterprise" },
];
/* ------------------------------ sections ------------------------------ */
export const sections: SidebarSection[] = [
{ id: "starred", label: "Starred", collapsible: true },
{ id: "channels", label: "Channels", collapsible: true },
{ id: "dms", label: "Direct messages", collapsible: true },
];
/* ------------------------------ channels ------------------------------ */
export const channels: Channel[] = [
{ id: "c_general", kind: "channel", name: "general", topic: "Company-wide announcements and work-based matters", memberIds: ["u_me", "u_ava", "u_noah", "u_mia", "u_liam", "u_zoe", "u_sara", "u_bot"], isStarred: true, sectionId: "starred", unreadCount: 3, mentionCount: 1, lastActivityTs: now - 4 * min },
{ id: "c_field", kind: "channel", name: "field-ops", topic: "Crews, schedules, and on-site coordination", memberIds: ["u_me", "u_noah", "u_liam", "u_ethan"], isStarred: true, sectionId: "starred", unreadCount: 7, lastActivityTs: now - 12 * min },
{ id: "c_leads", kind: "channel", name: "leads-pipeline", topic: "New leads and verification queue", memberIds: ["u_me", "u_ava", "u_sara", "u_zoe"], sectionId: "channels", unreadCount: 0, lastActivityTs: now - 2 * hr },
{ id: "c_estimates", kind: "channel", name: "estimates", topic: "ProCanvas estimates & approvals", memberIds: ["u_me", "u_mia", "u_ava"], sectionId: "channels", unreadCount: 2, lastActivityTs: now - 40 * min },
{ id: "c_storm", kind: "channel", name: "storm-intel", topic: "Weather events & rapid dispatch", memberIds: ["u_me", "u_liam", "u_noah", "u_bot"], sectionId: "channels", unreadCount: 0, lastActivityTs: now - 5 * hr },
{ id: "c_random", kind: "channel", name: "random", topic: "Non-work banter", memberIds: ["u_me", "u_ava", "u_noah", "u_mia", "u_zoe"], sectionId: "channels", isMuted: true, unreadCount: 0, lastActivityTs: now - day },
{ id: "c_partners", kind: "channel", name: "acme-partners", topic: "Shared with Acme Roofing", memberIds: ["u_me", "u_sara", "u_ethan"], sectionId: "channels", external: true, unreadCount: 1, lastActivityTs: now - 3 * hr },
{ id: "c_private", kind: "private", name: "leadership", topic: "Leadership only", memberIds: ["u_me", "u_ava", "u_sara"], sectionId: "channels", unreadCount: 0, lastActivityTs: now - 6 * hr },
// DMs
{ id: "d_ava", kind: "dm", name: "Ava Mitchell", memberIds: ["u_me", "u_ava"], sectionId: "dms", unreadCount: 0, mentionCount: 0, lastActivityTs: now - 20 * min },
{ id: "d_liam", kind: "dm", name: "Liam Foster", memberIds: ["u_me", "u_liam"], sectionId: "dms", unreadCount: 2, mentionCount: 2, lastActivityTs: now - 8 * min },
{ id: "d_bot", kind: "dm", name: "LynkBot", memberIds: ["u_me", "u_bot"], sectionId: "dms", unreadCount: 0, lastActivityTs: now - hr },
{ id: "g_crew", kind: "group_dm", name: "Noah, Mia, Zoe", memberIds: ["u_me", "u_noah", "u_mia", "u_zoe"], sectionId: "dms", unreadCount: 0, lastActivityTs: now - 90 * min },
];
/* ------------------------------ messages ------------------------------ */
let seq = 1;
function m(
channelId: string,
authorId: string,
minutesAgo: number,
body: string,
extra: Partial<Message> = {},
): Message {
return {
id: `m_${seq++}`,
channelId,
authorId,
ts: now - minutesAgo * min,
body,
reactions: [],
deliveryState: "sent",
...extra,
};
}
export const messages: Message[] = [
// #general
m("c_general", "u_ava", 190, "Morning team 👋 Reminder: Q3 territory review is Thursday 10am CT. Agenda in the canvas."),
m("c_general", "u_sara", 180, "Thanks Ava. I'll add the Acme renewal numbers.", { reactions: [{ emoji: "🙌", userIds: ["u_ava", "u_me"] }] }),
m("c_general", "u_bot", 120, "Deploy `web@1.9.2` shipped to production. 0 errors in the last hour.", { systemEvent: undefined, reactions: [{ emoji: "🚀", userIds: ["u_me", "u_noah", "u_mia"] }] }),
m("c_general", "u_ava", 45, "@james can you approve the new estimate template before the review?", { mentions: ["u_me"], replyCount: 2, replyUserIds: ["u_me", "u_mia"], lastReplyTs: now - 30 * min }),
m("c_general", "u_me", 30, "On it — reviewing now. Looks solid.", { parentId: "m_4" }),
m("c_general", "u_mia", 29, "Added the line-item breakdown you asked for 📐", { parentId: "m_4" }),
m("c_general", "u_noah", 4, "Crew 2 wrapped the Henderson job early. Photos uploaded.", { attachments: [{ id: "a1", kind: "image", name: "henderson-roof.jpg", sizeLabel: "2.4 MB", previewColor: "#2d3a2f" }], reactions: [{ emoji: "🔥", userIds: ["u_me"] }, { emoji: "👏", userIds: ["u_ava", "u_sara"] }] }),
// #field-ops
m("c_field", "u_liam", 300, "Dispatch board for today is set. 6 jobs, 3 crews."),
m("c_field", "u_noah", 240, "Copy. Crew 1 starting at the Willow St property."),
m("c_field", "u_ethan", 60, "Running 20 min late to the 2pm — traffic on I-35.", { reactions: [{ emoji: "👍", userIds: ["u_liam"] }] }),
m("c_field", "u_liam", 40, "No problem, pushed the window. Customer notified.", { blocks: [{ type: "callout", text: "SLA: customer must be notified within 15 min of any delay." }] }),
m("c_field", "u_noah", 12, "Weather alert incoming for Sector 4 — see #storm-intel.", { mentions: [], reactions: [] }),
// #estimates
m("c_estimates", "u_mia", 120, "Uploaded 3 new estimates for review.", { attachments: [{ id: "a2", kind: "file", name: "estimate-4471.pdf", sizeLabel: "180 KB", previewColor: "#3a2f2d" }] }),
m("c_estimates", "u_ava", 40, "Approved 4471 and 4472. 4473 needs a material adjustment.", { reactions: [{ emoji: "✅", userIds: ["u_mia"] }] }),
// #leads-pipeline
m("c_leads", "u_zoe", 200, "12 new leads from the storm campaign overnight 🌩️"),
m("c_leads", "u_sara", 150, "Verified 8 of them. 4 flagged for manual review.", { blocks: [{ type: "bullet", items: ["Verified: 8", "Manual review: 4", "Duplicate: 0"] }] }),
// #storm-intel
m("c_storm", "u_bot", 300, "⚠️ NWS: Severe thunderstorm watch for Sector 4 until 6pm CT. Hail up to 1in possible."),
m("c_storm", "u_liam", 290, "Rerouting crews away from Sector 4. Safety first."),
// #acme-partners (external)
m("c_partners", "u_ethan", 180, "Hi team — Acme here. We can take the overflow inspections this week.", { reactions: [{ emoji: "🤝", userIds: ["u_me", "u_sara"] }] }),
// DM with Liam
m("d_liam", "u_liam", 12, "Hey James — can you sign off on the overtime for crew 2?"),
m("d_liam", "u_liam", 8, "@james ^ need it before payroll closes at 5.", { mentions: ["u_me"] }),
// DM with Ava
m("d_ava", "u_ava", 60, "Sending the review deck over in a few."),
m("d_ava", "u_me", 20, "Perfect, thanks!"),
// DM with bot
m("d_bot", "u_bot", 60, "Your daily digest: 3 mentions, 2 tickets awaiting reply, 1 estimate pending approval.", { blocks: [{ type: "bullet", items: ["3 mentions across #general, #field-ops", "2 support tickets need a reply", "Estimate 4473 pending approval"] }] }),
// group DM
m("g_crew", "u_zoe", 100, "Team lunch Friday? 🍕"),
m("g_crew", "u_mia", 95, "I'm in!", { reactions: [{ emoji: "🍕", userIds: ["u_zoe", "u_noah"] }] }),
];
/* ------------------------------- inbox -------------------------------- */
export const inbox: InboxItem[] = [
{ id: "i1", kind: "MENTION", state: "OPEN", title: "Liam Foster mentioned you", preview: "@james ^ need it before payroll closes at 5.", channelId: "d_liam", messageId: "m_21", actorId: "u_liam", ts: now - 8 * min, priority: "high" },
{ id: "i2", kind: "NEEDS_REPLY", state: "OPEN", title: "Ava is waiting on approval", preview: "can you approve the new estimate template before the review?", channelId: "c_general", messageId: "m_4", actorId: "u_ava", ts: now - 45 * min, priority: "normal" },
{ id: "i3", kind: "SUPPORT_UPDATE", state: "OPEN", title: "Ticket TCK-1042 needs a reply", preview: "Customer replied: 'Still seeing the sync issue on mobile.'", actorId: "u_zoe", ts: now - 25 * min, priority: "urgent" },
{ id: "i4", kind: "REACTION", state: "OPEN", title: "Noah reacted 🔥 to your message", preview: "Crew 2 wrapped the Henderson job early.", channelId: "c_general", actorId: "u_noah", ts: now - 3 * min, priority: "low" },
{ id: "i5", kind: "THREAD_REPLY", state: "OPEN", title: "New reply in a thread you're in", preview: "Mia: Added the line-item breakdown you asked for", channelId: "c_general", messageId: "m_4", actorId: "u_mia", ts: now - 29 * min, priority: "normal" },
{ id: "i6", kind: "MEETING_FOLLOWUP", state: "OPEN", title: "Follow-up: Q3 review action items", preview: "Send revised payment schedule to Acme", ts: now - 2 * hr, dueAt: now + day, priority: "normal" },
{ id: "i7", kind: "SAVED", state: "OPEN", title: "Saved for later", preview: "SLA: customer must be notified within 15 min of any delay.", channelId: "c_field", messageId: "m_11", ts: now - hr, priority: "low" },
{ id: "i8", kind: "APP", state: "OPEN", title: "LynkBot digest ready", preview: "Your daily digest: 3 mentions, 2 tickets awaiting reply.", channelId: "d_bot", actorId: "u_bot", ts: now - hr, priority: "low" },
];
/* ------------------------------ tickets ------------------------------- */
function tmsg(id: string, ticketId: string, authorId: string, minutesAgo: number, body: string): Message {
return { id, channelId: `t_${ticketId}`, authorId, ts: now - minutesAgo * min, body, deliveryState: "sent" };
}
export const tickets: Ticket[] = [
{
id: "TCK-1042", ref: "TCK-1042", subject: "Mobile app not syncing new leads", requesterId: "u_zoe", assigneeId: "u_me",
state: "OPEN", priority: "URGENT", channelId: "c_leads", category: "Sync / Data", createdTs: now - 3 * hr, updatedTs: now - 25 * min,
firstResponseDueTs: now - 10 * min, slaBreached: true, tags: ["mobile", "sync", "p1"],
messages: [
tmsg("tm1", "1042", "u_zoe", 180, "Customer reports new leads aren't appearing on the mobile app after the last update."),
tmsg("tm2", "1042", "u_me", 150, "Thanks — can you confirm the app version and device?"),
tmsg("tm3", "1042", "u_zoe", 25, "Still seeing the sync issue on mobile. iOS 18, app v1.9.1."),
],
},
{
id: "TCK-1041", ref: "TCK-1041", subject: "Estimate PDF export missing logo", requesterId: "u_mia", assigneeId: "u_me",
state: "PENDING", priority: "NORMAL", channelId: "c_estimates", category: "Estimates", createdTs: now - day, updatedTs: now - 4 * hr, tags: ["export", "branding"],
messages: [
tmsg("tm4", "1041", "u_mia", 1440, "Exported estimates are missing the company logo in the header."),
tmsg("tm5", "1041", "u_me", 300, "Reproduced. Fix queued for the next release. Marking pending."),
],
},
{
id: "TCK-1040", ref: "TCK-1040", subject: "Request: SMS notifications for dispatch", requesterId: "u_liam", state: "NEW", priority: "LOW", category: "Feature request", createdTs: now - 2 * hr, updatedTs: now - 2 * hr, tags: ["dispatch", "notifications"],
messages: [tmsg("tm6", "1040", "u_liam", 120, "Could we get SMS alerts when a job is reassigned?")],
},
{
id: "TCK-1039", ref: "TCK-1039", subject: "Billing — duplicate charge on invoice 8871", requesterId: "u_sara", assigneeId: "u_ava", state: "RESOLVED", priority: "HIGH", category: "Billing", createdTs: now - 3 * day, updatedTs: now - day, csat: 5, tags: ["billing"],
messages: [
tmsg("tm7", "1039", "u_sara", 4320, "Client was charged twice on invoice 8871."),
tmsg("tm8", "1039", "u_ava", 1500, "Refund processed. Confirmed with the client. Closing out."),
],
},
{
id: "TCK-1038", ref: "TCK-1038", subject: "Territory map tiles not loading", requesterId: "u_noah", assigneeId: "u_me", state: "CLOSED", priority: "NORMAL", category: "Maps", createdTs: now - 5 * day, updatedTs: now - 4 * day, csat: 4, tags: ["maps"],
messages: [tmsg("tm9", "1038", "u_noah", 7200, "Map tiles were blank on the territory view. Seems fixed now.")],
},
];
export const availability: AgentAvailability[] = [
{ userId: "u_me", online: true, activeCount: 2, maxActive: 5, queue: "Tier 1" },
{ userId: "u_ava", online: true, activeCount: 1, maxActive: 4, queue: "Billing" },
{ userId: "u_zoe", online: true, activeCount: 3, maxActive: 5, queue: "Success" },
{ userId: "u_sara", online: false, activeCount: 0, maxActive: 4, queue: "Accounts" },
];
/* --------------------------- notifications ---------------------------- */
export const notifications: Notification[] = [
{ id: "n1", kind: "MENTION", title: "Liam Foster", body: "mentioned you in a DM", ts: now - 8 * min, read: false, channelId: "d_liam", messageId: "m_21" },
{ id: "n2", kind: "SUPPORT_UPDATE", title: "TCK-1042", body: "SLA breached — first response overdue", ts: now - 10 * min, read: false },
{ id: "n3", kind: "REACTION", title: "Noah Bennett", body: "reacted 🔥 to your message", ts: now - 3 * min, read: false, channelId: "c_general", messageId: "m_7" },
{ id: "n4", kind: "SYSTEM", title: "LynkBot", body: "Deploy web@1.9.2 shipped to production", ts: now - 2 * hr, read: true },
];
export function bootstrapPayload(): Bootstrap {
return { me, workspace, workspaces, users, sections, channels };
}
+291
View File
@@ -0,0 +1,291 @@
import type {
InboxItem,
InboxState,
Message,
SearchResult,
Ticket,
TicketState,
} from "@lynkd/messaging-inbox-sdk";
import * as seed from "./data";
/**
* In-memory mutable store. Next.js dev/prod route handlers share one Node
* process, so a module singleton persists across requests for the session.
* This is the ONLY stateful piece of the "backend" — swap it for the IIOS
* client and the BFF routes stay identical.
*/
class MockStore {
users = clone(seed.users);
channels = clone(seed.channels);
messages = clone(seed.messages);
inbox = clone(seed.inbox);
tickets = clone(seed.tickets);
notifications = clone(seed.notifications);
availability = clone(seed.availability);
private idc = 10000;
me() {
return this.users.find((u) => u.id === seed.me.id) ?? this.users[0];
}
bootstrap() {
return {
me: this.me(),
workspace: seed.workspace,
workspaces: seed.workspaces,
users: this.users,
sections: seed.sections,
channels: this.channels,
};
}
addMember(channelId: string, userId: string) {
const c = this.channels.find((x) => x.id === channelId);
if (c && !c.memberIds.includes(userId)) c.memberIds.push(userId);
return c ?? null;
}
removeMember(channelId: string, userId: string) {
const c = this.channels.find((x) => x.id === channelId);
if (c) c.memberIds = c.memberIds.filter((id) => id !== userId);
return c ?? null;
}
markRead(channelId: string) {
const c = this.channels.find((x) => x.id === channelId);
if (c) {
c.unreadCount = 0;
c.mentionCount = 0;
}
return c ?? null;
}
editMessage(messageId: string, body: string): Message | null {
const m = this.messages.find((x) => x.id === messageId);
if (!m) return null;
m.body = body;
m.editedTs = Date.now();
return m;
}
deleteMessage(messageId: string): boolean {
const before = this.messages.length;
this.messages = this.messages.filter((m) => m.id !== messageId && m.parentId !== messageId);
return this.messages.length !== before;
}
savedMessages(): Message[] {
return this.messages.filter((m) => m.isSaved).sort((a, b) => b.ts - a.ts);
}
threadParents(): Message[] {
return this.messages.filter((m) => (m.replyCount ?? 0) > 0).sort((a, b) => (b.lastReplyTs ?? b.ts) - (a.lastReplyTs ?? a.ts));
}
setStatus(text: string | undefined, emoji: string | undefined, clearAt?: number) {
const me = this.me();
me.statusText = text;
me.statusEmoji = emoji;
me.statusClearAt = clearAt;
return me;
}
updateMe(patch: { name?: string; title?: string; avatarColor?: string; avatarUrl?: string | null; presence?: string }) {
const me = this.me();
if (patch.name !== undefined) me.name = patch.name;
if (patch.title !== undefined) me.title = patch.title;
if (patch.avatarColor !== undefined) me.avatarColor = patch.avatarColor;
if (patch.avatarUrl !== undefined) me.avatarUrl = patch.avatarUrl ?? undefined;
if (patch.presence !== undefined) me.presence = patch.presence as any;
return me;
}
channelMessages(channelId: string): Message[] {
return this.messages
.filter((m) => m.channelId === channelId && !m.parentId)
.sort((a, b) => a.ts - b.ts);
}
thread(channelId: string, parentId: string) {
const parent = this.messages.find((m) => m.id === parentId) ?? null;
const replies = this.messages
.filter((m) => m.parentId === parentId)
.sort((a, b) => a.ts - b.ts);
return { parent, replies };
}
send(
channelId: string,
body: string,
opts: { parentId?: string; scheduledFor?: number; attachments?: any[]; threadRootId?: string | null } = {},
): Message {
const msg: Message = {
id: `m_new_${this.idc++}`,
channelId,
authorId: seed.me.id,
ts: Date.now(),
body,
reactions: [],
deliveryState: "sent",
parentId: opts.parentId ?? null,
scheduledFor: opts.scheduledFor,
attachments: opts.attachments && opts.attachments.length ? opts.attachments : undefined,
threadRootId: opts.threadRootId ?? undefined,
};
this.messages.push(msg);
if (opts.parentId) {
const parent = this.messages.find((m) => m.id === opts.parentId);
if (parent) {
parent.replyCount = (parent.replyCount ?? 0) + 1;
parent.lastReplyTs = msg.ts;
parent.replyUserIds = Array.from(new Set([...(parent.replyUserIds ?? []), seed.me.id]));
}
}
const ch = this.channels.find((c) => c.id === channelId);
if (ch) ch.lastActivityTs = msg.ts;
return msg;
}
createChannel(input: { name: string; kind?: string; topic?: string; memberIds?: string[] }) {
const kind = (input.kind as any) || "channel";
const isDm = kind === "dm" || kind === "group_dm";
const id = `${isDm ? "d" : "c"}_new_${this.idc++}`;
const channel: any = {
id,
kind,
name: input.name.replace(/^#/, "").trim() || "new-channel",
topic: input.topic,
memberIds: Array.from(new Set([seed.me.id, ...(input.memberIds ?? [])])),
sectionId: isDm ? "dms" : "channels",
unreadCount: 0,
lastActivityTs: Date.now(),
};
this.channels.push(channel);
return channel;
}
react(messageId: string, emoji: string): Message | null {
const msg = this.messages.find((m) => m.id === messageId);
if (!msg) return null;
msg.reactions = msg.reactions ?? [];
const r = msg.reactions.find((x) => x.emoji === emoji);
const uid = seed.me.id;
if (r) {
if (r.userIds.includes(uid)) {
r.userIds = r.userIds.filter((id) => id !== uid);
if (r.userIds.length === 0) msg.reactions = msg.reactions.filter((x) => x.emoji !== emoji);
} else {
r.userIds.push(uid);
}
} else {
msg.reactions.push({ emoji, userIds: [uid] });
}
return msg;
}
pin(messageId: string): Message | null {
const msg = this.messages.find((m) => m.id === messageId);
if (!msg) return null;
msg.isPinned = !msg.isPinned;
return msg;
}
save(messageId: string): Message | null {
const msg = this.messages.find((m) => m.id === messageId);
if (!msg) return null;
msg.isSaved = !msg.isSaved;
return msg;
}
listInbox(state?: InboxState): InboxItem[] {
const s = state ?? "OPEN";
return this.inbox.filter((i) => i.state === s).sort((a, b) => b.ts - a.ts);
}
patchInbox(id: string, state: InboxState): InboxItem | null {
const item = this.inbox.find((i) => i.id === id);
if (!item) return null;
item.state = state;
return item;
}
listTickets(scope?: string): Ticket[] {
let list = this.tickets;
if (scope === "mine") list = list.filter((t) => t.requesterId === seed.me.id || t.assigneeId === seed.me.id);
if (scope === "assigned") list = list.filter((t) => t.assigneeId === seed.me.id);
return [...list].sort((a, b) => b.updatedTs - a.updatedTs);
}
getTicket(id: string): Ticket | null {
return this.tickets.find((t) => t.id === id) ?? null;
}
patchTicket(id: string, state: TicketState): Ticket | null {
const t = this.tickets.find((x) => x.id === id);
if (!t) return null;
t.state = state;
t.updatedTs = Date.now();
if (state === "RESOLVED" || state === "CLOSED") t.slaBreached = false;
return t;
}
replyTicket(id: string, body: string): Ticket | null {
const t = this.tickets.find((x) => x.id === id);
if (!t) return null;
t.messages = t.messages ?? [];
t.messages.push({
id: `tm_new_${this.idc++}`,
channelId: `t_${id}`,
authorId: seed.me.id,
ts: Date.now(),
body,
deliveryState: "sent",
});
t.updatedTs = Date.now();
if (t.state === "NEW") t.state = "OPEN";
t.slaBreached = false;
return t;
}
search(q: string): SearchResult[] {
const query = q.toLowerCase().trim();
if (!query) return [];
const out: SearchResult[] = [];
for (const c of this.channels) {
if (c.name.toLowerCase().includes(query) || c.topic?.toLowerCase().includes(query)) {
out.push({ id: c.id, type: "channel", title: c.kind === "channel" ? `#${c.name}` : c.name, subtitle: c.topic, channelId: c.id });
}
}
for (const u of this.users) {
if (u.name.toLowerCase().includes(query) || u.handle.includes(query)) {
out.push({ id: u.id, type: "person", title: u.name, subtitle: u.title });
}
}
for (const msg of this.messages) {
if (msg.body.toLowerCase().includes(query)) {
out.push({ id: msg.id, type: "message", title: msg.body.slice(0, 80), subtitle: `in ${this.channelLabel(msg.channelId)}`, channelId: msg.channelId, ts: msg.ts, parentId: msg.parentId ?? undefined });
}
}
for (const t of this.tickets) {
if (t.subject.toLowerCase().includes(query) || t.ref.toLowerCase().includes(query)) {
out.push({ id: t.id, type: "ticket", title: `${t.ref} · ${t.subject}`, subtitle: t.state });
}
}
return out.slice(0, 20);
}
private channelLabel(id: string): string {
const c = this.channels.find((x) => x.id === id);
if (!c) return "conversation";
return c.kind === "channel" || c.kind === "private" ? `#${c.name}` : c.name;
}
}
function clone<T>(v: T): T {
return JSON.parse(JSON.stringify(v));
}
// preserve the singleton across HMR reloads in dev (bump the key when the
// store's shape/methods change so HMR rebuilds it instead of reusing a stale one)
const g = globalThis as unknown as { __lynkdStore_v3?: MockStore };
export const store = g.__lynkdStore_v3 ?? (g.__lynkdStore_v3 = new MockStore());