Skip to content

feat(crm): scroll do calendário, destaque de data, e agendar unificad…#59

Merged
sejaanomalo-web merged 1 commit into
mainfrom
claude/crm-calendario-agendar-unificado
Jul 13, 2026
Merged

feat(crm): scroll do calendário, destaque de data, e agendar unificad…#59
sejaanomalo-web merged 1 commit into
mainfrom
claude/crm-calendario-agendar-unificado

Conversation

@sejaanomalo-web

@sejaanomalo-web sejaanomalo-web commented Jul 13, 2026

Copy link
Copy Markdown
Owner

…o na etiqueta

Corrige o calendário cortando meses (flex-wrap quebrava o overflow:auto interno) e destaca dia/mês/semana em selo nos cartões de compromisso. Remove o botão de link do calendário (uso desnecessário por ora).

Unifica "+ Agendar" dentro do picker de etiqueta: marcar uma etapa do funil que implica compromisso (reunião/follow-up/fechamento) abre o mesmo formulário de data, com o detalhe pré-preenchido "Etapa com Fulano" (editável). Etapas sem compromisso associado só trocam a fase na hora. Novo estilo do menu (maior, com emoji por etapa), botão movido pro cabeçalho da conversa no lugar do antigo "+ Agendar".

Summary by CodeRabbit

  • New Features

    • Added emoji indicators to CRM activity labels.
    • Combined label selection and scheduling into a unified workflow.
    • Added scheduling options for activities that require a date and time.
    • Improved appointment cards with date badges and clearer contact details.
  • Updates

    • Simplified the contact activity interface.
    • Removed calendar link generation and copying.
    • Improved calendar scrolling and layout behavior.

…o na etiqueta

Corrige o calendário cortando meses (flex-wrap quebrava o overflow:auto
interno) e destaca dia/mês/semana em selo nos cartões de compromisso.
Remove o botão de link do calendário (uso desnecessário por ora).

Unifica "+ Agendar" dentro do picker de etiqueta: marcar uma etapa do
funil que implica compromisso (reunião/follow-up/fechamento) abre o
mesmo formulário de data, com o detalhe pré-preenchido "Etapa com Fulano"
(editável). Etapas sem compromisso associado só trocam a fase na hora.
Novo estilo do menu (maior, com emoji por etapa), botão movido pro
cabeçalho da conversa no lugar do antigo "+ Agendar".
@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
metas-anomalo Ready Ready Preview, Comment Jul 13, 2026 7:37pm

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

O PR transforma o seletor de etiquetas em um fluxo de etapas com agendamento, integra-o ao cabeçalho do Thread e adiciona helpers de atividade. Também remove links do calendário e reformula os cartões de próximos compromissos.

Changes

Fluxo de etapas e agendamento

Layer / File(s) Summary
Helpers e seleção de etapas
lib/crm-tipos-atividade.ts, components/crm/Etiquetas.tsx
Etapas passam a ser normalizadas, classificadas como agendáveis e exibidas com emojis; o seletor atribui etiquetas diretamente ou inicia o agendamento.
Menu de etapas e tipos customizados
components/crm/Etiquetas.tsx
O menu lista etapas, identifica as atribuídas e permite criar ou remover tipos de atividade customizados.
Formulário de agendamento
components/crm/Etiquetas.tsx
O formulário valida data e hora, cria follow-ups, atualiza a interface e controla estados de sucesso e erro.
Integração no cabeçalho do Thread
components/crm/Thread.tsx
O seletor de etapas é integrado ao cabeçalho e o menu anterior de atribuição de atividades é removido.

Interface do calendário

Layer / File(s) Summary
Layout e remoção do link
components/crm/Calendario.tsx
A geração e cópia do link do calendário são removidas, enquanto a troca de período e o layout de rolagem são mantidos ou ajustados.
Cartão de compromisso
components/crm/Calendario.tsx
CartaoCompromisso passa a renderizar uma área de data, título, categoria, contato e indicador de conclusão sem mostrarData.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Contato
  participant EtiquetasPicker
  participant MenuEtapas
  participant FormAgendar
  participant crmActions
  Contato->>EtiquetasPicker: seleciona uma etapa
  EtiquetasPicker->>MenuEtapas: exibe etapas e tipos customizados
  MenuEtapas-->>EtiquetasPicker: retorna a etapa escolhida
  EtiquetasPicker->>FormAgendar: abre formulário quando necessário
  FormAgendar->>crmActions: cria follow-up
  crmActions-->>FormAgendar: retorna sucesso ou erro
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and matches the main CRM changes: calendar scrolling, date emphasis, and unified scheduling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/crm-calendario-agendar-unificado

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
components/crm/Calendario.tsx (1)

