Initial commit: SKEEN Derma Experts - Sistema Integral de Gestión Clínica
- Frontend React (SKEEN Brand) con Vite, TypeScript, Tailwind - Frontend Homenest (versión alternativa) - Módulos Odoo 17 custom (citas, pacientes, monedero, pagos, ventas, inventario, whatsapp) - WACRM fork (Next.js 16 + Supabase) - Hermes + Bridge + Skills (Qwen3.6 via Nan Builders) - Scripts de migración y operación - Documentación extensiva en docs/
This commit is contained in:
71
wacrm/src/lib/inbox/conversations.ts
Normal file
71
wacrm/src/lib/inbox/conversations.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { Conversation, Contact, Tag } from "@/types";
|
||||
|
||||
/**
|
||||
* Conversation select that embeds the contact plus its tags, so the Inbox
|
||||
* can filter conversations by contact tag without a second round-trip.
|
||||
* `contact_tags(tags(*))` returns the join rows; {@link normalizeConversation}
|
||||
* flattens them onto `contact.tags`.
|
||||
*/
|
||||
export const CONVERSATION_SELECT =
|
||||
"*, contact:contacts(*, contact_tags(tags(*)))";
|
||||
|
||||
/** Raw shape returned by {@link CONVERSATION_SELECT} before flattening. */
|
||||
type RawContact = Contact & { contact_tags?: { tags: Tag | null }[] };
|
||||
type RawConversation = Omit<Conversation, "contact"> & {
|
||||
contact?: RawContact | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Flatten the embedded `contact_tags(tags(*))` join into `contact.tags`.
|
||||
* Safe to call on rows fetched with {@link CONVERSATION_SELECT}; a row with
|
||||
* no contact (e.g. a freshly-inserted conversation) passes through untouched.
|
||||
*/
|
||||
export function normalizeConversation(raw: RawConversation): Conversation {
|
||||
const rawContact = raw.contact;
|
||||
if (!rawContact) return raw as Conversation;
|
||||
|
||||
const { contact_tags, ...contact } = rawContact;
|
||||
return {
|
||||
...raw,
|
||||
contact: {
|
||||
...contact,
|
||||
tags: (contact_tags ?? [])
|
||||
.map((ct) => ct.tags)
|
||||
.filter((t): t is Tag => t != null),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeConversations(
|
||||
rows: RawConversation[],
|
||||
): Conversation[] {
|
||||
return rows.map(normalizeConversation);
|
||||
}
|
||||
|
||||
export interface ContactFilters {
|
||||
/** Tag ids; a conversation matches if its contact has ANY of them (OR). */
|
||||
tagIds: string[];
|
||||
/** Exact company match, or null for no company filter. */
|
||||
company: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a conversation passes the contact-based Inbox filters (issue #272).
|
||||
* Empty `tagIds` and null `company` are no-ops, so the default (no filters)
|
||||
* always matches. Tags use OR logic, consistent with Broadcast audiences.
|
||||
*/
|
||||
export function matchesContactFilters(
|
||||
conversation: Conversation,
|
||||
{ tagIds, company }: ContactFilters,
|
||||
): boolean {
|
||||
if (tagIds.length > 0) {
|
||||
const contactTagIds = conversation.contact?.tags ?? [];
|
||||
if (!contactTagIds.some((t) => tagIds.includes(t.id))) return false;
|
||||
}
|
||||
|
||||
if (company !== null && conversation.contact?.company?.trim() !== company) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user