Dentolize · Update-Button & Conversation Tags Fix Walkthrough
On this pageBusiness viewTechnical view

WhatsApp Conversation Tag Sync

Business view

Dentolize's official WhatsApp integration lets a clinic message patients and leads through WhatsApp, and lets staff filter or automate around Patient Tags (e.g. "VIP", "Insurance Pending") directly from the chat list. To make that fast, each WhatsApp conversation stores its own copy of the linked patient's tags, rather than looking them up fresh every time.

That second copy was never being kept up to date:

  • Editing a lead's tags (from the lead form) never propagated to the

conversation's copy — even though the code clearly intended to do this.

  • Creating a brand-new patient directly from a WhatsApp conversation never

seeded the conversation's tag copy from the tags chosen on the new patient form.

  • The "Add new patient" button inside a conversation didn't even carry the

conversation's existing tags into the pre-filled form, so staff had to re-add tags by hand after conversion.

The practical effect: any WhatsApp automation, filter, or view that relies on a conversation's tags could be looking at stale or empty data — tags a patient clearly had, according to their record, would not show up on their WhatsApp thread, and any rule set up to route or auto-respond based on tags could silently never fire.

All three gaps are closed in this PR. Tags now flow both ways: editing a lead updates its conversation(s), creating a patient from a conversation seeds the conversation's tags, and opening "Add new patient" from a conversation carries over whatever tags the conversation already had.

Technical view

The data model

Both Patient and OnlineConversation have their own patientTags relation to PatientTag (packages/server/src/types.graphql:3043 for the conversation side). They are two separate join-table rows, not one shared source — every place that changes a patient's tags has to explicitly decide whether a linked conversation's copy needs updating too, and a patient can reach a conversation two different ways:

  • Via the lead: Lead.onlineConversation
  • Via the patient directly: Patient.onlineConversation

A lead and its patient can each be linked to a different OnlineConversation (or the same one, or none) — hence the fix has to account for up to two records.

Bug 1 — editing a lead never touched the conversation

File: packages/server/src/resolvers/mutations/actions/leads/editLead.js

The mutation always contained a block intended to push tag changes to the conversation:

if (updatedItem.onlineConversation) {
  await tx.onlineConversation.update({
    where: { id: updatedItem.onlineConversation.id },
    data: { patientTags: { set: patientTags ? patientTags.map(id => ({ id })) : [] } }
  })
}

The bug: the Prisma select used to build updatedItem never asked for onlineConversation, so updatedItem.onlineConversation was always undefined — the block's guard was permanently false, and it silently never ran. Same problem existed on the initial item fetch used elsewhere in the function.

The fix, editLead.js:57 and editLead.js:224, adds onlineConversation: { select: { id: true } } to both the item (initial read) and updatedItem (post-update read) selects, so the id is actually available.

It also rewrites the update logic itself (editLead.js:254-274):

// A conversation hangs off the lead, off the patient behind it, or off both, and each keeps its own copy of the
// tags. This only ever looked at the lead's, which the select never asked for, so no conversation was ever updated.
const previousTags = item.patient?.patientTags || []
const nextTags = patientTags || []

const tagsChanged =
  previousTags.length !== nextTags.length || previousTags.some(tag => !nextTags.includes(tag.id))

if (tagsChanged) {
  const conversations = [...new Set([updatedItem.onlineConversation?.id, item.patient?.onlineConversation?.id])].filter(
    Boolean
  )

  for await (const id of conversations) {
    await tx.onlineConversation.update({
      where: { id },
      data: { patientTags: { set: nextTags.map(tag => ({ id: tag })) } },
      select: { id: true }
    })
  }
}

Two behavioral changes beyond "actually select the id":

  1. Both possible conversations are updated, not just the lead's. The

lead's conversation (updatedItem.onlineConversation) and the patient's own conversation (item.patient?.onlineConversation) are collected into a Set (deduping when they're the same record) and both get the new tag list.

  1. The update is skipped when tags didn't actually change

(tagsChanged), avoiding an unnecessary write on every lead edit.

Bug 2 — new patients from a conversation never seeded the conversation's tags

File: packages/server/src/resolvers/mutations/actions/patient/addNewPatient.js:239-244

When a new patient is created with a linked conversation (newItem.onlineConversation), the mutation already reassigned any orphaned appointments on that phone number to the new patient. Now it also writes the patient's chosen tags back onto the conversation:

// The conversation keeps its own copy of the tags, so it takes the ones the patient was created with
await tx.onlineConversation.update({
  where: { id: newItem.onlineConversation.id },
  data: { patientTags: { set: (args.patientTags || []).map(id => ({ id })) } },
  select: { id: true }
})

Without this, a brand-new patient created from a WhatsApp chat would start with whatever tags the conversation happened to have (usually none), even if the person creating the patient explicitly picked tags on the new patient form.

Bug 3 — the "Add new patient" form didn't know the conversation's tags

Files: packages/clinic-web/src/components/dashboard/officialWhatsapp/conversations/NewPatientButton.js:36 (web) and packages/clinic-mobile/src/components/dashboard/WhatsApp/conversationMessages/ConversationMessagesScreen.js:74 (mobile)

Both places build the pre-filled data handed to the new-patient form when staff tap "Add new patient" from inside a conversation. Neither included the conversation's existing patientTags — so even though the conversation already had tags, the new-patient form always started with an empty tag list. Both now pass patientTags through:

// web (NewPatientButton.js)
conversationId: conversation.id,
patientTags: conversation.patientTags
// mobile (ConversationMessagesScreen.js)
conversationId: data.onlineConversationDetails.id,
patientTags: data.onlineConversationDetails.patientTags

PatientForm.js:328-330 (clinic-web) already knew how to consume a patientTags array on selectedItem and pre-select those tags in the form — it just never received one from this entry point before. Once the patient is created, this closes the loop with Bug 2's fix: the tags shown on the form flow into args.patientTags, which addNewPatient.js then writes back onto the conversation.

The working reference this fix now matches

This wasn't a new pattern invented for this PR — editing a patient's tags directly (not through a lead) already did this correctly, in packages/server/src/resolvers/mutations/actions/patient/editPatient.js:438-449: it selects onlineConversation up front and updates it when the tag list changes. That code path was untouched by this PR because it already worked. The editLead.js fix effectively brings the lead-edit path up to parity with logic that already existed and worked elsewhere in the same codebase.

Net effect

A patient's tags and their linked WhatsApp conversation's tags now stay in sync in both directions this PR touches: editing a lead's tags updates the conversation, and creating a patient from a conversation seeds the conversation from the form (which itself now starts pre-filled from the conversation). There is no migration or backfill in this PR — conversations whose tags drifted out of sync before this fix shipped will only catch up the next time their linked lead is edited or a new patient is created from them.