213-222: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Removing flexWrap fixes scrolling but drops the narrow-viewport fallback.

The minHeight:0 fix for month-cutoff/scroll is correct. However, dropping flexWrap also means the calendar column (flex:"1 1 440px", minWidth:0) and the panel (flex:"1 1 300px", minWidth:264) now stay side-by-side on one row at any width instead of stacking — on narrow viewports they'll squeeze together instead of wrapping vertically as before.

If this view is ever rendered below ~750px wide (tablet/narrow window), consider a media query or a width-based conditional to switch flexDirection to column there, since the fixed panel minWidth:264 will otherwise force the calendar column below its usable width.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/crm/Calendario.tsx` around lines 213 - 222, Preserve the parent
container’s fixed-height scrolling behavior while restoring the narrow-viewport
layout fallback. Update the calendar/panel wrapper around the visible flex
container to switch flexDirection to column below roughly 750px, using the
project’s existing responsive/media-query pattern if available; keep row layout
and the current flex/minHeight settings for wider viewports.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/crm/Calendario.tsx`:
- Around line 477-554: Update CartaoCompromisso to use a single explicit
timezone, such as America/Sao_Paulo, for hora and diaSemana formatting, and
derive the displayed day with the same timezone-aware approach instead of
quando.getDate(). Keep the existing pt-BR formatting and card layout unchanged,
ensuring SSR and hydration produce identical values.

In `@components/crm/Etiquetas.tsx`:
- Around line 388-406: Update salvar to guard against re-entry while
criarFollowUpAction is in flight: return immediately when the existing pending
state indicates a submission is active, and set that state before awaiting the
action. Clear pending after the request completes, preserving the current
success and error handling so duplicate follow-ups cannot be created by repeated
clicks.

---

Nitpick comments:
In `@components/crm/Calendario.tsx`:
- Around line 213-222: Preserve the parent container’s fixed-height scrolling
behavior while restoring the narrow-viewport layout fallback. Update the
calendar/panel wrapper around the visible flex container to switch flexDirection
to column below roughly 750px, using the project’s existing
responsive/media-query pattern if available; keep row layout and the current
flex/minHeight settings for wider viewports.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 66340aac-8ff8-418e-b4ee-502867c445af

📥 Commits

Reviewing files that changed from the base of the PR and between 68e4fff and ecc535b.

📒 Files selected for processing (4)
  • components/crm/Calendario.tsx
  • components/crm/Etiquetas.tsx
  • components/crm/Thread.tsx
  • lib/crm-tipos-atividade.ts

Comment on lines +477 to +554
/** Cartão clicável de um compromisso (na lista do período e nos próximos) —
* a data vem em destaque num "selo" à esquerda (dia grande + mês/semana),
* com o nome/objetivo do compromisso ao lado. */
function CartaoCompromisso({
atividade,
onAbrir,
mostrarData,
}: {
atividade: CrmAtividadeRow
onAbrir: () => void
mostrarData?: boolean
}) {
const nome = atividade.lead_nome || atividade.lead_telefone || "Lead sem nome"
const quando = atividade.agendado_para ? new Date(atividade.agendado_para) : null
const hora = quando
? quando.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })
: ""
const dataCurta = quando
? quando.toLocaleDateString("pt-BR", { day: "2-digit", month: "short" })
const diaSemana = quando
? quando.toLocaleDateString("pt-BR", { weekday: "short" }).replace(".", "")
: ""

