diff --git a/apps/hq-web/src/components/ClientFormDialog.tsx b/apps/hq-web/src/components/ClientFormDialog.tsx
index 90483c5..f85b1bf 100644
--- a/apps/hq-web/src/components/ClientFormDialog.tsx
+++ b/apps/hq-web/src/components/ClientFormDialog.tsx
@@ -113,10 +113,15 @@ export function ClientFormDialog(props: {
-
+ {/* Typed roles (D18 WS-F): whatsapp/secretary get labeled rows on the support card; free text stays allowed. */}
+
))}
+
{error !== undefined && {error}}
diff --git a/apps/hq-web/src/pages/ClientDetail.tsx b/apps/hq-web/src/pages/ClientDetail.tsx
index 54e67d0..e1bf62f 100644
--- a/apps/hq-web/src/pages/ClientDetail.tsx
+++ b/apps/hq-web/src/pages/ClientDetail.tsx
@@ -545,6 +545,23 @@ function SupportCard(props: { client: Client; onChanged: () => void }) {
{cell('OS', c.os)}
{cell('District', c.district)}
{cell('Sector', c.sector)}
+ {/* Typed contact rows (spec §5): whatsapp/secretary contacts surface here by label. */}
+ {c.contacts
+ .filter((ct) => ct.role === 'whatsapp' || ct.role === 'secretary')
+ .map((ct, i) => {
+ const num = ct.phone ?? ct.email
+ return (
+
+
+ {ct.role === 'whatsapp' ? 'WhatsApp' : 'Secretary'}
+
+ {[ct.name, num].filter((x) => x !== undefined && x !== '').join(' · ')}
+ {num !== undefined && num !== '' && (
+
+ )}
+
+ )
+ })}
DB password
{revealed !== undefined
diff --git a/apps/hq-web/src/pages/Clients.tsx b/apps/hq-web/src/pages/Clients.tsx
index 1a589dc..1a54108 100644
--- a/apps/hq-web/src/pages/Clients.tsx
+++ b/apps/hq-web/src/pages/Clients.tsx
@@ -38,7 +38,12 @@ export const DOC_TONE: Record = {
export function Clients() {
const nav = useNavigate()
const [q, setQ] = useState('')
- const { data, error, reload } = useData(() => getClients(q), [q])
+ // D18 WS-F: the old book's two working filters, applied server-side.
+ const [district, setDistrict] = useState('')
+ const [sector, setSector] = useState('')
+ const { data, error, reload } = useData(
+ () => getClients(q, { district, sector }), [q, district, sector],
+ )
const [sp, setSp] = useSearchParams()
const [creating, setCreating] = useState(sp.get('new') === '1')
const [statusFilter, setStatusFilter] = useState('all')
@@ -65,6 +70,14 @@ export function Clients() {
className="wf" style={{ maxWidth: 280 }} placeholder="Search name / code / GSTIN…"
value={q} onChange={(e) => setQ(e.target.value)}
/>
+ setDistrict(e.target.value)}
+ />
+ setSector(e.target.value)}
+ />
setConverting({ id: d.id, docNo: d.docNo! })}
- >
- Convert & send
-
+ // The whole navigates on click — without this guard the button
+ // click bubbles up, the page unmounts and the confirm never shows.
+ e.stopPropagation()}>
+
+
)
: null,
}))}
diff --git a/apps/hq-web/src/pages/Pipeline.tsx b/apps/hq-web/src/pages/Pipeline.tsx
index c0651b7..87c3dd2 100644
--- a/apps/hq-web/src/pages/Pipeline.tsx
+++ b/apps/hq-web/src/pages/Pipeline.tsx
@@ -1,7 +1,7 @@
import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { formatINR } from '@sims/domain'
-import { Badge, Button, DataTable, EmptyState, ErrorState, FilterChips, Notice, PageHeader, Skeleton, Toolbar } from '@sims/ui'
+import { Badge, Button, ConfirmDialog, DataTable, EmptyState, ErrorState, FilterChips, Notice, PageHeader, Skeleton, Toolbar, useToast } from '@sims/ui'
import {
documentAction, getEmployees, getPipeline, isManagerial,
type Employee, type PipelineFilter, type PipelineRow, type PipelineStage,
@@ -32,10 +32,13 @@ const BAND_TONE = { green: 'ok', amber: 'warn', red: 'err' } as const
*/
export function Pipeline() {
const nav = useNavigate()
+ const toast = useToast()
const [filter, setFilter] = useState('all')
const [owner, setOwner] = useState('')
const [page, setPage] = useState(1)
const [error, setError] = useState()
+ // D18 WS-G: same one-click convert-and-send as the Documents register.
+ const [converting, setConverting] = useState<{ docId: string; clientName: string } | undefined>()
const managerial = isManagerial()
const list = useData(
@@ -75,8 +78,8 @@ export function Pipeline() {
}
if (r.nextAction === 'convert_invoice') {
return (
-