diff --git a/frontend/src/features/admin/AdminPortal.tsx b/frontend/src/features/admin/AdminPortal.tsx index 2db2e9f..0cdad0f 100644 --- a/frontend/src/features/admin/AdminPortal.tsx +++ b/frontend/src/features/admin/AdminPortal.tsx @@ -1,19 +1,4 @@ -import { - BarChart3, - ChevronRight, - Contact, - LayoutTemplate, - LockKeyhole, - Megaphone, - MessageSquareText, - Network, - Phone, - ScrollText, - Send, - Settings, - ShieldCheck, - Workflow, -} from 'lucide-react' +import { ChevronRight, LockKeyhole, MessageSquareText, Network, Phone, ShieldCheck } from 'lucide-react' import { useEffect, useState } from 'react' import type { FormEvent } from 'react' import { @@ -405,23 +390,9 @@ function AdminCompliance() { ) } -const WORKSPACE_NAV = [ - { id: 'conversations', label: 'Conversations', icon: MessageSquareText }, - { id: 'compose', label: 'Send message', icon: Send }, - { id: 'templates', label: 'Templates', icon: LayoutTemplate }, - { id: 'contacts', label: 'Contacts', icon: Contact }, - { id: 'campaigns', label: 'Campaigns', icon: Megaphone }, - { id: 'automations', label: 'Automations', icon: Workflow }, - { id: 'analytics', label: 'Analytics', icon: BarChart3 }, - { id: 'logs', label: 'Webhook logs', icon: ScrollText }, - { id: 'numbers', label: 'Phone numbers', icon: Phone }, -] as const - export function AdminApp() { const [authenticated, setAuthenticated] = useState(null) - const [section, setSection] = useState<'compliance' | 'whatsapp'>('whatsapp') - const [focus, setFocus] = useState<'inbox' | 'templates' | 'compose'>('inbox') - const [nav, setNav] = useState('conversations') + const [section, setSection] = useState<'compliance' | 'whatsapp'>('compliance') useEffect(() => { adminApi.me().then(() => setAuthenticated(true)).catch(() => setAuthenticated(false)) @@ -432,74 +403,39 @@ export function AdminApp() { setAuthenticated(false) } - function openWorkspace(id: string) { - setSection('whatsapp') - setNav(id) - if (id === 'templates') setFocus('templates') - else if (id === 'compose') setFocus('compose') - else setFocus('inbox') - } - if (authenticated === null) return
Checking admin session…
- if (!authenticated) { - return ( -
-
-
UrmiAdmin portal
-
-
- setAuthenticated(true)} /> -
-
- ) - } return ( -
- -
- {section === 'whatsapp' - ? - : ( - <> -
- Settings - -
- - - )} -
+ + + +
+ )} + +
+ {authenticated + ? (section === 'compliance' ? : ) + : setAuthenticated(true)} />} +
) } diff --git a/frontend/src/features/admin/AdminWhatsAppPage.tsx b/frontend/src/features/admin/AdminWhatsAppPage.tsx index be9c19d..1e487ce 100644 --- a/frontend/src/features/admin/AdminWhatsAppPage.tsx +++ b/frontend/src/features/admin/AdminWhatsAppPage.tsx @@ -1,16 +1,11 @@ import { - Ban, - Bell, Braces, Check, CheckCheck, - ChevronDown, Image as ImageIcon, - Megaphone, MessageSquareText, MoreHorizontal, Paperclip, - Pencil, Plus, RefreshCw, Search, @@ -18,10 +13,7 @@ import { Settings2, SlidersHorizontal, Smile, - Sparkles, Star, - StickyNote, - UserPlus, UserRound, X, } from 'lucide-react' @@ -38,7 +30,7 @@ import { type SendMode = 'text' | 'template' | 'media' | 'buttons' type ListFilter = 'all' | 'unread' | 'important' -type ProfileTab = 'details' | 'notes' | 'tags' | 'events' +type ProfileTab = 'details' | 'events' type UiMessage = AdminWhatsAppMessage & { buttons?: string[] } @@ -108,177 +100,6 @@ function formatWhen(value: string) { }) } -function ago(minutes: number) { - return new Date(Date.now() - minutes * 60_000).toISOString() -} - -function demoMessage( - peerPhone: string, - id: string, - direction: 'inbound' | 'outbound', - body: string, - created_at: string, - status: string, - message_type = 'text', - buttons?: string[], -): UiMessage { - return { - id, - direction, - wa_message_id: `wamid.demo.${id}`, - from: direction === 'inbound' ? peerPhone : 'business', - to: direction === 'outbound' ? peerPhone : 'business', - body, - message_type, - status, - created_at, - buttons, - } -} - -const DEMO_THREADS: Array & { unread: number; important: boolean }> = [ - { - peer: 'demo:sd', - title: '+91 91491 64337', - phoneLabel: '+91 91491 64337', - initials: 'SD', - color: '#3b82f6', - online: true, - demo: true, - source: 'Website', - tags: [], - unread: 1, - important: false, - messages: [ - demoMessage('919149164337', 'sd-1', 'inbound', 'Hi', ago(28), 'received'), - demoMessage('919149164337', 'sd-2', 'outbound', 'Hello! 👋\nWelcome to Urmi.\n\nHow can we help you today?', ago(27), 'read'), - demoMessage('919149164337', 'sd-3', 'inbound', 'I want to know about WhatsApp Business integration and pricing.', ago(25), 'received'), - demoMessage( - '919149164337', - 'sd-4', - 'outbound', - 'Sure! I can help with that.\nWould you like to:\n• See our pricing plans\n• Learn about the integration process\n• Talk to our team\n\nPlease let me know what you’re interested in.', - ago(24), - 'read', - ), - demoMessage('919149164337', 'sd-5', 'outbound', 'Urmi onboarding — your workspace is ready to connect WhatsApp.', ago(23), 'delivered', 'template'), - demoMessage('919149164337', 'sd-6', 'outbound', 'Choose what you want to review next.', ago(22), 'sent', 'interactive_buttons', ['See pricing', 'Talk to our team']), - ], - }, - { - peer: 'demo:riya', - title: 'Riya Kapoor', - phoneLabel: '+91 98111 20014', - initials: 'RK', - color: '#ec4899', - online: false, - demo: true, - source: 'WhatsApp', - tags: ['Pricing'], - unread: 2, - important: true, - messages: [ - demoMessage('919811120014', 'rk-1', 'inbound', 'Can you share pricing?', ago(46), 'received'), - ], - }, - { - peer: 'demo:amit', - title: 'Amit Bansal', - phoneLabel: '+91 98200 34221', - initials: 'AB', - color: '#22c55e', - online: false, - demo: true, - source: 'Website', - tags: ['Template'], - unread: 0, - important: false, - messages: [ - demoMessage('919820034221', 'ab-1', 'outbound', 'Template approved', ago(160), 'read', 'template'), - ], - }, - { - peer: 'demo:vikram', - title: 'Vikram Chennai', - phoneLabel: '+91 98400 11820', - initials: 'VC', - color: '#8b5cf6', - online: false, - demo: true, - source: 'Referral', - tags: [], - unread: 0, - important: false, - messages: [ - demoMessage('919840011820', 'vc-1', 'inbound', 'Thanks, received!', ago(190), 'received'), - ], - }, - { - peer: 'demo:neha', - title: 'Neha Sharma', - phoneLabel: '+91 99000 44118', - initials: 'NS', - color: '#f59e0b', - online: true, - demo: true, - source: 'Website', - tags: ['Docs'], - unread: 1, - important: false, - messages: [ - demoMessage('919900044118', 'ns-1', 'inbound', 'Do you have integration docs?', ago(220), 'received'), - ], - }, - { - peer: 'demo:delhi', - title: 'Delhi Pharma', - phoneLabel: '+91 98100 77881', - initials: 'DP', - color: '#14b8a6', - online: false, - demo: true, - source: 'Campaign', - tags: ['Bulk SMS'], - unread: 0, - important: true, - messages: [ - demoMessage('919810077881', 'dp-1', 'inbound', 'We need bulk SMS also', ago(260), 'received'), - ], - }, - { - peer: 'demo:saanvi', - title: 'Saanvi Gupta', - phoneLabel: '+91 98765 12009', - initials: 'SG', - color: '#6366f1', - online: false, - demo: true, - source: 'Website', - tags: ['KYC'], - unread: 0, - important: false, - messages: [ - demoMessage('919876512009', 'sg-1', 'inbound', 'Is KYC required?', ago(60 * 26), 'received'), - ], - }, - { - peer: 'demo:manish', - title: 'Manish Kumar', - phoneLabel: '+91 98181 33002', - initials: 'MK', - color: '#0ea5e9', - online: false, - demo: true, - source: 'WhatsApp', - tags: [], - unread: 0, - important: false, - messages: [ - demoMessage('919818133002', 'mk-1', 'inbound', 'Call me when free', ago(60 * 28), 'received'), - ], - }, -] - function eventLabel(message: UiMessage) { if (message.message_type === 'template') return message.direction === 'outbound' ? 'Template sent' : 'Template received' if (message.message_type === 'interactive_buttons') return 'Buttons sent' @@ -287,18 +108,12 @@ function eventLabel(message: UiMessage) { return message.direction === 'inbound' ? 'Customer message' : 'Reply sent' } -export function AdminWhatsAppPage({ - onLogout, - focus = 'inbox', -}: { - onLogout?: () => void - focus?: 'inbox' | 'templates' | 'compose' -}) { +export function AdminWhatsAppPage() { const [config, setConfig] = useState(null) const [messages, setMessages] = useState([]) const [templates, setTemplates] = useState([]) const [loading, setLoading] = useState(true) - const [selectedPeer, setSelectedPeer] = useState('demo:sd') + const [selectedPeer, setSelectedPeer] = useState(null) const [newPeer, setNewPeer] = useState('') const [startingNew, setStartingNew] = useState(false) const [sendMode, setSendMode] = useState('text') @@ -306,7 +121,6 @@ export function AdminWhatsAppPage({ const [error, setError] = useState('') const [sending, setSending] = useState(false) const [savingConfig, setSavingConfig] = useState(false) - const [accountOpen, setAccountOpen] = useState(false) const [phoneNumberId, setPhoneNumberId] = useState('') const [businessAccountId, setBusinessAccountId] = useState('') @@ -321,34 +135,20 @@ export function AdminWhatsAppPage({ const [mediaLink, setMediaLink] = useState('') const [mediaCaption, setMediaCaption] = useState('') const [mediaFilename, setMediaFilename] = useState('') - const [buttonBody, setButtonBody] = useState('Choose an option') - const [buttonOne, setButtonOne] = useState('See pricing') - const [buttonTwo, setButtonTwo] = useState('Talk to our team') + const [buttonBody, setButtonBody] = useState('') + const [buttonOne, setButtonOne] = useState('') + const [buttonTwo, setButtonTwo] = useState('') const [buttonThree, setButtonThree] = useState('') const [threadQuery, setThreadQuery] = useState('') const [listFilter, setListFilter] = useState('all') const [readPeers, setReadPeers] = useState>(new Set()) - const [starred, setStarred] = useState>(new Set(['demo:riya', 'demo:delhi'])) - const [demoExtras, setDemoExtras] = useState>({}) - const [names, setNames] = useState>({}) - const [editingName, setEditingName] = useState(false) - const [nameDraft, setNameDraft] = useState('') - const [notes, setNotes] = useState>({}) - const [tags, setTags] = useState>({}) - const [tagDraft, setTagDraft] = useState('') - const [languages, setLanguages] = useState>({}) - const [timezones, setTimezones] = useState>({}) - const [blocked, setBlocked] = useState>(new Set()) + const [starred, setStarred] = useState>(new Set()) const [profileTab, setProfileTab] = useState('details') - const [assignee, setAssignee] = useState('Unassigned') - const [assignOpen, setAssignOpen] = useState(false) const [moreOpen, setMoreOpen] = useState(false) const [detailsId, setDetailsId] = useState(null) const [reactFor, setReactFor] = useState(null) const [emojiOpen, setEmojiOpen] = useState(false) - const [actionNote, setActionNote] = useState('') - const chatEndRef = useRef(null) const replyRef = useRef(null) @@ -379,63 +179,38 @@ export function AdminWhatsAppPage({ return () => window.clearInterval(timer) }, []) - useEffect(() => { - if (focus === 'templates') setSendMode('template') - if (focus === 'compose') { - setSendMode('text') - replyRef.current?.focus() - } - }, [focus]) - const threads = useMemo(() => { - const real = new Map() + const grouped = new Map() for (const message of messages) { const peer = message.direction === 'inbound' ? message.from : message.to if (!peer || message.message_type === 'status') continue - const list = real.get(peer) ?? [] + const list = grouped.get(peer) ?? [] list.push(message) - real.set(peer, list) + grouped.set(peer, list) } - const rows: ThreadRow[] = [] - for (const [peer, thread] of real) { - const sorted = [...thread].sort((a, b) => a.created_at.localeCompare(b.created_at)) - const latest = sorted[sorted.length - 1] - const unread = !readPeers.has(peer) && latest.direction === 'inbound' ? 1 : 0 - rows.push({ - peer, - title: names[peer] || formatWaNumber(peer), - phoneLabel: formatWaNumber(peer), - initials: names[peer] ? names[peer].slice(0, 2).toUpperCase() : null, - color: AVATAR_COLORS[peer.length % AVATAR_COLORS.length], - messages: sorted, - latest, - unread, - important: starred.has(peer), - online: Date.now() - new Date(latest.created_at).getTime() < 15 * 60_000, - demo: false, - source: 'WhatsApp', - tags: tags[peer] ?? [], + return [...grouped.entries()] + .map(([peer, thread]) => { + const sorted = [...thread].sort((a, b) => a.created_at.localeCompare(b.created_at)) + const latest = sorted[sorted.length - 1] + return { + peer, + title: formatWaNumber(peer), + phoneLabel: formatWaNumber(peer), + initials: null, + color: AVATAR_COLORS[peer.length % AVATAR_COLORS.length], + messages: sorted, + latest, + unread: !readPeers.has(peer) && latest.direction === 'inbound' ? 1 : 0, + important: starred.has(peer), + online: Date.now() - new Date(latest.created_at).getTime() < 15 * 60_000, + demo: false, + source: 'WhatsApp', + tags: [], + } satisfies ThreadRow }) - } - - for (const demo of DEMO_THREADS) { - const extras = demoExtras[demo.peer] ?? [] - const sorted = [...demo.messages, ...extras].sort((a, b) => a.created_at.localeCompare(b.created_at)) - const latest = sorted[sorted.length - 1] - rows.push({ - ...demo, - title: names[demo.peer] || demo.title, - messages: sorted, - latest, - unread: readPeers.has(demo.peer) ? 0 : demo.unread, - important: starred.has(demo.peer), - tags: tags[demo.peer] ?? demo.tags, - }) - } - - return rows.sort((a, b) => b.latest.created_at.localeCompare(a.latest.created_at)) - }, [messages, demoExtras, names, readPeers, starred, tags]) + .sort((a, b) => b.latest.created_at.localeCompare(a.latest.created_at)) + }, [messages, readPeers, starred]) const visibleThreads = useMemo(() => { const query = threadQuery.trim().toLowerCase() @@ -465,11 +240,9 @@ export function AdminWhatsAppPage({ setDetailsId(null) setReactFor(null) setProfileTab('details') - if (!thread.demo) { - const lastInbound = [...thread.messages].reverse().find((item) => item.direction === 'inbound' && item.wa_message_id) - if (lastInbound?.wa_message_id) { - adminApi.markWhatsAppRead(lastInbound.wa_message_id).catch(() => undefined) - } + const lastInbound = [...thread.messages].reverse().find((item) => item.direction === 'inbound' && item.wa_message_id) + if (lastInbound?.wa_message_id) { + adminApi.markWhatsAppRead(lastInbound.wa_message_id).catch(() => undefined) } } @@ -502,29 +275,9 @@ export function AdminWhatsAppPage({ } } - function pushDemoMessage(peer: string, message: UiMessage) { - setDemoExtras((current) => ({ ...current, [peer]: [...(current[peer] ?? []), message] })) - window.setTimeout(() => { - setDemoExtras((current) => ({ - ...current, - [peer]: (current[peer] ?? []).map((item) => item.id === message.id ? { ...item, status: 'delivered' } : item), - })) - }, 700) - window.setTimeout(() => { - setDemoExtras((current) => ({ - ...current, - [peer]: (current[peer] ?? []).map((item) => item.id === message.id ? { ...item, status: 'read' } : item), - })) - }, 1600) - } - async function sendReaction(message: UiMessage, emoji: string) { if (!active) return setReactFor(null) - if (active.demo) { - pushDemoMessage(active.peer, demoMessage(active.phoneLabel, `react-${Date.now()}`, 'outbound', `Reacted ${emoji}`, new Date().toISOString(), 'sent', 'reaction')) - return - } setSending(true) setError('') try { @@ -549,62 +302,36 @@ export function AdminWhatsAppPage({ setError('Choose a conversation or enter a recipient number') return } - if (active && blocked.has(active.peer)) { - setError('This contact is blocked') - return - } setSending(true) setError('') try { - if (recipient.startsWith('demo:')) { - const phone = active?.phoneLabel ?? recipient - const id = `local-${Date.now()}` - let body = textBody.trim() - let type: UiMessage['message_type'] = 'text' - let buttons: string[] | undefined - if (sendMode === 'template') { - body = templateName || 'Template message' - type = 'template' - } else if (sendMode === 'media') { - body = mediaCaption.trim() || `${mediaType} attachment` - type = mediaType - } else if (sendMode === 'buttons') { - body = buttonBody.trim() - type = 'interactive_buttons' - buttons = [buttonOne, buttonTwo, buttonThree].map((item) => item.trim()).filter(Boolean) - } - pushDemoMessage(recipient, { - ...demoMessage(phone, id, 'outbound', body, new Date().toISOString(), 'sent', type, buttons), - }) - } else { - const payload: AdminWhatsAppSendPayload = { to: recipient, message_type: 'text' } - if (sendMode === 'text') { - payload.text = textBody.trim() - payload.reply_to = replyTo.trim() || undefined - } else if (sendMode === 'template') { - payload.message_type = 'template' - payload.template_name = templateName - payload.template_language = templateLanguage - } else if (sendMode === 'media') { - payload.message_type = mediaType - payload.media_link = mediaLink.trim() - payload.media_caption = mediaCaption.trim() || undefined - payload.media_filename = mediaFilename.trim() || undefined - payload.reply_to = replyTo.trim() || undefined - } else if (sendMode === 'buttons') { - payload.message_type = 'interactive_buttons' - payload.interactive_body = buttonBody.trim() - payload.interactive_buttons = [ - { id: 'btn1', title: buttonOne.trim() }, - ...(buttonTwo.trim() ? [{ id: 'btn2', title: buttonTwo.trim() }] : []), - ...(buttonThree.trim() ? [{ id: 'btn3', title: buttonThree.trim() }] : []), - ] - payload.reply_to = replyTo.trim() || undefined - } - const sent = await adminApi.sendWhatsAppMessage(payload) - if (startingNew || !active) setSelectedPeer(sent.to) - await loadMessages() + const payload: AdminWhatsAppSendPayload = { to: recipient, message_type: 'text' } + if (sendMode === 'text') { + payload.text = textBody.trim() + payload.reply_to = replyTo.trim() || undefined + } else if (sendMode === 'template') { + payload.message_type = 'template' + payload.template_name = templateName + payload.template_language = templateLanguage + } else if (sendMode === 'media') { + payload.message_type = mediaType + payload.media_link = mediaLink.trim() + payload.media_caption = mediaCaption.trim() || undefined + payload.media_filename = mediaFilename.trim() || undefined + payload.reply_to = replyTo.trim() || undefined + } else if (sendMode === 'buttons') { + payload.message_type = 'interactive_buttons' + payload.interactive_body = buttonBody.trim() + payload.interactive_buttons = [ + { id: 'btn1', title: buttonOne.trim() }, + ...(buttonTwo.trim() ? [{ id: 'btn2', title: buttonTwo.trim() }] : []), + ...(buttonThree.trim() ? [{ id: 'btn3', title: buttonThree.trim() }] : []), + ] + payload.reply_to = replyTo.trim() || undefined } + const sent = await adminApi.sendWhatsAppMessage(payload) + if (startingNew || !active) setSelectedPeer(sent.to) + await loadMessages() setTextBody('') setMediaCaption('') setReplyTo('') @@ -619,30 +346,17 @@ export function AdminWhatsAppPage({ } } - function addTag() { - const value = tagDraft.trim() - if (!value || !active) return - setTags((current) => ({ - ...current, - [active.peer]: [...new Set([...(current[active.peer] ?? active.tags), value])], - })) - setTagDraft('') - } - if (loading) return
Loading WhatsApp Business…
const readyToSend = Boolean(config?.ready_to_send) - const language = active ? languages[active.peer] ?? 'English' : 'English' - const timezone = active ? timezones[active.peer] ?? 'Asia/Kolkata (GMT+5:30)' : 'Asia/Kolkata (GMT+5:30)' return ( -
-
- +
+
+
+

WhatsApp Business

+

Live conversations on {config ? formatWaNumber(config.business_phone) : 'the business line'}.

+
@@ -654,28 +368,8 @@ export function AdminWhatsAppPage({ - -
- - {accountOpen && ( -
- -
- )} -
-
-
- -
-
-

WhatsApp Business

-

Manage conversations, send messages, and grow your business with Urmi.

+
{showSetup && (
@@ -703,7 +397,7 @@ export function AdminWhatsAppPage({

Conversations

-
)}
-
- - {assignOpen && ( -
- {['Unassigned', 'Admin'].map((option) => ( - - ))} -
- )} -
- {moreOpen && active && (
-
)}
- {assignee !== 'Unassigned' && {assignee}}
@@ -911,7 +588,6 @@ export function AdminWhatsAppPage({ {templates.map((template) => ( ))} - {templates.length === 0 && } setTemplateLanguage(event.target.value)} aria-label="Language code" /> @@ -924,7 +600,7 @@ export function AdminWhatsAppPage({ - setMediaLink(event.target.value)} placeholder="Public file link" required={!active?.demo} /> + setMediaLink(event.target.value)} placeholder="Public file link" required /> {mediaType === 'document' && setMediaFilename(event.target.value)} placeholder="Filename" />} )} @@ -948,16 +624,6 @@ export function AdminWhatsAppPage({ placeholder="Type a message…" required={sendMode !== 'template'} /> -
@@ -974,7 +640,7 @@ export function AdminWhatsAppPage({ )}
{error &&

{error}

} -
@@ -983,33 +649,15 @@ export function AdminWhatsAppPage({