Commit Graph

169 Commits

Author SHA1 Message Date
Arnas Donauskas
427b4612da
Merge pull request #310 from bellopushon/feat/automation-deal-selectors
feat(automations): add deal pipeline selectors
2026-07-01 11:46:13 +03:00
Arnas Donauskas
f4beb6f368
Merge pull request #322 from RiadDeveloper/main
Prevent redundant profile fetches on refresh
2026-07-01 11:27:20 +03:00
Arnas Donauskas
6ff44644c6
Merge pull request #279 from AlexTPaul/main
feat: Notifications
2026-07-01 11:24:36 +03:00
Arnas Donauskas
7baeb94003 fix(settings): preserve WhatsApp credential drafts on tab refocus
The WhatsApp settings load effect re-ran fetchConfig() whenever `user`
or `profileLoading` changed. Supabase's onAuthStateChange fires a token
refresh (new `user` object, profileLoading toggles) when the browser
tab regains focus, so returning to the tab re-hydrated the form and
overwrote any unsaved credential input.

Guard the effect with a ref tracking the account id already loaded, so
it only fetches once per account and ignores auth-state churn that
doesn't actually change the account. Explicit reloads after save,
registration verification, and reset still call fetchConfig directly.

Closes #307

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 11:16:54 +03:00
Riad Developer
1fce87c4c6
Prevent redundant profile fetches on refresh
Replace `user` object with `user?.id` in dependency arrays to prevent unnecessary re-renders. Add `lastFetchedUserIdRef` to track the user ID for successfully initiated profile fetches, preventing redundant API calls when the window refocuses or the auth token is refreshed.
2026-06-30 13:01:09 -07:00
AlexTPaul
cd863cf9ee feat(notifications): implement account-based notification retrieval and update logic 2026-06-30 11:16:20 +05:30
Alex Titus Paul
cf842fcee2
Merge branch 'ArnasDon:main' into main 2026-06-30 10:44:07 +05:30
Arnas Donauskas
65b677181e
Merge pull request #320 from ArnasDon/fix/template-picker-account-scope
fix(templates): scope template picker to account, not user
2026-06-29 17:35:51 +03:00
Arnas Donauskas
3110932d78
fix(templates): scope template picker to account, not user
The inbox/contact TemplatePicker fetched approved templates filtered by
`user_id = current user`, so in a shared account a teammate only saw
templates they personally created — others' approved templates were
hidden, leaving them unable to send those templates.