return (
<button
type="button"
onClick={onAbrir}
className="w-full"
className="w-full flex items-stretch"
style={{
textAlign: "left",
display: "block",
padding: 10,
borderRadius: 9,
gap: 10,
padding: 8,
borderRadius: 10,
background: "var(--surface-2, rgba(255,255,255,0.04))",
border: `1px solid ${ACCENT}2b`,
borderLeft: `3px solid ${ACCENT}`,
opacity: atividade.concluido_em ? 0.55 : 1,
}}
>
<div className="flex items-center justify-between gap-2">
<p style={{ fontSize: 12, fontWeight: 600 }} className="truncate">
{atividade.categoria || atividade.titulo || "Follow-up"}
</p>
<span style={{ fontSize: 10, color: ACCENT, flexShrink: 0 }}>
{mostrarData ? `${dataCurta} · ${hora}` : hora}
<div
style={{
flexShrink: 0,
width: 44,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
borderRadius: 8,
background: `${ACCENT}22`,
border: `1px solid ${ACCENT}55`,
color: ACCENT,
padding: "4px 2px",
}}
>
<span style={{ fontSize: 9, textTransform: "uppercase", fontWeight: 700, lineHeight: 1 }}>
{diaSemana}
</span>
<span style={{ fontSize: 19, fontWeight: 800, lineHeight: 1.3 }}>
{quando ? String(quando.getDate()).padStart(2, "0") : "--"}
</span>
<span style={{ fontSize: 9, fontWeight: 700, lineHeight: 1 }}>
{quando ? MESES_CURTO[quando.getMonth()] : ""}
</span>
</div>
<p style={{ fontSize: 11, color: "var(--text-3)", marginTop: 2 }} className="truncate">
{nome}
{atividade.lead_empresa_nome ? ` · ${atividade.lead_empresa_nome}` : ""}
</p>
{atividade.concluido_em && (
<span style={{ fontSize: 9, color: "var(--success)" }}>✓ concluído</span>
)}
<div style={{ flex: 1, minWidth: 0 }}>
<div className="flex items-center justify-between gap-2">
<p style={{ fontSize: 13, fontWeight: 700 }} className="truncate">
{atividade.categoria || atividade.titulo || "Follow-up"}
</p>
{hora && (
<span style={{ fontSize: 11, color: ACCENT, flexShrink: 0, fontWeight: 600 }}>
{hora}
</span>
)}
</div>
<p style={{ fontSize: 11.5, color: "var(--text-2, #ddd)", marginTop: 2 }} className="truncate">
{nome}
{atividade.lead_empresa_nome ? ` · ${atividade.lead_empresa_nome}` : ""}
</p>
{atividade.concluido_em && (
<span style={{ fontSize: 9, color: "var(--success)" }}>✓ concluído</span>
)}
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Badge/card refactor looks solid; two toLocaleDateString/toLocaleTimeString calls risk hydration mismatch.

The new left "selo" badge design (weekday/day/month) and right content layout are clean and match the PR goal of surfacing day/month/week on the cards. One concern flagged by static analysis: quando.toLocaleTimeString("pt-BR", ...) (line 490) and quando.toLocaleDateString("pt-BR", { weekday: "short" }) (line 493) run during SSR and hydration without an explicit timeZone. Locale is pinned to pt-BR, but the server's timezone (commonly UTC on most hosts) can still differ from the browser's, producing a different hour/weekday string (and even a different quando.getDate() at line 530, which uses the same implicit local timezone) between server-render and hydration.

Passing an explicit timeZone (e.g. "America/Sao_Paulo") to these calls would make server and client output deterministic and avoid the hydration warning/re-render.

🕒 Proposed fix
   const hora = quando
-    ? quando.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })
+    ? quando.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit", timeZone: "America/Sao_Paulo" })
     : ""
   const diaSemana = quando
-    ? quando.toLocaleDateString("pt-BR", { weekday: "short" }).replace(".", "")
+    ? quando.toLocaleDateString("pt-BR", { weekday: "short", timeZone: "America/Sao_Paulo" }).replace(".", "")
     : ""

