Commit Graph

447 Commits

Author SHA1 Message Date
Arnas Donauskas
c2fee3f2c5
Merge pull request #266 from ArnasDon/fix/settings-fullbleed-width
Settings: full-bleed layout for wide screens
2026-06-18 15:34:26 +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
7e531e6fbd
Merge pull request #265 from ArnasDon/feat/settings-redesign
Settings redesign: grouped rail + Overview landing
2026-06-18 15:11: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
3b55b3f1d9
Merge pull request #264 from ArnasDon/feat/template-image-upload
feat(templates): upload image for template headers; fix image-URL submit (#230)
2026-06-18 14:06:03 +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
60466d14b7
Merge pull request #263 from ArnasDon/feat/chat-media-ga
feat(inbox): graduate chat media send to GA (remove beta flag)
2026-06-18 11:40:46 +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
34e8498a67
Merge pull request #261 from ArnasDon/fix/256-condition-three-outputs
fix(automations): remove spurious third output on condition steps
2026-06-18 11:25:59 +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
f35ae4ef1c
Merge pull request #259 from ArnasDon/feat/chat-media-send
feat(inbox): send photo, video, document & voice in chat (#213)
2026-06-18 11:02:37 +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
9eaa15b9df
Merge pull request #260 from ArnasDon/fix/inbox-chat-sidebar-overlap
fix(inbox): chat thread overlapping contact sidebar on fullscreen desktop (#257)
2026-06-18 10:28:24 +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
e7d8951936
fix(inbox): keep chat thread from overlapping contact sidebar (#257)
The page wrapper around the message thread already carries `min-w-0`
(issue #165), but the MessageThread root itself kept the default
`min-width: auto`. A single wide message (a long unbroken URL or word)
expanded the whole thread past its flex share, so at lg+ the chat
painted on top of the contact sidebar: outgoing bubbles were clipped on
the right and the hover toolbar floated over the Tags panel.

Adding `min-w-0` to the thread root lets it shrink to its column so the
bubbles' existing break-words / max-w-[75%] caps actually apply. Verified
in-browser: a 1272px-wide column went from 962px of overlap to 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:24:40 +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
b56e6b6e3a
Merge pull request #255 from ArnasDon/feat/bulk-delete-contacts
feat(contacts): allow bulk-deleting selected contacts (#239)
2026-06-17 17:12:06 +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
d6c595c0ed
Merge pull request #254 from ArnasDon/feat/inbox-unread-filter
feat(inbox): add "Unread" filter to conversation list
2026-06-17 13:41:29 +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
6c5cfdd377
Merge pull request #253 from ArnasDon/feat/light-dark-mode
feat(theme): add light/dark mode toggle (#243)
2026-06-17 13:12:59 +03:00
Arnas Donauskas
43b842efb6
fix(theme): keep status-badge token contract green (CI test)
The sweep over-tokenized the neutral status badges in the three status
data modules to bg-muted/border-border, which broke
broadcast-status.test.ts ("each variant has the dark-theme class
triple") — it asserts every badge is bg-*/10 + text-* + border-*/20.

The badge system intentionally uses fixed palette colors at low opacity
(translucent fills are mode-tolerant). Restore the original slate fills
(so dark is identical and the triple contract holds) but keep
text-muted-foreground for the label, so neutral statuses stay legible in
light mode where a solid slate-400 would be too faint on white.

- broadcast-status: draft / recipient pending
- template-status: DRAFT / PENDING_DELETION
- trigger-meta: time_based / fallback

All 400 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:08:32 +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
Arnas Donauskas
e1ab7b2b58
Merge pull request #248 from ArnasDon/dependabot/npm_and_yarn/dev-dependencies-d322b45e97
chore(deps-dev): bump the dev-dependencies group across 1 directory with 4 updates
2026-06-16 11:45:21 +03:00
dependabot[bot]
5733b62898
chore(deps-dev): bump the dev-dependencies group across 1 directory with 4 updates
Bumps the dev-dependencies group with 4 updates in the / directory: [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss), [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [prettier](https://github.com/prettier/prettier) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `@tailwindcss/postcss` from 4.3.0 to 4.3.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/@tailwindcss-postcss)

Updates `@types/node` from 25.9.2 to 25.9.3
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `prettier` from 3.8.3 to 3.8.4
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.3...3.8.4)

Updates `vitest` from 4.1.8 to 4.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.9/packages/vitest)

---
updated-dependencies:
- dependency-name: "@tailwindcss/postcss"
  dependency-version: 4.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.9.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: prettier
  dependency-version: 3.8.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: vitest
  dependency-version: 4.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-16 08:43:33 +00:00
Arnas Donauskas
c58d5cb952
Merge pull request #249 from ArnasDon/dependabot/npm_and_yarn/shadcn-4.11.0
chore(deps): bump shadcn from 4.10.0 to 4.11.0
2026-06-16 11:40:41 +03:00
Arnas Donauskas
f6f0a21a04
Merge pull request #250 from ArnasDon/dependabot/npm_and_yarn/lucide-react-1.18.0
chore(deps): bump lucide-react from 1.17.0 to 1.18.0
2026-06-16 11:36:48 +03:00
Arnas Donauskas
53ba8c104e
Merge pull request #246 from iiamabhishek/main
feat(contacts): CSV import tag assignment and preview improvements
2026-06-16 11:31:58 +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
f36cd23123
Merge pull request #252 from ArnasDon/fix/whatsapp-test-number-pin
fix(whatsapp): allow connecting Meta test numbers without a PIN
2026-06-15 17:04:11 +03:00
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
dependabot[bot]
aa38cb4aa3
chore(deps): bump lucide-react from 1.17.0 to 1.18.0
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 1.17.0 to 1.18.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.18.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-15 10:36:56 +00:00
dependabot[bot]
fd2640c89a
chore(deps): bump shadcn from 4.10.0 to 4.11.0
Bumps [shadcn](https://github.com/shadcn-ui/ui/tree/HEAD/packages/shadcn) from 4.10.0 to 4.11.0.
- [Release notes](https://github.com/shadcn-ui/ui/releases)
- [Changelog](https://github.com/shadcn-ui/ui/blob/main/packages/shadcn/CHANGELOG.md)
- [Commits](https://github.com/shadcn-ui/ui/commits/shadcn@4.11.0/packages/shadcn)

---
updated-dependencies:
- dependency-name: shadcn
  dependency-version: 4.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-15 10:36:41 +00: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
5b6fe3d677
Merge pull request #241 from ArnasDon/fix/custom-fields-settings-tab
fix(settings): add Custom Fields tab to Settings (#238)
2026-06-11 16:23:05 +03:00
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
91e6375e61
Merge pull request #240 from ArnasDon/feat/custom-fields-update-contact-action
feat(automations): support custom contact fields in Update Contact Field action
2026-06-11 15:33:28 +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
c569426c8a
Merge pull request #237 from ArnasDon/fix/flow-keyword-comma-input
fix(flows): allow multiple comma-separated keywords in trigger (#234)
2026-06-11 11:18:57 +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
c3f8b3f71d
Merge pull request #231 from ArnasDon/fix/inbox-contact-list-scroll
fix(inbox): let the conversation list scroll instead of clipping
2026-06-08 19:29:40 +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
ba8d653bff
Merge pull request #228 from ArnasDon/feat/contact-phone-dedup
feat(contacts): prevent duplicate phone numbers on create/import (#212)
2026-06-08 16:01:19 +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