message_templates is account-owned and its RLS SELECT policy already
scopes rows to account members (is_account_member, migration 017), so
the user_id filter was both redundant and over-restrictive. Drop it and
rely on RLS, matching the broadcast template picker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 17:33:41 +03:00
Arnas Donauskas
19e35accb6
feat(contacts): send an approved template to a single contact (#296)
Adds a "Send template" action to the contact detail view so the
business can initiate (or re-open) a WhatsApp conversation with one
known contact by sending an approved template — without the awkward
single-recipient broadcast workaround.

The /api/whatsapp/send route now accepts `contact_id` as an
alternative to `conversation_id`: it verifies the contact is in the
caller's account, find-or-creates the contact's conversation
server-side, then runs the existing send path (phone-variant retry,
template send-builder, message + conversation persistence, flow
pause). The contact view reuses the existing inbox TemplatePicker for
template selection + variable/header/button input, keeping Meta
compliance unchanged (approved templates only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 17:11:02 +03:00
Arnas Donauskas
ce0d04ba80
feat(flows): group the add-step menu by node category
The canvas and list add-node menus both listed all 10 node types in
one flat dropdown. As the type list grows — especially for forks
adding their own node types (see #312) — a flat list gets hard to
scan. Bucket the types into Messaging / Logic & data / Flow control,
driven by a new `category` field on NODE_META so a new type can't go
uncategorized, and a `groupNodeTypesByCategory` helper shared by both
menus so they stay in lockstep.

No triggers/events category: in wacrm a flow is triggered by
flow-level config (trigger_type), not a canvas node, so `start` lives
under Flow control.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 15:21:07 +03:00
bellopushon
377c1b738a feat(automations): add deal pipeline selectors 2026-06-26 15:57:06 -04:00
Arnas Donauskas
1f4d1a3a11
Redesign flow editor UI to match Flow Builder design
Reworks the flow editor (/flows/[id]) to the Flow Builder design
handoff, adapted to wacrm's existing tokens, components, and
light+dark theming. Keeps all behaviour — React Flow canvas,
drag-to-connect, typed config forms, validation, save/activate/delete,
per-browser view persistence, mobile-forces-list.

- shared.tsx: per-node-type color system (nodeColors) + one-line
  blurbs, and a shared NodeIconChip component. Label color blends
  toward --foreground via color-mix so it stays readable in both modes.
- header.tsx: compact single-row toolbar (back / icon / inline name /
  status chip / edited dot · Runs / Delete / Activate / Save) plus a
  subtle inline description note.
- flow-editor-shell.tsx: app-like full-height layout (toolbar → mode
  row → stage → validation bar) with a segmented Canvas/List control
  and a complete node-type legend derived from NODE_META.
- flow-canvas.tsx: per-type colored node cards, branch ports, dot-grid
  background, rounded controls/minimap, top-left primary Add-node menu,
  colored inspector header.
- flow-builder.tsx: list rows tied to the same per-type color language.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 16:53:03 +03:00
Arnas Donauskas
33942dc8ac
fix(broadcasts): collect header media URL for image/video templates (#298)
Templates with an IMAGE/VIDEO/DOCUMENT header could not be broadcast:
the wizard never offered a place to provide the header media URL, so
the send either shipped the template's stale stored URL or — when none
was stored — threw "image header requires a media link or id at send
time", failing the whole broadcast. The send path already supported
`messageParams.headerMediaUrl`; only the UI/hook never collected it.

- step3-personalize: show a required "Header media" URL field (with
  http(s) validation and an image preview) when the selected template
  has a media header; seed it from the template's stored sample URL and
  block Next until a valid URL is provided.
- new-broadcast page: thread headerMediaUrl through wizard state.
- use-broadcast-sending: pass it as messageParams.headerMediaUrl to the
  broadcast API for media-header templates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 17:17:18 +03:00
Arnas Donauskas
456321a2f2
feat(api): public API groundwork — scoped API keys + /api/v1/me
Adds the machine-to-machine auth layer the public REST API needs so
external scripts and automations can drive wacrm (issue #245). This is
the groundwork release; data endpoints land one at a time on top of it.

- Migration 026: account-scoped `api_keys` table. SHA-256 hash at rest
  (plaintext shown once), display `key_prefix`, `scopes[]`, RLS via
  `is_account_member` (members read, admin+ manage).
- `src/lib/api-keys/`: key gen/hash (wacrm_live_ prefix, timing-safe),
  scope vocabulary, service-role auth-path store.
- `requireApiKey(request, scope?)` (src/lib/auth/api-context.ts):
  bearer parse -> hash lookup -> liveness -> per-key rate limit ->
  scopes-only authz. Returns a service-role-scoped account context.
- `/api/v1` envelope ({data} / {error:{code,message}}) + GET /api/v1/me
  reference endpoint that exercises the whole stack.
- Dashboard: admin+ key management routes + Settings -> API keys tab.
- Docs (docs/public-api.md), CHANGELOG, README, .env note.

Checks: typecheck clean, lint 0 errors, full suite green (+27 tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 15:35:43 +03:00
Arnas Donauskas
054c9e4e47
feat(team): member presence (online / away / offline) on the roster
Adds a live presence layer so the Settings → Team members roster and the
inbox Assign dropdown show who is actively using the dashboard, idle, or
gone. Implements #269.

How it works
- New `member_presence` table + `touch_presence` SECURITY DEFINER RPC
  (migration 024). Account-scoped SELECT RLS mirrors the read-only
  roster; the RPC derives account_id server-side, never client-supplied.
- The active client heartbeats 'online'/'away' every ~30s; 'offline' is
  derived by viewers from staleness, so a closed tab resolves to offline
  without an unreliable unload write.
- Pure, unit-tested derivation/formatting in src/lib/presence.ts.
- `usePresence` reads the table, subscribes to Realtime (subscribe-then-
  merge so events landing mid-fetch aren't clobbered), and re-derives
  offline on a 15s tick.
- Roster: avatar badge + tooltip + "N online · N away · N offline"
  summary, with an aria-label so AT/keyboard users get presence too.
  Inbox Assign dropdown: matching dot before each teammate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 14:37:29 +03:00
AlexTPaul
ab6a511d31 feat: Notifications 2026-06-22 13:12:28 +05:30
Arnas Donauskas
ca3aa29c3a
fix(automations): persist default match_type for Keyword Match triggers
Creating a keyword_match automation with Match type left on the default
"Contains" failed activation with `match type must be "exact" or
"contains"` (path trigger.match_type). The dropdown showed "Contains"
via a display-only fallback, but the value was never written to the
config, so the saved payload omitted match_type entirely.

Both engines already treat a missing match_type as "contains"
(automations/engine.ts, flows/engine.ts), so the activation validator
was stricter than runtime. Two coordinated fixes:

- validate.ts: accept a missing match_type (defaults to "contains");
  only an explicit, unrecognised value is rejected. Aligns the
  validator with the engines and any non-UI client.
- automation-builder.tsx: seed match_type: "contains" on mount of
  KeywordMatchConfig so the displayed default is actually persisted,
  keeping the saved record explicit and self-describing.

Adds a regression test that a keyword_match config with no match_type
passes activation validation.

Fixes #271

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 10:20:38 +03:00
Arnas Donauskas
9b32954d1a
feat(inbox): collapsible contact sidebar on desktop
Add a desktop-only toggle in the message-thread header to show/hide the
contact sidebar (tags, deals, notes). On smaller laptops the always-on
panel crowds the message thread; agents who only want to read and reply
can now reclaim that horizontal space.

The page owns the open/closed state and conditionally renders the
sidebar; the thread renders the toggle button and reflects the state.
The choice is persisted to localStorage (device-scoped, like the theme
prefs) so it survives reloads. To avoid a hydration mismatch we default
to open on the server and reconcile from storage in an effect after
mount. Mobile is unchanged — the sidebar is never a permanent panel
there and the toggle is hidden below lg.

Closes #258

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 14:06:02 +03:00
Arnas Donauskas
1de99be73a
fix(settings): full-bleed layout so the page fills wide screens
The Settings page was the only dashboard route capped at
max-w-[1040px] mx-auto, so it floated centered with large empty
gutters on wide monitors while every other page is full-bleed.

Remove the cap so the page fills the available width like the rest of
the app, and spend the space where it helps readability:
- Overview status tiles go 3-up on xl (from 2-up).
- Templates list goes 2-up on xl.
- Form-heavy panels (Profile, Login & security, Deals) keep a readable
  max-w-2xl; Fields & tags and Appearance use max-w-3xl — so inputs
  don't stretch edge-to-edge on large displays.
- Grid uses minmax(0,1fr) to avoid blowout from long content.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 15:30:41 +03:00
Arnas Donauskas
a45cbe6822
feat(settings): redesign Settings into a grouped rail + Overview landing
Replace the flat 8-tab Settings page with a cleaner information
architecture: a grouped left rail (Account / Workspace) plus a new
Overview landing with live status tiles. Related concerns are merged —
"Login & security" (password + sessions) and "Fields & tags" (tags +
custom fields).

- New shell: settings-sections (section model + legacy ?tab= mapping,
  tags/custom-fields -> fields), settings-rail, settings-overview
  (live counts + identity), security-panel, fields-and-tags-panel,
  settings-panel-head, settings-chip, role-meta (shared role chip
  metadata, de-duped from members-tab).
- Restyle the light panels to the mockup; reframe the heavy panels
  (WhatsApp, Templates) with the shared panel header, internals intact.
- Tokens: add --card-2 (light+dark) and @theme mappings for
  primary-hover/soft/soft-2 so the redesign stays fully tokenized.
- ?tab= remains the URL source of truth, keeping existing sidebar/header
  deep-links working.

Overview decouples the slow WhatsApp Meta health-ping from the cheap
count queries so a flaky ping never blanks the whole landing; template
counts use head-only count queries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 15:08:33 +03:00
Arnas Donauskas
4d23afc563
feat(templates): upload image for template headers; fix image-URL submit (#230)
Creating a template with an image header failed because the code sent
`example.header_url` — Meta only accepts a Resumable-Upload media handle
(`example.header_handle`) at creation time, never a plain URL.

- Resumable Upload: new `uploadResumableMedia` (meta-api) implements the
  two-step `/{app_id}/uploads` flow → media handle. Needs META_APP_ID.
- Handle derivation: `ensureImageHeaderHandle` fetches the header image
  (uploaded file OR pasted URL) and turns it into a handle before submit.
  Wired into both the submit and edit (PATCH) routes with actionable 400s
  (missing META_APP_ID, unreachable URL, wrong type, >5 MB). This fixes
  BOTH the new upload path and the previously-broken manual-URL path.
- Template builder: add an image file upload (account-scoped chat-media
  bucket via uploadAccountMedia) with preview + JPEG/PNG + 5 MB validation,
  alongside the URL field.
- Send-time fix: an approved image template now sends the public
  header_media_url as `link`; the resumable header_handle is NO LONGER
  used as a send media id (it's invalid for sends and would fail).
- META_APP_ID documented as required for image-header templates.

Scope: image headers (the issue). Helper is generic for video/document.
typecheck/lint/build green; 421 tests (10 new).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 14:00:54 +03:00
Arnas Donauskas
ac9fc8dfc9
feat(inbox): graduate chat media send to GA (remove beta flag)
Photo/video/document/voice send has been validated in production, so it's
now available to every account. Removes the 'chat_media' beta gate:

- Composer always shows the attach menu + voice recorder (no
  useBetaFeature check).
- Send route no longer 403s media kinds for non-opted-in accounts.
- Deletes the now-unused beta-features helper, the useBetaFeature hook,
  and their test (added solely for this rollout; no other consumer).

The profiles.beta_features column (migration 011) is left in place for
future betas. typecheck/lint/build green; 411 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 11:38:54 +03:00
Arnas Donauskas
e855e6677f
Merge pull request #262 from ArnasDon/fix/chat-media-client-recording
refactor(inbox): record voice notes client-side, drop server ffmpeg
2026-06-18 11:29:10 +03:00
Arnas Donauskas
4fa29b7a45
refactor(inbox): record voice notes client-side, drop server ffmpeg
Follow-up to #259. Voice notes are encoded to Ogg/Opus in the browser via
opus-recorder (small WASM encoder, lazy-loaded on record), so there's no
server-side transcode and nothing to provision on the host. The server
stores the .ogg like any other attachment; WhatsApp renders it as a voice
note.

- Composer: MediaRecorder + /api/whatsapp/transcode-audio replaced with
  opus-recorder. Encoder worker vendored at public/opus/encoderWorker.min.js
  (no install-time download to fail), ignored by eslint + prettier.
- Removed the transcode route, the audio-transcode lib + tests, the
  ffmpeg-static dep + ambient types, the FFMPEG_PATH env doc, and the
  unused transcodeAudio rate limit.
- Added opus-recorder ambient types.

Still behind the 'chat_media' beta flag with per-kind size caps.
typecheck/lint/build green; 414 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 11:27:12 +03:00
Arnas Donauskas
07e513690f
fix(automations): remove spurious third output on condition steps
A Condition (If/Else) step rendered three add-step connectors: Yes, No,
and a trailing linear "+". ConditionBranches already renders the Yes/No
branch StepLists (each with its own AddButton), while StepRenderer also
emitted a trailing AddButton after every step — including conditions.

Skip the trailing AddButton when step_type === "condition", since a
condition branches into Yes/No and has no linear continue path. Steps
after a condition are added inside one of its branches.

Fixes #256

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 11:20:55 +03:00
Arnas Donauskas
873ba1f4c6
feat(inbox): gate media send behind 'chat_media' beta flag
Media sends (photo/video/document/voice) are now opt-in per account via
the existing profiles.beta_features mechanism (migration 011 — same
pattern used to dogfood Flows before GA). No migration needed.

- New shared `hasBetaFeature` + `CHAT_MEDIA_BETA` ('chat_media') key, and
  a `useBetaFeature` client hook mirroring useCan (false while loading).
- Composer: the attach menu, hidden file inputs, and voice recorder only
  render when the flag is on; the hint indent reverts so non-beta users
  see the unchanged text + template composer.
- Server: the send route rejects media kinds (403) and the transcode
  route rejects (403) unless the caller's profile is opted in — so the
  endpoints can't be driven ahead of the rollout. Text + templates stay
  available to everyone.

Enable for one account via Supabase Studio:
  UPDATE profiles SET beta_features = array_append(beta_features, 'chat_media')
  WHERE email = 'someone@example.com';

Tests: hasBetaFeature null-safety + membership (419 total). typecheck/lint/build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:44:02 +03:00
Arnas Donauskas
96e7a56e3e
fix(inbox): harden media send — GC orphans, per-type caps, voice cap
Addresses senior-review findings on the media-send feature:

- Storage lifecycle: uploads are GC'd on the abandonment paths
  (discarded draft, failed/errored send, unmount with a staged draft,
  and replacing one draft with another) via a new deleteAccountMedia
  helper. Previously every cancel/failure orphaned a public object.
- Per-kind size caps (image 5MB, video/audio/document 16MB) mirror
  Meta's limits and are enforced BEFORE upload, so a too-large image no
  longer uploads then fails at the Meta call.
- Voice notes auto-stop at a 5-min cap and always render as a voice note
  (transcode anything that isn't already OGG/Opus — Safari MP4 included),
  not just Chromium WebM.
- Send route rejects unknown message_type and captions over Meta's 1024
  cap up front; transcode route guards its output size too.
- DraftPreview hoisted to a stable module-scope component so the caption
  input no longer remounts (and drops focus) on each keystroke.

Tests: per-kind cap assertions added (418 total). typecheck/lint/build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:28:19 +03:00
Arnas Donauskas
312683ca05
feat(inbox): send photo, video, document & voice in chat (#213)
Agents can now attach a photo/video/document or record a voice note in
the inbox composer and send it live in a 1:1 thread — previously media
could only be received from customers or sent via the Flows builder.

- 023_chat_media.sql: new account-scoped `chat-media` Storage bucket
  (mirrors flow-media RLS from 020) plus Meta-accepted audio MIME types.
- meta-api: sendMediaMessage gains an `audio` kind (link-only — Meta
  rejects caption/filename on audio; WhatsApp renders OGG/Opus as a
  voice note).
- /api/whatsapp/send: media branch reusing the phone-variant retry loop.
- /api/whatsapp/transcode-audio: Node-runtime route remuxing the
  WebM/Opus that Chromium's MediaRecorder produces to OGG/Opus via
  ffmpeg-static (Firefox/Safari record accepted formats and skip it).
- composer: attach menu, file pickers, voice recorder + auto-transcode,
  preview with caption; thread: optimistic handleSendMedia.
- shared uploadAccountMedia helper (flows form refactored onto it).
- next.config: allow microphone=(self) (was fully blocked) + CSP
  media-src; document optional FFMPEG_PATH override.

Tests: meta-api audio payload shape, buildMediaPath, audio-transcode
helpers (14 new). typecheck/lint/build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:07:16 +03:00
Arnas Donauskas
ee8da11985
feat(contacts): allow bulk-deleting selected contacts (#239)
Add row selection to the contacts table so multiple contacts can be
deleted at once instead of one at a time.

- New Checkbox UI primitive wrapping @base-ui/react/checkbox, matching
  the switch.tsx token conventions (incl. indeterminate state).
- Header select-all + per-row checkboxes; row-checkbox clicks stop
  propagation so they don't open the detail sheet.
- Bulk action bar with selected count, Clear, and a role-gated
  "Delete selected" action.
- handleBulkDelete via .delete().in('id', ids) — RLS already restricts
  deletes to account members, so no migration/API route is needed.
- Selection is page-scoped and resets whenever the contact set reloads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 16:44:41 +03:00
Arnas Donauskas
66a5bb51d2
feat(inbox): add "Unread" filter to conversation list
Adds an "Unread" option to the Inbox conversation-list filter dropdown,
showing only conversations with unread messages (unread_count > 0).

Closes #233

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:40:06 +03:00
Arnas Donauskas
1474657d03
fix(theme): light-mode polish from review pass
Self-review of the light/dark sweep surfaced neutral colors that lived
in raw inline-style / <style jsx> / gradient values (so the className
sweep correctly skipped them) and a few contrast/affordance nits:

- reply-quote: adapt to the outbound (primary) bubble via onPrimary so
  the quote/author stay legible on the violet fill in light mode; wire
  it from message-bubble.
- dashboard SVG chrome -> tokens: conversations-chart gridline/crosshair,
  pipeline-donut background ring, automation-builder dot-grid gradient.
- members-tab viewer chip: bg-card -> bg-transparent (outline) so it
  doesn't blend into a card surface in light mode.
- tag-manager / pipeline-settings color-swatch selection rings: raw
  slate/white -> var(--popover)/var(--foreground) so they flip.
- pipeline-board scrollbar (<style jsx>): slate rgb -> var tokens.
- activity-feed zebra stripe: bg-card/40 -> bg-muted/40 (survives light).
- forgot-password / import-modal: restore real hover (dead no-op hovers).
- themed-toaster: gate theme via useSyncExternalStore to avoid a
  hydration mismatch on sonner's theme attribute (no setState-in-effect).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 09:28:22 +03:00
Arnas Donauskas
4aa329b527
feat(theme): add light/dark mode toggle (#243)
Introduce a light/dark MODE axis orthogonal to the existing accent
themes. Neutral surface tokens now live in html[data-mode="light|dark"]
blocks; accent tokens (--primary*, --ring, --chart-1, --sidebar-primary*)
stay in html[data-theme] blocks, so any accent works in either mode.

- globals.css: split neutrals (mode) from accent (theme); add light
  palette; dark values unchanged (dark stays the default).
- lib/themes.ts: Mode type, DEFAULT_MODE=dark, MODE_STORAGE_KEY, isMode.
- use-theme.tsx: provider owns mode/setMode/toggleMode + cross-tab sync.
- layout.tsx: boot script applies persisted mode before first paint
  (no flash); colorScheme "dark light".
- mode-toggle.tsx (header sun/moon) + themed-toaster.tsx (mode-aware).
- appearance-panel.tsx: light/dark control + updated copy.
- Tokenize hardcoded neutral colors (slate/white/gray) -> semantic
  tokens across the app so every surface flips with mode. Flow canvas
  and message-bubble timestamps made mode-aware too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:29:46 +03:00
Abhishek Sonwal
f39ca60629 refactor(contacts): improve code readability and formatting in import modal and CSV parsing
- Reformatted code in `import-modal.tsx` for better readability, including consistent indentation and line breaks.
- Enhanced `parse-contact-csv.ts` and `parse-contact-csv.test.ts` with consistent string delimiters and improved whitespace handling.
- Updated comments for clarity and consistency across the import functionality.
- Ensured consistent use of single quotes for strings throughout the files.
2026-06-16 12:59:03 +05:30
Abhishek Sonwal
a3ce8fcf9b
Merge branch 'ArnasDon:main' into main 2026-06-16 12:17:02 +05:30
Arnas Donauskas
3b924c85bc
fix(whatsapp): allow connecting Meta test numbers without a PIN
The WhatsApp config POST handler hard-failed with a 400 whenever a
number needed registration (first save or number change) and no
6-digit two-step-verification PIN was supplied. Meta test numbers
(Developer Console) are pre-registered by Meta and expose no PIN to
set, so test-number users could never complete setup (issue #242).

The PIN-based /{phone_number_id}/register call only governs inbound
webhook routing for production numbers under a shared WABA (issue
#136) — it has nothing to do with credential validity or outbound
sends. So treat it as best-effort: when no PIN is supplied, skip
/register, save the (already Meta-verified) credentials as connected,
and leave registered_at null. The response now reports `registered`
accurately and adds `registration_skipped` so the UI can react.

UI: the save handler no longer falsely claims the number is "Live"
on a skipped registration — it points at the existing "Not
registered" banner, which already offers a PIN-retry path. The PIN
field is relabelled (optional) with copy explaining it's only for
production inbound routing and that test numbers have no PIN.

Fixes #242

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 17:01:36 +03:00
Abhishek Sonwal
b780fddb3b feat(contacts): enhance import modal with company column support and improved CSV parsing
- Updated `parseContactCsv` to include a flag for the presence of a company column in the CSV.
- Modified the import modal state management to handle the new company column, improving user experience during contact import.
- Enhanced the `ImportPreviewTags` component to conditionally render based on the presence of company data.
- Adjusted the UI to better display tag and company information in the import preview.
2026-06-15 13:27:56 +05:30
Abhishek Sonwal
73062cf06d feat(contacts): enhance import modal with tag preview and CSV parsing improvements
- Added `ImportPreviewTags` component to display tags with color coding in the import modal.
- Updated `parseContactCsv` to return an object containing both parsed rows and a flag indicating the presence of a tags column.
- Modified state management in the import modal to handle tags and their colors, improving user experience during contact import.
- Adjusted the import modal to conditionally render the tags column based on the presence of tags in the preview.
2026-06-15 13:12:00 +05:30
Abhishek Sonwal
3a416832a7 feat(contacts): enhance CSV import functionality with tag assignment
- Introduced `parseContactCsv` for improved CSV parsing, replacing the previous inline parsing logic.
- Added support for importing tags alongside contacts, allowing users to assign tags during the import process.
- Implemented `assignImportedContactTags` to handle tag assignments after contact creation.
- Updated the import result state to include the number of tags assigned and added relevant success and info notifications for users.
- Enhanced the import modal description to clarify the format for tag input in CSV files.
2026-06-15 13:00:18 +05:30
Arnas Donauskas
599b21eed0
fix(settings): add Custom Fields tab to Settings (#238)
Custom-field definitions are account-wide config — a sibling of Tags,
Templates and Deals, all of which live under Settings. But the field
catalogue could only be managed via an admin-only button on the Contacts
page, so users looking under Settings found nothing (#238).

Surface it as a "Custom Fields" tab in Settings:

- Extract the manager's body into a reusable `CustomFieldsPanel`;
  `CustomFieldsManager` keeps its dialog signature and now wraps the
  panel, so the Contacts page is unchanged. Radix unmounts dialog
  content on close, so the panel still refetches on each open.
- New `CustomFieldsSettings` renders the panel inline with a
  settings-style header.
- Tab is admin+ gated (mirrors the Contacts gate); deep-linking to it
  as a non-admin falls back to Profile. `custom_fields` RLS still
  enforces writes server-side.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:48:41 +03:00
Arnas Donauskas
d477a7a429
feat(automations): support custom contact fields in Update Contact Field action
The "Update Contact Field" automation action could only write the built-in
name/email/company columns. This wires up the existing custom_fields /
contact_custom_values tables so automations can read and update arbitrary
custom fields, and adds the missing UI to define them.

- Add a "Custom fields" manager dialog on the Contacts page (admin+ gated)
  to create / rename / delete field definitions. Inserts carry account_id.
- Load account custom fields into the automation builder's ResourcesContext
  and offer them in the Field dropdown, encoded as `custom:<id>`. A saved
  field that's since been deleted is preserved as an "(unknown field)"
  option so editing doesn't silently drop it.
- Engine: interpolate the value ({{ vars.* }} / {{ message.text }}) for all
  fields, and for `custom:<id>` verify the field belongs to the automation's
  account (defense in depth) before upserting into contact_custom_values.
- Tests: cover the custom-value upsert, variable interpolation, and
  cross-account rejection.

Closes #235

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:23:49 +03:00
Arnas Donauskas
82caecec09
fix(flows): allow multiple comma-separated keywords in trigger (#234)
The list-view keyword trigger input parsed its value into the keywords
array on every keystroke (split/trim/filter(Boolean)) and re-rendered
from keywords.join(", "). Typing a comma produced a trailing empty
segment that filter(Boolean) dropped, so the comma was stripped the
instant it was typed — making it impossible to enter a second keyword.

Extract a KeywordsInput component that holds a local draft string and
only commits (parse + dedupe-empties + rejoin) on blur / Enter, mirroring
the automations builder's KeywordMatchConfig. Multi-keyword entry like
"support, help, hi" now works.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:16:16 +03:00
Arnas Donauskas
9277746161
fix(inbox): let the conversation list scroll instead of clipping
The conversation/contact list on the Inbox renders inside a flex
column with a fixed height, with the list in a `<ScrollArea
className="flex-1">`. A flex child defaults to `min-height: auto`, so
the ScrollArea grew to fit every conversation rather than shrinking to
the remaining space. The list then overflowed its container and was
clipped by the parent's `overflow-hidden`, with no vertical scrollbar —
so contacts past the fold were unreachable (issue #229).

Add `min-h-0` to the ScrollArea so it can shrink below its content and
the viewport scrolls.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:26:29 +03:00
Arnas Donauskas
3dce1b56cd
feat(contacts): prevent duplicate phone numbers on create/import (#212)
The same phone could become multiple contacts: only the WhatsApp
webhook de-duped, while manual create and CSV import inserted raw
un-normalized phones, and the DB had no uniqueness. That fragmented
conversations, deals, and tags across duplicates.

- Migration 022: generated `contacts.phone_normalized` (digits-only),
  a one-shot `merge_duplicate_contacts()` that collapses existing
  dupes into the oldest row (re-pointing conversations, deals, notes,
  tags, custom values, broadcast recipients — no data loss), then a
  UNIQUE (account_id, phone_normalized) index as the authoritative
  guarantee for every write path.
- Shared `src/lib/contacts/dedupe.ts`: findExistingContact (suffix
  prefilter + phonesMatch), isExactMatch, isUniqueViolation,
  dedupeByPhone. The webhook now reuses findExistingContact (and
  recovers from a unique-violation race instead of dropping the
  message).
- Contact form: on-blur duplicate check — hard-block + "view existing"
  link on an exact match, soft warning on a trunk-variant near-match;
  unique-violation backstop on submit.
- CSV import: de-dupes within the file and against existing contacts,
  reports "imported / skipped / failed", treats 23505 as skipped.
- Contact type gains account_id (was missing) + phone_normalized.
- Unit tests for the dedupe helpers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 15:52:38 +03:00
Arnas Donauskas
3abecb433b
feat(deals): make default deal currency configurable per account (#218)
wacrm hardcoded USD across every deal-value formatter, the new-deal
form, and automation-created deals, making deal tracking unhelpful for
non-US self-hosted users. Add a per-account default currency.

- Migration 021: accounts.default_currency TEXT NOT NULL DEFAULT 'USD'
  with a 3-letter-code CHECK. The existing admin-only accounts_update
  RLS policy already gates writes.
- New src/lib/currency.ts: single source of truth (CURRENCIES list,
  formatCurrency, formatCurrencyShort), replacing ~6 duplicated
  USD-hardcoded helpers.
- useAuth exposes defaultCurrency (joined from accounts), falling back
  to USD while loading / on pre-021 schemas.
- Settings → Deals tab to pick the currency (admin-only, read-only for
  others to match RLS).
- Deal form defaults new deals to the account currency and sources its
  dropdown from CURRENCIES; existing deals keep their saved currency.
- Pipeline board/analytics, dashboard "Open Deals Value", and the
  pipeline donut format totals in the account currency. Individual
  deal cards still show each deal's own currency.
- Automation create_deal sets the new deal's currency from the
  account default instead of the static DB default.

One currency per account (no FX conversion) per the issue's
recommended first pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:59:48 +03:00
Arnas Donauskas
efd72b131e
fix(automations): pick tags, agents & templates by name in the builder
The automation builder exposed several developer-facing fields that
made automations hard to configure from the UI (issue #216):

- Keyword Match parsed/trimmed on every keystroke, so typing a
  comma-separated, multi-word list like "SEO, search engine
  optimization" was impossible — the comma and trailing space were
  stripped immediately.
- Tag Added / Add Tag / Remove Tag asked for a raw tag_id.
- Assign Conversation → Specific agent asked for a raw agent_id.
- Send Template asked for a manually typed template name + language.

Changes:
- Keyword input keeps a local draft and only parses into the keywords
  array on blur / Enter, then re-displays the cleaned form.
- New tag / agent / template pickers select existing resources by name
  while still storing ids / names internally, so the stored config
  shape (and the engine/validation) is unchanged. Resources load once
  at the builder root and are shared via context.
- Tags & approved templates load from Supabase (RLS-scoped, matching
  the broadcast picker); members load from /api/account/members.
- Every picker falls back to its original raw input when its list is
  empty (fresh account / older deployment), so automations stay
  authorable. Saved-but-missing ids are preserved when editing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:21:44 +03:00
Arnas Donauskas
165bdd3335
fix(pipelines): keep horizontal scrollbar visible on desktop boards
The pipeline board uses overflow-x-auto but hid the scrollbar on fine
pointers, assuming the full board always fits on lg+. That assumption
breaks once a pipeline has many stages: columns keep a 260px min-width,
so the row overflows the viewport and the rightmost stage gets clipped
with no scrollbar or affordance to reveal it (issue #214).

Scope the scrollbar-hiding to touch devices (where the snap/peek layout
already hints at more content) and show a thin, themed horizontal
scrollbar on desktop so the overflow is discoverable and usable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:08:27 +03:00
Arnas Donauskas
c5ee04ce9e
fix(account-sharing): stamp account_id on account-scoped inserts (#205)
Migration 017 made domain tables account-scoped: account_id is NOT NULL
and INSERT RLS is WITH CHECK (is_account_member(account_id, ...)). Several
client- and server-side create flows still inserted only user_id, so on a
fresh install they failed with 403 / "new row violates row-level security
policy" / not-null violations.

Resolve account_id from useAuth() (client) or the caller's profile (server)
and include it on every account-scoped insert:

- contacts: contact-form, import-modal, broadcast CSV upsert
- contact_notes: contact-sidebar, contact-detail-view
- tags: tag-manager
- broadcasts: broadcasts/new page, use-broadcast-sending
- deals: deal-form
- automations: POST /api/automations, POST /api/automations/[id]/duplicate

Each path also guards on a missing accountId with a clear message instead
of letting the insert fail opaquely.

pipelines/page.tsx and api/flows/route.ts are intentionally left to the
already-open PR #193, which fixes those same paths (and adds tests).

Verified: a full sweep of every account-scoped insert/upsert across src/
now includes account_id (except the two paths owned by #193). typecheck,
lint (0 errors), and 370 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 10:20:54 +03:00
Arnas Donauskas
5332f77831
feat(members): GA the Members tab — remove account_sharing beta gate
The multi-user account-sharing surface (Members tab + invite/role
management APIs + /join redeem flow) is complete, so drop the
`account_sharing` beta flag that hid it during development. Mirrors
the Flows soft-GA in 0.2.0.

- settings: Members tab is now always rendered (no beta_features
  check, no stale-link fallback). The `profiles.beta_features`
  column itself stays for future betas.
- sidebar: the active-account strip no longer keys off the flag.
  It now shows whenever the account name differs from the user's
  own name — i.e. once you've renamed the account or joined a
  shared one — preserving the original "don't duplicate a solo
  user's name" behaviour without the flag.
- CHANGELOG/README: document Members as a shipped, all-users
  feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 17:39:46 +03:00
Arnas Donauskas
47987acb06
Merge pull request #163 from dhruv465/codex/fix-cache-flow-canvas-drag
fix flow drag responsiveness and stale chunks
2026-06-02 13:39:03 +03:00