As per static analysis hints, no-locale-format-in-render flags exactly this pattern on lines 490 and 493.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** Cartão clicável de um compromisso (na lista do período e nos próximos)
* a data vem em destaque num "selo" à esquerda (dia grande + mês/semana),
* com o nome/objetivo do compromisso ao lado. */
function CartaoCompromisso({
atividade,
onAbrir,
mostrarData,
}: {
atividade: CrmAtividadeRow
onAbrir: () => void
mostrarData?: boolean
}) {
const nome = atividade.lead_nome || atividade.lead_telefone || "Lead sem nome"
const quando = atividade.agendado_para ? new Date(atividade.agendado_para) : null
const hora = quando
? quando.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })
: ""
const dataCurta = quando
? quando.toLocaleDateString("pt-BR", { day: "2-digit", month: "short" })
const diaSemana = quando
? quando.toLocaleDateString("pt-BR", { weekday: "short" }).replace(".", "")
: ""
return (
<button
type="button"
onClick={onAbrir}
className="w-full"
className="w-full flex items-stretch"
style={{
textAlign: "left",
display: "block",
padding: 10,
borderRadius: 9,
gap: 10,
padding: 8,
borderRadius: 10,
background: "var(--surface-2, rgba(255,255,255,0.04))",
border: `1px solid ${ACCENT}2b`,
borderLeft: `3px solid ${ACCENT}`,
opacity: atividade.concluido_em ? 0.55 : 1,
}}
>
<div className="flex items-center justify-between gap-2">
<p style={{ fontSize: 12, fontWeight: 600 }} className="truncate">
{atividade.categoria || atividade.titulo || "Follow-up"}
</p>
<span style={{ fontSize: 10, color: ACCENT, flexShrink: 0 }}>
{mostrarData ? `${dataCurta} · ${hora}` : hora}
<div
style={{
flexShrink: 0,
width: 44,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
borderRadius: 8,
background: `${ACCENT}22`,
border: `1px solid ${ACCENT}55`,
color: ACCENT,
padding: "4px 2px",
}}
>
<span style={{ fontSize: 9, textTransform: "uppercase", fontWeight: 700, lineHeight: 1 }}>
{diaSemana}
</span>
<span style={{ fontSize: 19, fontWeight: 800, lineHeight: 1.3 }}>
{quando ? String(quando.getDate()).padStart(2, "0") : "--"}
</span>
<span style={{ fontSize: 9, fontWeight: 700, lineHeight: 1 }}>
{quando ? MESES_CURTO[quando.getMonth()] : ""}
</span>
</div>
<p style={{ fontSize: 11, color: "var(--text-3)", marginTop: 2 }} className="truncate">
{nome}
{atividade.lead_empresa_nome ? ` · ${atividade.lead_empresa_nome}` : ""}
</p>
{atividade.concluido_em && (
<span style={{ fontSize: 9, color: "var(--success)" }}>✓ concluído</span>
)}
<div style={{ flex: 1, minWidth: 0 }}>
<div className="flex items-center justify-between gap-2">
<p style={{ fontSize: 13, fontWeight: 700 }} className="truncate">
{atividade.categoria || atividade.titulo || "Follow-up"}
</p>
{hora && (
<span style={{ fontSize: 11, color: ACCENT, flexShrink: 0, fontWeight: 600 }}>
{hora}
</span>
)}
</div>
<p style={{ fontSize: 11.5, color: "var(--text-2, #ddd)", marginTop: 2 }} className="truncate">
{nome}
{atividade.lead_empresa_nome ? ` · ${atividade.lead_empresa_nome}` : ""}
</p>
{atividade.concluido_em && (
<span style={{ fontSize: 9, color: "var(--success)" }}>✓ concluído</span>
)}
</div>
const nome = atividade.lead_nome || atividade.lead_telefone || "Lead sem nome"
const quando = atividade.agendado_para ? new Date(atividade.agendado_para) : null
const hora = quando
? quando.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit", timeZone: "America/Sao_Paulo" })
: ""
const diaSemana = quando
? quando.toLocaleDateString("pt-BR", { weekday: "short", timeZone: "America/Sao_Paulo" }).replace(".", "")
: ""
🧰 Tools
🪛 React Doctor (0.7.6)

[error] 490-490: This can cause a hydration mismatch because toLocaleTimeString() formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.

Format locale/timezone-dependent values in a post-mount useEffect + state, or pass an explicit locale and timeZone so the server and the browser render the same text. Only runs on SSR-capable projects.

(no-locale-format-in-render)


[error] 493-493: This can cause a hydration mismatch because toLocaleDateString() formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.

Format locale/timezone-dependent values in a post-mount useEffect + state, or pass an explicit locale and timeZone so the server and the browser render the same text. Only runs on SSR-capable projects.

(no-locale-format-in-render)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/crm/Calendario.tsx` around lines 477 - 554, Update
CartaoCompromisso to use a single explicit timezone, such as America/Sao_Paulo,
for hora and diaSemana formatting, and derive the displayed day with the same
timezone-aware approach instead of quando.getDate(). Keep the existing pt-BR
formatting and card layout unchanged, ensuring SSR and hydration produce
identical values.

Source: Linters/SAST tools

Comment on lines +388 to +406
async function salvar() {
if (!quando) {
setStatus("Escolha data e hora.")
return
}
const fd = new FormData()
fd.set("lead_id", leadId)
fd.set("categoria", categoria)
fd.set("titulo", titulo)
fd.set("agendado_para", quando)
const r = await criarFollowUpAction(fd)
if (r.ok) {
setStatus("Marcado ✓")
router.refresh()
setTimeout(onClose, 700)
} else {
setStatus(r.erro ?? "Erro")
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== File list =="
git ls-files | rg '^(components/crm/Etiquetas\.tsx|.*follow.*up.*|.*crm_atividades.*|.*atividade.*)$' || true

echo
echo "== Outline of components/crm/Etiquetas.tsx =="
ast-grep outline components/crm/Etiquetas.tsx --view expanded || true

echo
echo "== Relevant lines in components/crm/Etiquetas.tsx =="
sed -n '360,470p' components/crm/Etiquetas.tsx

echo
echo "== Search for criarFollowUpAction =="
rg -n "criarFollowUpAction|crm_atividades|agendado_para|follow.?up" . --glob '!**/node_modules/**' --glob '!**/.next/**' || true

Repository: sejaanomalo-web/metas-anomalo

Length of output: 10931


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== lib/crm-atividades-actions.ts (create follow-up path) =="
sed -n '132,190p' lib/crm-atividades-actions.ts

echo
echo "== Table definition for public.crm_atividades =="
sed -n '102,130p' supabase/migrations/20260624_crm_fase0.sql

echo
echo "== Any uniqueness or dedup constraints on crm_atividades =="
rg -n "unique|UNIQUE|dedup|duplicate|on conflict|conflict" supabase/migrations lib/crm-atividades-actions.ts components/crm/Etiquetas.tsx

Repository: sejaanomalo-web/metas-anomalo

Length of output: 7893


🌐 Web query:

React 18.3 useTransition startTransition async callback pending await semantics

💡 Result:

In React 18.3, the startTransition function (and the startTransition returned by useTransition) operates synchronously [1][2][3]. When you pass a callback to it, React executes that callback immediately to mark any state updates triggered during its synchronous execution as transitions [1][2][4]. Regarding async callbacks and await semantics: 1. Scope Limitation: The "transition" marking is strictly scoped to the synchronous portion of the callback execution [2][5]. If you await a promise, the execution context of the callback effectively ends for the purposes of marking state updates [2]. Any state updates scheduled after an await are not automatically marked as transitions [1][4][5]. 2. Requirement for Nested Wrapping: Because of the synchronous nature of the marking, any state update that occurs after an await within the callback must be wrapped in an additional startTransition call if you wish for that specific update to be treated as a transition [1][4][6]. Failure to do so will result in the update being treated as a standard, urgent (non-transition) update [2][5]. 3. useTransition Pending State: While useTransition allows you to pass an async function, the isPending state reflects the duration of the initial synchronous execution and any immediate effects; it does not automatically track the entire lifetime of an asynchronous operation across await boundaries [7][4]. Developers are responsible for managing the transition status manually across async steps using nested startTransition calls to ensure that subsequent updates remain grouped correctly [1][4][6]. These semantics exist because React currently lacks an Async Context API to implicitly associate state updates with an asynchronous call stack across ticks [7]. This behavior is a known limitation in the React 18 release line [1][4].

Citations:


Guard salvar against re-entry. pending drops before the awaited insert finishes, so the button can be clicked again while the request is still in flight. Since criarFollowUpAction inserts unconditionally and there’s no uniqueness guard, a double click can create duplicate follow-ups.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/crm/Etiquetas.tsx` around lines 388 - 406, Update salvar to guard
against re-entry while criarFollowUpAction is in flight: return immediately when
the existing pending state indicates a submission is active, and set that state
before awaiting the action. Clear pending after the request completes,
preserving the current success and error handling so duplicate follow-ups cannot
be created by repeated clicks.

@sejaanomalo-web
sejaanomalo-web merged commit 80a4e61 into main Jul 13, 2026
3 checks passed
@sejaanomalo-web
sejaanomalo-web deleted the claude/crm-calendario-agendar-unificado branch July 13, 2026 19:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant