@open-mercato/core 0.4.5-develop-811deeb983 → 0.4.5-develop-3d8e759e45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/modules/catalog/inbox-actions.js +51 -0
  2. package/dist/modules/catalog/inbox-actions.js.map +7 -0
  3. package/dist/modules/customers/inbox-actions.js +230 -0
  4. package/dist/modules/customers/inbox-actions.js.map +7 -0
  5. package/dist/modules/inbox_ops/api/emails/[id]/route.js +40 -1
  6. package/dist/modules/inbox_ops/api/emails/[id]/route.js.map +2 -2
  7. package/dist/modules/inbox_ops/api/extract/route.js +87 -0
  8. package/dist/modules/inbox_ops/api/extract/route.js.map +7 -0
  9. package/dist/modules/inbox_ops/api/proposals/[id]/translate/route.js +6 -1
  10. package/dist/modules/inbox_ops/api/proposals/[id]/translate/route.js.map +2 -2
  11. package/dist/modules/inbox_ops/api/proposals/counts/route.js.map +2 -2
  12. package/dist/modules/inbox_ops/backend/inbox-ops/log/page.js +40 -14
  13. package/dist/modules/inbox_ops/backend/inbox-ops/log/page.js.map +2 -2
  14. package/dist/modules/inbox_ops/backend/inbox-ops/log/page.meta.js +2 -2
  15. package/dist/modules/inbox_ops/backend/inbox-ops/log/page.meta.js.map +2 -2
  16. package/dist/modules/inbox_ops/backend/inbox-ops/page.js +161 -79
  17. package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
  18. package/dist/modules/inbox_ops/backend/inbox-ops/page.meta.js +2 -2
  19. package/dist/modules/inbox_ops/backend/inbox-ops/page.meta.js.map +2 -2
  20. package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js +109 -62
  21. package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js.map +3 -3
  22. package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.meta.js +2 -2
  23. package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.meta.js.map +2 -2
  24. package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js +36 -14
  25. package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js.map +2 -2
  26. package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.meta.js +2 -2
  27. package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.meta.js.map +2 -2
  28. package/dist/modules/inbox_ops/components/proposals/ActionCard.js +65 -10
  29. package/dist/modules/inbox_ops/components/proposals/ActionCard.js.map +2 -2
  30. package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js +58 -10
  31. package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js.map +2 -2
  32. package/dist/modules/inbox_ops/lib/constants.js.map +2 -2
  33. package/dist/modules/inbox_ops/lib/contactValidation.js +40 -0
  34. package/dist/modules/inbox_ops/lib/contactValidation.js.map +7 -0
  35. package/dist/modules/inbox_ops/lib/executionEngine.js +31 -826
  36. package/dist/modules/inbox_ops/lib/executionEngine.js.map +3 -3
  37. package/dist/modules/inbox_ops/lib/executionHelpers.js +368 -0
  38. package/dist/modules/inbox_ops/lib/executionHelpers.js.map +7 -0
  39. package/dist/modules/inbox_ops/lib/extractionPrompt.js +28 -35
  40. package/dist/modules/inbox_ops/lib/extractionPrompt.js.map +3 -3
  41. package/dist/modules/inbox_ops/lib/inbox-actions-generated.d.js +1 -0
  42. package/dist/modules/inbox_ops/lib/inbox-actions-generated.d.js.map +7 -0
  43. package/dist/modules/inbox_ops/lib/translationProvider.js +15 -10
  44. package/dist/modules/inbox_ops/lib/translationProvider.js.map +2 -2
  45. package/dist/modules/inbox_ops/subscribers/extractionWorker.js +16 -16
  46. package/dist/modules/inbox_ops/subscribers/extractionWorker.js.map +2 -2
  47. package/dist/modules/sales/inbox-actions.js +278 -0
  48. package/dist/modules/sales/inbox-actions.js.map +7 -0
  49. package/jest.config.cjs +1 -0
  50. package/jest.mocks/inbox-actions.generated.js +5 -0
  51. package/package.json +2 -2
  52. package/src/modules/catalog/inbox-actions.ts +60 -0
  53. package/src/modules/customers/inbox-actions.ts +285 -0
  54. package/src/modules/inbox_ops/api/emails/[id]/route.ts +44 -0
  55. package/src/modules/inbox_ops/api/extract/route.ts +94 -0
  56. package/src/modules/inbox_ops/api/proposals/[id]/translate/route.ts +6 -1
  57. package/src/modules/inbox_ops/api/proposals/counts/route.ts +2 -0
  58. package/src/modules/inbox_ops/backend/inbox-ops/log/page.meta.ts +2 -2
  59. package/src/modules/inbox_ops/backend/inbox-ops/log/page.tsx +43 -13
  60. package/src/modules/inbox_ops/backend/inbox-ops/page.meta.ts +2 -2
  61. package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +176 -81
  62. package/src/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.meta.ts +2 -2
  63. package/src/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.tsx +122 -68
  64. package/src/modules/inbox_ops/backend/inbox-ops/settings/page.meta.ts +2 -2
  65. package/src/modules/inbox_ops/backend/inbox-ops/settings/page.tsx +36 -14
  66. package/src/modules/inbox_ops/components/proposals/ActionCard.tsx +91 -7
  67. package/src/modules/inbox_ops/components/proposals/EditActionDialog.tsx +64 -12
  68. package/src/modules/inbox_ops/lib/constants.ts +9 -0
  69. package/src/modules/inbox_ops/lib/contactValidation.ts +54 -0
  70. package/src/modules/inbox_ops/lib/executionEngine.ts +47 -1060
  71. package/src/modules/inbox_ops/lib/executionHelpers.ts +527 -0
  72. package/src/modules/inbox_ops/lib/extractionPrompt.ts +45 -34
  73. package/src/modules/inbox_ops/lib/inbox-actions-generated.d.ts +11 -0
  74. package/src/modules/inbox_ops/lib/translationProvider.ts +16 -10
  75. package/src/modules/inbox_ops/subscribers/extractionWorker.ts +16 -18
  76. package/src/modules/sales/inbox-actions.ts +359 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/inbox_ops/backend/inbox-ops/page.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Settings, Inbox, Copy } from 'lucide-react'\n\ntype ProposalRow = {\n id: string\n summary: string\n confidence: string\n status: string\n inboxEmailId: string\n createdAt: string\n participants?: { name: string; email: string }[]\n actionCount?: number\n pendingActionCount?: number\n discrepancyCount?: number\n emailSubject?: string | null\n emailFrom?: string | null\n receivedAt?: string | null\n}\n\ntype ProposalListResponse = {\n items?: ProposalRow[]\n total?: number\n page?: number\n totalPages?: number\n}\n\ntype StatusCounts = {\n pending: number\n partial: number\n accepted: number\n rejected: number\n}\n\nconst STATUS_COLORS: Record<string, string> = {\n pending: 'bg-yellow-100 text-yellow-800',\n partial: 'bg-blue-100 text-blue-800',\n accepted: 'bg-green-100 text-green-800',\n rejected: 'bg-red-100 text-red-800',\n processing: 'bg-purple-100 text-purple-800',\n}\n\nfunction ConfidenceBadge({ value }: { value: string }) {\n const num = parseFloat(value)\n const pct = Math.round(num * 100)\n const color = num >= 0.8 ? 'bg-green-100 text-green-800' : num >= 0.6 ? 'bg-yellow-100 text-yellow-800' : 'bg-red-100 text-red-800'\n return <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${color}`}>{pct}%</span>\n}\n\nfunction StatusBadge({ status }: { status: string }) {\n const t = useT()\n const statusLabels: Record<string, string> = {\n pending: t('inbox_ops.status.pending', 'Pending'),\n partial: t('inbox_ops.status.partial', 'Partial'),\n accepted: t('inbox_ops.status.accepted', 'Accepted'),\n rejected: t('inbox_ops.status.rejected', 'Rejected'),\n processing: t('inbox_ops.status.processing', 'Processing'),\n }\n const color = STATUS_COLORS[status] || 'bg-gray-100 text-gray-800'\n return <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${color}`}>{statusLabels[status] || status}</span>\n}\n\nexport default function InboxOpsProposalsPage() {\n const t = useT()\n const router = useRouter()\n const scopeVersion = useOrganizationScopeVersion()\n\n const [items, setItems] = React.useState<ProposalRow[]>([])\n const [total, setTotal] = React.useState(0)\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(25)\n const [statusFilter, setStatusFilter] = React.useState<string | undefined>()\n const [search, setSearch] = React.useState('')\n const [isLoading, setIsLoading] = React.useState(true)\n const [counts, setCounts] = React.useState<StatusCounts>({ pending: 0, partial: 0, accepted: 0, rejected: 0 })\n const [settings, setSettings] = React.useState<{ inboxAddress?: string } | null>(null)\n const [copied, setCopied] = React.useState(false)\n\n const loadProposals = React.useCallback(async () => {\n setIsLoading(true)\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', String(pageSize))\n if (statusFilter) params.set('status', statusFilter)\n if (search) params.set('search', search)\n\n const result = await apiCall<ProposalListResponse>(`/api/inbox_ops/proposals?${params}`)\n if (result?.ok && result.result?.items) {\n setItems(result.result.items)\n setTotal(result.result.total || 0)\n }\n setIsLoading(false)\n }, [page, pageSize, statusFilter, search, scopeVersion])\n\n const loadCounts = React.useCallback(async () => {\n const result = await apiCall<StatusCounts>('/api/inbox_ops/proposals/counts')\n if (result?.ok && result.result) setCounts(result.result)\n }, [scopeVersion])\n\n const loadSettings = React.useCallback(async () => {\n const result = await apiCall<{ settings: { inboxAddress?: string } | null }>('/api/inbox_ops/settings')\n if (result?.ok && result.result?.settings) setSettings(result.result.settings)\n }, [scopeVersion])\n\n React.useEffect(() => {\n loadProposals()\n loadCounts()\n loadSettings()\n }, [loadProposals, loadCounts, loadSettings])\n\n const handleCopyAddress = React.useCallback(() => {\n if (settings?.inboxAddress) {\n navigator.clipboard.writeText(settings.inboxAddress)\n setCopied(true)\n setTimeout(() => setCopied(false), 2000)\n }\n }, [settings])\n\n const columns: ColumnDef<ProposalRow>[] = React.useMemo(() => [\n {\n accessorKey: 'summary',\n header: t('inbox_ops.summary', 'Summary'),\n cell: ({ row }) => (\n <div className=\"min-w-0\">\n <Link\n href={`/backend/inbox-ops/proposals/${row.original.id}`}\n className=\"text-sm font-medium text-primary hover:underline truncate max-w-[300px] block\"\n >\n {row.original.emailSubject || row.original.summary?.slice(0, 80) || t('inbox_ops.untitled_proposal', 'Untitled proposal')}\n </Link>\n {row.original.emailFrom && (\n <span className=\"text-xs text-muted-foreground truncate block\">{row.original.emailFrom}</span>\n )}\n </div>\n ),\n },\n {\n accessorKey: 'status',\n header: t('inbox_ops.list.status', 'Status'),\n cell: ({ row }) => <StatusBadge status={row.original.status} />,\n },\n {\n id: 'actions_count',\n header: t('inbox_ops.actions_count', 'Actions'),\n cell: ({ row }) => {\n const pending = row.original.pendingActionCount ?? 0\n const total = row.original.actionCount ?? 0\n if (total === 0) return <span className=\"text-sm text-muted-foreground\">\u2014</span>\n return (\n <span className=\"text-sm text-muted-foreground\">\n {t('inbox_ops.list.action_summary', '{pending}/{total} actions')\n .replace('{pending}', String(pending))\n .replace('{total}', String(total))}\n </span>\n )\n },\n },\n {\n accessorKey: 'confidence',\n header: t('inbox_ops.confidence', 'Confidence'),\n cell: ({ row }) => <ConfidenceBadge value={row.original.confidence} />,\n },\n {\n accessorKey: 'receivedAt',\n header: t('inbox_ops.received_at', 'Received'),\n cell: ({ row }) => {\n const dateStr = row.original.receivedAt || row.original.createdAt\n const d = new Date(dateStr)\n return <span className=\"text-sm text-muted-foreground\">{d.toLocaleDateString()} {d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>\n },\n },\n ], [t])\n\n const totalCount = counts.pending + counts.partial + counts.accepted + counts.rejected\n const isEmpty = totalCount === 0 && !isLoading\n\n const tabs = [\n { label: `${t('common.all', 'All')} (${totalCount})`, value: undefined },\n { label: `${t('inbox_ops.status.pending', 'Pending')} (${counts.pending})`, value: 'pending' },\n { label: `${t('inbox_ops.status.partial', 'Partial')} (${counts.partial})`, value: 'partial' },\n { label: `${t('inbox_ops.status.accepted', 'Accepted')} (${counts.accepted})`, value: 'accepted' },\n { label: `${t('inbox_ops.status.rejected', 'Rejected')} (${counts.rejected})`, value: 'rejected' },\n ]\n\n return (\n <Page>\n <div className=\"flex items-center justify-between px-3 py-3 md:px-6 md:py-4\">\n <h1 className=\"text-lg font-semibold\">{t('inbox_ops.title', 'InboxOps')}</h1>\n <Link href=\"/backend/inbox-ops/settings\">\n <Button variant=\"outline\" size=\"sm\">\n <Settings className=\"h-4 w-4\" />\n <span className=\"hidden md:inline ml-1\">{t('inbox_ops.settings.title', 'Settings')}</span>\n </Button>\n </Link>\n </div>\n\n <PageBody>\n {isEmpty ? (\n <div className=\"flex flex-col items-center justify-center py-16 text-center\">\n <Inbox className=\"h-12 w-12 text-muted-foreground mb-4\" />\n <h2 className=\"text-lg font-semibold mb-2\">{t('inbox_ops.empty.title', 'Forward emails to start')}</h2>\n {settings?.inboxAddress && (\n <div className=\"mt-4 flex items-center gap-2 bg-muted rounded-lg px-4 py-3\">\n <code className=\"text-sm font-mono\">{settings.inboxAddress}</code>\n <Button variant=\"outline\" size=\"sm\" onClick={handleCopyAddress}>\n <Copy className=\"h-4 w-4\" />\n {copied ? t('inbox_ops.settings.copied', 'Copied') : t('inbox_ops.settings.copy', 'Copy')}\n </Button>\n </div>\n )}\n <ol className=\"mt-6 text-sm text-muted-foreground text-left space-y-2\">\n <li>1. {t('inbox_ops.empty.step1', 'Forward any email thread to this address')}</li>\n <li>2. {t('inbox_ops.empty.step2', \"We'll analyze it and propose actions\")}</li>\n <li>3. {t('inbox_ops.empty.step3', 'Review and accept with one click')}</li>\n </ol>\n </div>\n ) : (\n <>\n <div className=\"flex items-center gap-2 px-3 py-2 md:px-0 overflow-x-auto\">\n {tabs.map((tab) => (\n <Button\n key={tab.value ?? 'all'}\n variant={statusFilter === tab.value ? 'default' : 'outline'}\n size=\"sm\"\n onClick={() => { setStatusFilter(tab.value); setPage(1) }}\n >\n {tab.label}\n </Button>\n ))}\n </div>\n\n <div className=\"overflow-auto\">\n <div className=\"min-w-[640px]\">\n <DataTable\n columns={columns}\n data={items}\n isLoading={isLoading}\n onRowClick={(row) => router.push(`/backend/inbox-ops/proposals/${row.id}`)}\n pagination={{\n page,\n pageSize,\n total,\n totalPages: Math.ceil(total / pageSize),\n onPageChange: setPage,\n }}\n />\n </div>\n </div>\n </>\n )}\n </PageBody>\n </Page>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { Settings, Inbox, Copy } from 'lucide-react'\n\ntype ProposalRow = {\n id: string\n summary: string\n confidence: string\n status: string\n inboxEmailId: string\n createdAt: string\n participants?: { name: string; email: string }[]\n actionCount?: number\n pendingActionCount?: number\n discrepancyCount?: number\n emailSubject?: string | null\n emailFrom?: string | null\n receivedAt?: string | null\n}\n\ntype ProposalListResponse = {\n items?: ProposalRow[]\n total?: number\n page?: number\n totalPages?: number\n}\n\ntype StatusCounts = {\n pending: number\n partial: number\n accepted: number\n rejected: number\n}\n\nconst STATUS_COLORS: Record<string, string> = {\n pending: 'bg-yellow-100 text-yellow-800',\n partial: 'bg-blue-100 text-blue-800',\n accepted: 'bg-green-100 text-green-800',\n rejected: 'bg-red-100 text-red-800',\n processing: 'bg-purple-100 text-purple-800',\n}\n\nfunction ConfidenceBadge({ value }: { value: string }) {\n const num = parseFloat(value)\n const pct = Math.round(num * 100)\n const color = num >= 0.8 ? 'bg-green-100 text-green-800' : num >= 0.6 ? 'bg-yellow-100 text-yellow-800' : 'bg-red-100 text-red-800'\n return <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${color}`}>{pct}%</span>\n}\n\nfunction StatusBadge({ status }: { status: string }) {\n const t = useT()\n const statusLabels: Record<string, string> = {\n pending: t('inbox_ops.status.pending', 'Pending'),\n partial: t('inbox_ops.status.partial', 'Partial'),\n accepted: t('inbox_ops.status.accepted', 'Accepted'),\n rejected: t('inbox_ops.status.rejected', 'Rejected'),\n processing: t('inbox_ops.status.processing', 'Processing'),\n }\n const color = STATUS_COLORS[status] || 'bg-gray-100 text-gray-800'\n return <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${color}`}>{statusLabels[status] || status}</span>\n}\n\nexport default function InboxOpsProposalsPage() {\n const t = useT()\n const router = useRouter()\n const scopeVersion = useOrganizationScopeVersion()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const { runMutation } = useGuardedMutation<Record<string, unknown>>({\n contextId: 'inbox-ops-proposals',\n })\n\n const [items, setItems] = React.useState<ProposalRow[]>([])\n const [total, setTotal] = React.useState(0)\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(25)\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [search, setSearch] = React.useState('')\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [initialLoadComplete, setInitialLoadComplete] = React.useState(false)\n const [counts, setCounts] = React.useState<StatusCounts>({ pending: 0, partial: 0, accepted: 0, rejected: 0 })\n const [settings, setSettings] = React.useState<{ inboxAddress?: string } | null>(null)\n const [copied, setCopied] = React.useState(false)\n\n const statusFilter = typeof filterValues.status === 'string' ? filterValues.status : undefined\n\n const loadProposals = React.useCallback(async () => {\n setIsLoading(true)\n setError(null)\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', String(pageSize))\n if (statusFilter) params.set('status', statusFilter)\n if (search.trim()) params.set('search', search.trim())\n\n try {\n const result = await apiCall<ProposalListResponse>(`/api/inbox_ops/proposals?${params}`)\n if (result?.ok && result.result?.items) {\n setItems(result.result.items)\n setTotal(result.result.total || 0)\n } else {\n setError(t('inbox_ops.flash.load_failed', 'Failed to load proposals'))\n }\n } catch {\n setError(t('inbox_ops.flash.load_failed', 'Failed to load proposals'))\n }\n setIsLoading(false)\n }, [page, pageSize, statusFilter, search, scopeVersion, t])\n\n const loadCounts = React.useCallback(async () => {\n const result = await apiCall<StatusCounts>('/api/inbox_ops/proposals/counts')\n if (result?.ok && result.result) setCounts(result.result)\n }, [scopeVersion])\n\n const loadSettings = React.useCallback(async () => {\n const result = await apiCall<{ settings: { inboxAddress?: string } | null }>('/api/inbox_ops/settings')\n if (result?.ok && result.result?.settings) setSettings(result.result.settings)\n }, [scopeVersion])\n\n React.useEffect(() => {\n Promise.all([loadProposals(), loadCounts(), loadSettings()]).then(() => {\n setInitialLoadComplete(true)\n })\n }, []) // eslint-disable-line react-hooks/exhaustive-deps\n\n React.useEffect(() => {\n if (initialLoadComplete) loadProposals()\n }, [page, statusFilter, search, scopeVersion]) // eslint-disable-line react-hooks/exhaustive-deps\n\n const handleCopyAddress = React.useCallback(() => {\n if (settings?.inboxAddress) {\n navigator.clipboard.writeText(settings.inboxAddress)\n setCopied(true)\n setTimeout(() => setCopied(false), 2000)\n }\n }, [settings])\n\n const handleRefresh = React.useCallback(() => {\n loadProposals()\n loadCounts()\n }, [loadProposals, loadCounts])\n\n const handleFiltersApply = React.useCallback((values: FilterValues) => {\n setFilterValues(values)\n setPage(1)\n }, [])\n\n const handleFiltersClear = React.useCallback(() => {\n setFilterValues({})\n setPage(1)\n }, [])\n\n const handleRejectProposal = React.useCallback(async (proposalId: string) => {\n const confirmed = await confirm({\n title: t('inbox_ops.action.reject_all', 'Reject Proposal'),\n text: t('inbox_ops.action.reject_all_confirm', 'Reject all pending actions in this proposal?'),\n })\n if (!confirmed) return\n\n const result = await runMutation({\n operation: () => apiCall<{ ok: boolean }>(\n `/api/inbox_ops/proposals/${proposalId}/reject`,\n { method: 'POST' },\n ),\n context: {},\n })\n if (result?.ok && result.result?.ok) {\n flash(t('inbox_ops.action.proposal_rejected', 'Proposal rejected'), 'success')\n loadProposals()\n loadCounts()\n } else {\n flash(t('inbox_ops.flash.action_reject_failed', 'Failed to reject'), 'error')\n }\n }, [confirm, t, loadProposals, loadCounts, runMutation])\n\n const filters = React.useMemo<FilterDef[]>(() => [\n {\n id: 'status',\n label: t('inbox_ops.list.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: 'pending', label: `${t('inbox_ops.status.pending', 'Pending')} (${counts.pending})` },\n { value: 'partial', label: `${t('inbox_ops.status.partial', 'Partial')} (${counts.partial})` },\n { value: 'accepted', label: `${t('inbox_ops.status.accepted', 'Accepted')} (${counts.accepted})` },\n { value: 'rejected', label: `${t('inbox_ops.status.rejected', 'Rejected')} (${counts.rejected})` },\n ],\n },\n ], [t, counts])\n\n const columns: ColumnDef<ProposalRow>[] = React.useMemo(() => [\n {\n accessorKey: 'summary',\n header: t('inbox_ops.summary', 'Summary'),\n cell: ({ row }) => (\n <div className=\"min-w-0\">\n <Link\n href={`/backend/inbox-ops/proposals/${row.original.id}`}\n className=\"text-sm font-medium text-primary hover:underline truncate max-w-[300px] block\"\n >\n {row.original.emailSubject || row.original.summary?.slice(0, 80) || t('inbox_ops.untitled_proposal', 'Untitled proposal')}\n </Link>\n {row.original.emailFrom && (\n <span className=\"text-xs text-muted-foreground truncate block\">{row.original.emailFrom}</span>\n )}\n </div>\n ),\n },\n {\n accessorKey: 'status',\n header: t('inbox_ops.list.status', 'Status'),\n cell: ({ row }) => <StatusBadge status={row.original.status} />,\n },\n {\n id: 'actions_count',\n header: t('inbox_ops.list.progress', 'Progress'),\n cell: ({ row }) => {\n const pending = row.original.pendingActionCount ?? 0\n const total = row.original.actionCount ?? 0\n if (total === 0) return <span className=\"text-sm text-muted-foreground\">\u2014</span>\n return (\n <span className=\"text-sm text-muted-foreground\">\n {t('inbox_ops.list.action_summary', '{pending}/{total} actions')\n .replace('{pending}', String(pending))\n .replace('{total}', String(total))}\n </span>\n )\n },\n },\n {\n accessorKey: 'confidence',\n header: t('inbox_ops.confidence', 'Confidence'),\n cell: ({ row }) => <ConfidenceBadge value={row.original.confidence} />,\n },\n {\n accessorKey: 'receivedAt',\n header: t('inbox_ops.received_at', 'Received'),\n cell: ({ row }) => {\n const dateStr = row.original.receivedAt || row.original.createdAt\n const d = new Date(dateStr)\n return <span className=\"text-sm text-muted-foreground\">{d.toLocaleDateString()} {d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>\n },\n },\n ], [t])\n\n const totalCount = counts.pending + counts.partial + counts.accepted + counts.rejected\n\n const emptyStateContent = initialLoadComplete && totalCount === 0 ? (\n <div className=\"flex flex-col items-center justify-center py-16 text-center\">\n <Inbox className=\"h-12 w-12 text-muted-foreground mb-4\" />\n <h2 className=\"text-lg font-semibold mb-2\">{t('inbox_ops.empty.title', 'Forward emails to start')}</h2>\n {settings?.inboxAddress && (\n <div className=\"mt-4 flex items-center gap-2 bg-muted rounded-lg px-4 py-3\">\n <code className=\"text-sm font-mono\">{settings.inboxAddress}</code>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={handleCopyAddress}>\n <Copy className=\"h-4 w-4\" />\n {copied ? t('inbox_ops.settings.copied', 'Copied') : t('inbox_ops.settings.copy', 'Copy')}\n </Button>\n </div>\n )}\n <ol className=\"mt-6 text-sm text-muted-foreground text-left space-y-2\">\n <li>1. {t('inbox_ops.empty.step1', 'Forward any email thread to this address')}</li>\n <li>2. {t('inbox_ops.empty.step2', \"We'll analyze it and propose actions\")}</li>\n <li>3. {t('inbox_ops.empty.step3', 'Review and accept with one click')}</li>\n </ol>\n </div>\n ) : undefined\n\n if (error && !initialLoadComplete) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage label={error} />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <DataTable<ProposalRow>\n title={t('inbox_ops.title', 'AI Inbox Actions')}\n refreshButton={{\n label: t('inbox_ops.list.actions.refresh', 'Refresh'),\n onRefresh: handleRefresh,\n }}\n actions={(\n <div className=\"flex items-center gap-2\">\n {settings?.inboxAddress && (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={handleCopyAddress}>\n <Copy className=\"h-4 w-4\" />\n <span className=\"hidden md:inline ml-1\">\n {copied ? t('inbox_ops.settings.copied', 'Copied') : t('inbox_ops.settings.copy', 'Copy')}\n </span>\n </Button>\n )}\n <Button variant=\"outline\" size=\"sm\" asChild>\n <Link href=\"/backend/inbox-ops/settings\">\n <Settings className=\"h-4 w-4\" />\n <span className=\"hidden md:inline ml-1\">{t('inbox_ops.list.actions.settings', 'Settings')}</span>\n </Link>\n </Button>\n </div>\n )}\n columns={columns}\n data={items}\n searchValue={search}\n onSearchChange={(value) => { setSearch(value); setPage(1) }}\n searchPlaceholder={t('inbox_ops.list.searchPlaceholder', 'Search proposals...')}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={handleFiltersApply}\n onFiltersClear={handleFiltersClear}\n onRowClick={(row) => router.push(`/backend/inbox-ops/proposals/${row.id}`)}\n rowActions={(row) => (\n <RowActions items={[\n {\n id: 'view',\n label: t('inbox_ops.list.actions.view', 'View'),\n onSelect: () => router.push(`/backend/inbox-ops/proposals/${row.id}`),\n },\n ...(row.status === 'pending' || row.status === 'partial' ? [{\n id: 'reject',\n label: t('inbox_ops.list.actions.reject', 'Reject'),\n destructive: true,\n onSelect: () => handleRejectProposal(row.id),\n }] : []),\n ]} />\n )}\n pagination={{\n page,\n pageSize,\n total,\n totalPages: Math.ceil(total / pageSize),\n onPageChange: setPage,\n }}\n isLoading={isLoading}\n emptyState={emptyStateContent}\n />\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["total"]
7
7
  }
@@ -10,12 +10,12 @@ const metadata = {
10
10
  requireFeatures: ["inbox_ops.proposals.view"],
11
11
  pageTitle: "Proposals",
12
12
  pageTitleKey: "inbox_ops.nav.proposals",
13
- pageGroup: "InboxOps",
13
+ pageGroup: "AI Inbox Actions",
14
14
  pageGroupKey: "inbox_ops.nav.group",
15
15
  pagePriority: 45,
16
16
  pageOrder: 100,
17
17
  icon: inboxIcon,
18
- breadcrumb: [{ label: "InboxOps", labelKey: "inbox_ops.nav.group" }]
18
+ breadcrumb: [{ label: "AI Inbox Actions", labelKey: "inbox_ops.nav.group" }]
19
19
  };
20
20
  export {
21
21
  metadata
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/inbox_ops/backend/inbox-ops/page.meta.ts"],
4
- "sourcesContent": ["import React from 'react'\n\nconst inboxIcon = React.createElement(\n 'svg',\n { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2 },\n React.createElement('polyline', { points: '22 12 16 12 14 15 10 15 8 12 2 12' }),\n React.createElement('path', { d: 'M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z' }),\n)\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['inbox_ops.proposals.view'],\n pageTitle: 'Proposals',\n pageTitleKey: 'inbox_ops.nav.proposals',\n pageGroup: 'InboxOps',\n pageGroupKey: 'inbox_ops.nav.group',\n pagePriority: 45,\n pageOrder: 100,\n icon: inboxIcon,\n breadcrumb: [{ label: 'InboxOps', labelKey: 'inbox_ops.nav.group' }],\n} as const\n"],
5
- "mappings": "AAAA,OAAO,WAAW;AAElB,MAAM,YAAY,MAAM;AAAA,EACtB;AAAA,EACA,EAAE,OAAO,IAAI,QAAQ,IAAI,SAAS,aAAa,MAAM,QAAQ,QAAQ,gBAAgB,aAAa,EAAE;AAAA,EACpG,MAAM,cAAc,YAAY,EAAE,QAAQ,oCAAoC,CAAC;AAAA,EAC/E,MAAM,cAAc,QAAQ,EAAE,GAAG,6GAA6G,CAAC;AACjJ;AAEO,MAAM,WAAW;AAAA,EACtB,aAAa;AAAA,EACb,iBAAiB,CAAC,0BAA0B;AAAA,EAC5C,WAAW;AAAA,EACX,cAAc;AAAA,EACd,WAAW;AAAA,EACX,cAAc;AAAA,EACd,cAAc;AAAA,EACd,WAAW;AAAA,EACX,MAAM;AAAA,EACN,YAAY,CAAC,EAAE,OAAO,YAAY,UAAU,sBAAsB,CAAC;AACrE;",
4
+ "sourcesContent": ["import React from 'react'\n\nconst inboxIcon = React.createElement(\n 'svg',\n { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2 },\n React.createElement('polyline', { points: '22 12 16 12 14 15 10 15 8 12 2 12' }),\n React.createElement('path', { d: 'M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z' }),\n)\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['inbox_ops.proposals.view'],\n pageTitle: 'Proposals',\n pageTitleKey: 'inbox_ops.nav.proposals',\n pageGroup: 'AI Inbox Actions',\n pageGroupKey: 'inbox_ops.nav.group',\n pagePriority: 45,\n pageOrder: 100,\n icon: inboxIcon,\n breadcrumb: [{ label: 'AI Inbox Actions', labelKey: 'inbox_ops.nav.group' }],\n} as const\n"],
5
+ "mappings": "AAAA,OAAO,WAAW;AAElB,MAAM,YAAY,MAAM;AAAA,EACtB;AAAA,EACA,EAAE,OAAO,IAAI,QAAQ,IAAI,SAAS,aAAa,MAAM,QAAQ,QAAQ,gBAAgB,aAAa,EAAE;AAAA,EACpG,MAAM,cAAc,YAAY,EAAE,QAAQ,oCAAoC,CAAC;AAAA,EAC/E,MAAM,cAAc,QAAQ,EAAE,GAAG,6GAA6G,CAAC;AACjJ;AAEO,MAAM,WAAW;AAAA,EACtB,aAAa;AAAA,EACb,iBAAiB,CAAC,0BAA0B;AAAA,EAC5C,WAAW;AAAA,EACX,cAAc;AAAA,EACd,WAAW;AAAA,EACX,cAAc;AAAA,EACd,cAAc;AAAA,EACd,WAAW;AAAA,EACX,MAAM;AAAA,EACN,YAAY,CAAC,EAAE,OAAO,oBAAoB,UAAU,sBAAsB,CAAC;AAC7E;",
6
6
  "names": []
7
7
  }
@@ -7,9 +7,10 @@ import { Page, PageBody } from "@open-mercato/ui/backend/Page";
7
7
  import { Button } from "@open-mercato/ui/primitives/button";
8
8
  import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
9
9
  import { flash } from "@open-mercato/ui/backend/FlashMessages";
10
- import { LoadingMessage } from "@open-mercato/ui/backend/detail";
10
+ import { LoadingMessage, ErrorMessage } from "@open-mercato/ui/backend/detail";
11
11
  import { useT, useLocale } from "@open-mercato/shared/lib/i18n/context";
12
12
  import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
13
+ import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
13
14
  import {
14
15
  ArrowLeft,
15
16
  CheckCircle,
@@ -22,7 +23,8 @@ import {
22
23
  Users,
23
24
  Languages
24
25
  } from "lucide-react";
25
- import { ActionCard, ConfidenceBadge, useActionTypeLabels } from "../../../../components/proposals/ActionCard.js";
26
+ import { ActionCard, ConfidenceBadge, useActionTypeLabels, useDiscrepancyDescriptions } from "../../../../components/proposals/ActionCard.js";
27
+ import { hasContactNameIssue } from "../../../../lib/contactValidation.js";
26
28
  import { EditActionDialog } from "../../../../components/proposals/EditActionDialog.js";
27
29
  function EmailThreadViewer({ email }) {
28
30
  const t = useT();
@@ -52,9 +54,14 @@ function ProposalDetailPage({ params }) {
52
54
  const [discrepancies, setDiscrepancies] = React.useState([]);
53
55
  const [email, setEmail] = React.useState(null);
54
56
  const [isLoading, setIsLoading] = React.useState(true);
57
+ const [error, setError] = React.useState(null);
55
58
  const [isProcessing, setIsProcessing] = React.useState(false);
56
59
  const { confirm, ConfirmDialogElement } = useConfirmDialog();
60
+ const { runMutation } = useGuardedMutation({
61
+ contextId: "inbox-ops-proposal-detail"
62
+ });
57
63
  const actionTypeLabels = useActionTypeLabels();
64
+ const resolveDiscrepancyDescription = useDiscrepancyDescriptions();
58
65
  const [editingAction, setEditingAction] = React.useState(null);
59
66
  const [sendingReplyId, setSendingReplyId] = React.useState(null);
60
67
  const [translation, setTranslation] = React.useState(null);
@@ -97,39 +104,53 @@ function ProposalDetailPage({ params }) {
97
104
  const handleTranslate = React.useCallback(async () => {
98
105
  if (!proposalId) return;
99
106
  setIsTranslating(true);
100
- const result = await apiCall(
101
- `/api/inbox_ops/proposals/${proposalId}/translate`,
102
- { method: "POST", body: JSON.stringify({ targetLocale: locale }) }
103
- );
107
+ const result = await runMutation({
108
+ operation: () => apiCall(
109
+ `/api/inbox_ops/proposals/${proposalId}/translate`,
110
+ { method: "POST", body: JSON.stringify({ targetLocale: locale }) }
111
+ ),
112
+ context: {}
113
+ });
104
114
  if (result?.ok && result.result?.translation) {
105
115
  setTranslation(result.result.translation);
106
116
  setShowTranslation(true);
107
117
  } else {
108
- flash(t("inbox_ops.translate.failed", "Translation failed"), "error");
118
+ const detail = result?.result?.error;
119
+ flash(detail ? `${t("inbox_ops.translate.failed", "Translation failed")}: ${detail}` : t("inbox_ops.translate.failed", "Translation failed"), "error");
109
120
  }
110
121
  setIsTranslating(false);
111
- }, [proposalId, locale, t]);
122
+ }, [proposalId, locale, t, runMutation]);
112
123
  const loadData = React.useCallback(async () => {
113
124
  if (!proposalId) return;
114
125
  setIsLoading(true);
115
- const result = await apiCall(`/api/inbox_ops/proposals/${proposalId}`);
116
- if (result?.ok && result.result) {
117
- setProposal(result.result.proposal);
118
- setActions(result.result.actions || []);
119
- setDiscrepancies(result.result.discrepancies || []);
120
- setEmail(result.result.email);
126
+ setError(null);
127
+ try {
128
+ const result = await apiCall(`/api/inbox_ops/proposals/${proposalId}`);
129
+ if (result?.ok && result.result) {
130
+ setProposal(result.result.proposal);
131
+ setActions(result.result.actions || []);
132
+ setDiscrepancies(result.result.discrepancies || []);
133
+ setEmail(result.result.email);
134
+ } else {
135
+ setError(t("inbox_ops.flash.load_failed", "Failed to load proposal"));
136
+ }
137
+ } catch {
138
+ setError(t("inbox_ops.flash.load_failed", "Failed to load proposal"));
121
139
  }
122
140
  setIsLoading(false);
123
- }, [proposalId]);
141
+ }, [proposalId, t]);
124
142
  React.useEffect(() => {
125
143
  loadData();
126
144
  }, [loadData]);
127
145
  const handleAcceptAction = React.useCallback(async (actionId) => {
128
146
  setIsProcessing(true);
129
- const result = await apiCall(
130
- `/api/inbox_ops/proposals/${proposalId}/actions/${actionId}/accept`,
131
- { method: "POST" }
132
- );
147
+ const result = await runMutation({
148
+ operation: () => apiCall(
149
+ `/api/inbox_ops/proposals/${proposalId}/actions/${actionId}/accept`,
150
+ { method: "POST" }
151
+ ),
152
+ context: {}
153
+ });
133
154
  if (result?.ok && result.result?.ok) {
134
155
  flash(t("inbox_ops.flash.action_executed", "Action executed"), "success");
135
156
  await loadData();
@@ -137,13 +158,16 @@ function ProposalDetailPage({ params }) {
137
158
  flash(result?.result?.error || t("inbox_ops.flash.action_execute_failed", "Failed to execute action"), "error");
138
159
  }
139
160
  setIsProcessing(false);
140
- }, [proposalId, loadData, t]);
161
+ }, [proposalId, loadData, t, runMutation]);
141
162
  const handleRejectAction = React.useCallback(async (actionId) => {
142
163
  setIsProcessing(true);
143
- const result = await apiCall(
144
- `/api/inbox_ops/proposals/${proposalId}/actions/${actionId}/reject`,
145
- { method: "POST" }
146
- );
164
+ const result = await runMutation({
165
+ operation: () => apiCall(
166
+ `/api/inbox_ops/proposals/${proposalId}/actions/${actionId}/reject`,
167
+ { method: "POST" }
168
+ ),
169
+ context: {}
170
+ });
147
171
  if (result?.ok && result.result?.ok) {
148
172
  flash(t("inbox_ops.flash.action_rejected", "Action rejected"), "success");
149
173
  await loadData();
@@ -151,27 +175,34 @@ function ProposalDetailPage({ params }) {
151
175
  flash(t("inbox_ops.flash.action_reject_failed", "Failed to reject action"), "error");
152
176
  }
153
177
  setIsProcessing(false);
154
- }, [proposalId, loadData]);
178
+ }, [proposalId, loadData, runMutation]);
155
179
  const handleAcceptAll = React.useCallback(async () => {
156
- const pendingCount = actions.filter((a) => a.status === "pending").length;
180
+ const pendingActions2 = actions.filter((a) => a.status === "pending");
181
+ const pendingCount = pendingActions2.length;
182
+ const nameIssueCount = pendingActions2.filter((a) => hasContactNameIssue(a)).length;
183
+ const confirmText = nameIssueCount > 0 ? t("inbox_ops.action.accept_all_confirm_with_skip", "Execute {count} pending actions? {skipCount} contact actions will be skipped due to missing names.").replace("{count}", String(pendingCount)).replace("{skipCount}", String(nameIssueCount)) : t("inbox_ops.action.accept_all_confirm", "Execute {count} pending actions?").replace("{count}", String(pendingCount));
157
184
  const confirmed = await confirm({
158
185
  title: t("inbox_ops.action.accept_all", "Accept All"),
159
- text: t("inbox_ops.action.accept_all_confirm", `Execute ${pendingCount} pending actions?`).replace("{count}", String(pendingCount))
186
+ text: confirmText
160
187
  });
161
188
  if (!confirmed) return;
162
189
  setIsProcessing(true);
163
- const result = await apiCall(
164
- `/api/inbox_ops/proposals/${proposalId}/accept-all`,
165
- { method: "POST" }
166
- );
190
+ const result = await runMutation({
191
+ operation: () => apiCall(
192
+ `/api/inbox_ops/proposals/${proposalId}/accept-all`,
193
+ { method: "POST" }
194
+ ),
195
+ context: {}
196
+ });
167
197
  if (result?.ok && result.result?.ok) {
168
- flash(t("inbox_ops.flash.accept_all_success", "{succeeded} actions executed").replace("{succeeded}", String(result.result.succeeded)) + (result.result.failed > 0 ? `, ${result.result.failed} failed` : ""), "success");
198
+ const msg = result.result.failed > 0 ? t("inbox_ops.flash.accept_all_partial", "{succeeded} actions executed, {failed} failed").replace("{succeeded}", String(result.result.succeeded)).replace("{failed}", String(result.result.failed)) : t("inbox_ops.flash.accept_all_success", "{succeeded} actions executed").replace("{succeeded}", String(result.result.succeeded));
199
+ flash(msg, "success");
169
200
  await loadData();
170
201
  } else {
171
202
  flash(t("inbox_ops.flash.accept_all_failed", "Failed to accept all actions"), "error");
172
203
  }
173
204
  setIsProcessing(false);
174
- }, [proposalId, actions, confirm, t, loadData]);
205
+ }, [proposalId, actions, confirm, t, loadData, runMutation]);
175
206
  const handleRejectAll = React.useCallback(async () => {
176
207
  const confirmed = await confirm({
177
208
  title: t("inbox_ops.action.reject_all", "Reject Proposal"),
@@ -179,35 +210,44 @@ function ProposalDetailPage({ params }) {
179
210
  });
180
211
  if (!confirmed) return;
181
212
  setIsProcessing(true);
182
- const result = await apiCall(
183
- `/api/inbox_ops/proposals/${proposalId}/reject`,
184
- { method: "POST" }
185
- );
213
+ const result = await runMutation({
214
+ operation: () => apiCall(
215
+ `/api/inbox_ops/proposals/${proposalId}/reject`,
216
+ { method: "POST" }
217
+ ),
218
+ context: {}
219
+ });
186
220
  if (result?.ok && result.result?.ok) {
187
221
  flash(t("inbox_ops.action.proposal_rejected", "Proposal rejected"), "success");
188
222
  await loadData();
189
223
  }
190
224
  setIsProcessing(false);
191
- }, [proposalId, confirm, t, loadData]);
225
+ }, [proposalId, confirm, t, loadData, runMutation]);
192
226
  const handleRetryExtraction = React.useCallback(async () => {
193
227
  if (!email) return;
194
228
  setIsProcessing(true);
195
- const result = await apiCall(
196
- `/api/inbox_ops/emails/${email.id}/reprocess`,
197
- { method: "POST" }
198
- );
229
+ const result = await runMutation({
230
+ operation: () => apiCall(
231
+ `/api/inbox_ops/emails/${email.id}/reprocess`,
232
+ { method: "POST" }
233
+ ),
234
+ context: {}
235
+ });
199
236
  if (result?.ok && result.result?.ok) {
200
237
  flash(t("inbox_ops.flash.reprocessing_started", "Reprocessing started"), "success");
201
238
  await loadData();
202
239
  }
203
240
  setIsProcessing(false);
204
- }, [email, loadData]);
241
+ }, [email, loadData, runMutation]);
205
242
  const handleSendReply = React.useCallback(async (actionId) => {
206
243
  setSendingReplyId(actionId);
207
- const result = await apiCall(
208
- `/api/inbox_ops/proposals/${proposalId}/replies/${actionId}/send`,
209
- { method: "POST" }
210
- );
244
+ const result = await runMutation({
245
+ operation: () => apiCall(
246
+ `/api/inbox_ops/proposals/${proposalId}/replies/${actionId}/send`,
247
+ { method: "POST" }
248
+ ),
249
+ context: {}
250
+ });
211
251
  if (result?.ok && result.result?.ok) {
212
252
  flash(t("inbox_ops.reply.sent_success", "Reply sent successfully"), "success");
213
253
  await loadData();
@@ -215,8 +255,9 @@ function ProposalDetailPage({ params }) {
215
255
  flash(result?.result?.error || t("inbox_ops.flash.send_reply_failed", "Failed to send reply"), "error");
216
256
  }
217
257
  setSendingReplyId(null);
218
- }, [proposalId, t, loadData]);
258
+ }, [proposalId, t, loadData, runMutation]);
219
259
  if (isLoading) return /* @__PURE__ */ jsx(LoadingMessage, { label: t("inbox_ops.loading_proposal", "Loading proposal...") });
260
+ if (error) return /* @__PURE__ */ jsx(ErrorMessage, { label: error });
220
261
  const pendingActions = actions.filter((a) => a.status === "pending");
221
262
  const emailIsProcessing = email?.status === "processing";
222
263
  const emailFailed = email?.status === "failed";
@@ -231,22 +272,23 @@ function ProposalDetailPage({ params }) {
231
272
  onSaved: loadData
232
273
  }
233
274
  ),
234
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-3 py-3 md:px-6 md:py-4 border-b", children: [
235
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
236
- /* @__PURE__ */ jsx(Link, { href: "/backend/inbox-ops", children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" }) }) }),
237
- /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
238
- /* @__PURE__ */ jsx("h1", { className: "text-lg font-semibold truncate", children: email?.subject || t("inbox_ops.proposal", "Proposal") }),
239
- /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
275
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3 md:px-6 md:py-4 border-b bg-background", children: [
276
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 md:gap-3 min-w-0", children: [
277
+ /* @__PURE__ */ jsx(Link, { href: "/backend/inbox-ops", children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" }) }) }),
278
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
279
+ /* @__PURE__ */ jsx("h1", { className: "text-base md:text-lg font-semibold truncate", children: email?.subject || t("inbox_ops.proposal", "Proposal") }),
280
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground truncate", children: [
240
281
  email?.forwardedByName || email?.forwardedByAddress,
241
282
  " \xB7 ",
242
283
  email?.receivedAt && new Date(email.receivedAt).toLocaleString()
243
284
  ] })
244
285
  ] })
245
286
  ] }),
246
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
287
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [
247
288
  pendingActions.length > 0 && /* @__PURE__ */ jsxs(
248
289
  Button,
249
290
  {
291
+ type: "button",
250
292
  variant: "outline",
251
293
  size: "sm",
252
294
  className: "h-11 md:h-9 text-destructive border-destructive/30 hover:bg-destructive/10",
@@ -258,7 +300,7 @@ function ProposalDetailPage({ params }) {
258
300
  ]
259
301
  }
260
302
  ),
261
- pendingActions.length > 1 && /* @__PURE__ */ jsxs(Button, { size: "sm", className: "h-11 md:h-9", onClick: handleAcceptAll, disabled: isProcessing, children: [
303
+ pendingActions.length > 1 && /* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", className: "h-11 md:h-9", onClick: handleAcceptAll, disabled: isProcessing, children: [
262
304
  isProcessing ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin mr-1" }) : /* @__PURE__ */ jsx(CheckCheck, { className: "h-4 w-4 mr-1" }),
263
305
  /* @__PURE__ */ jsx("span", { className: "hidden md:inline", children: t("inbox_ops.action.accept_all", "Accept All") })
264
306
  ] })
@@ -275,7 +317,7 @@ function ProposalDetailPage({ params }) {
275
317
  /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-red-700", children: t("inbox_ops.extraction_failed", "Extraction failed") })
276
318
  ] }),
277
319
  email?.processingError && /* @__PURE__ */ jsx("p", { className: "text-xs text-red-600 mb-3", children: email.processingError }),
278
- /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", onClick: handleRetryExtraction, disabled: isProcessing, children: [
320
+ /* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: handleRetryExtraction, disabled: isProcessing, children: [
279
321
  /* @__PURE__ */ jsx(RefreshCw, { className: "h-4 w-4 mr-1" }),
280
322
  t("inbox_ops.action.retry", "Retry")
281
323
  ] })
@@ -283,9 +325,10 @@ function ProposalDetailPage({ params }) {
283
325
  /* @__PURE__ */ jsxs("div", { className: "border rounded-lg p-3 md:p-4", children: [
284
326
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
285
327
  /* @__PURE__ */ jsx("h3", { className: "font-semibold text-sm", children: t("inbox_ops.summary", "Summary") }),
286
- proposal.workingLanguage && proposal.workingLanguage !== locale && /* @__PURE__ */ jsxs(
328
+ (proposal.workingLanguage || "en") !== locale && /* @__PURE__ */ jsxs(
287
329
  Button,
288
330
  {
331
+ type: "button",
289
332
  variant: "ghost",
290
333
  size: "sm",
291
334
  className: "h-8 text-xs",
@@ -333,15 +376,17 @@ function ProposalDetailPage({ params }) {
333
376
  /* @__PURE__ */ jsx("div", { className: "space-y-1.5", children: general.map((d) => /* @__PURE__ */ jsxs("div", { className: `flex items-start gap-2 text-xs rounded px-2 py-1.5 ${d.severity === "error" ? "bg-red-100 text-red-700 dark:bg-red-950/30" : "bg-yellow-100 text-yellow-700 dark:bg-yellow-950/30"}`, children: [
334
377
  /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3 w-3 mt-0.5 flex-shrink-0" }),
335
378
  /* @__PURE__ */ jsxs("div", { children: [
336
- /* @__PURE__ */ jsx("span", { children: d.description }),
379
+ /* @__PURE__ */ jsx("span", { children: resolveDiscrepancyDescription(d.description, d.foundValue) }),
337
380
  (d.expectedValue || d.foundValue) && /* @__PURE__ */ jsxs("div", { className: "mt-0.5 text-[11px] opacity-80", children: [
338
381
  d.expectedValue && /* @__PURE__ */ jsxs("span", { children: [
339
- "Expected: ",
382
+ t("inbox_ops.discrepancy.expected", "Expected"),
383
+ ": ",
340
384
  d.expectedValue
341
385
  ] }),
342
386
  d.expectedValue && d.foundValue && /* @__PURE__ */ jsx("span", { children: " \xB7 " }),
343
387
  d.foundValue && /* @__PURE__ */ jsxs("span", { children: [
344
- "Found: ",
388
+ t("inbox_ops.discrepancy.found", "Found"),
389
+ ": ",
345
390
  d.foundValue
346
391
  ] })
347
392
  ] })
@@ -362,12 +407,14 @@ function ProposalDetailPage({ params }) {
362
407
  onReject: handleRejectAction,
363
408
  onRetry: handleAcceptAction,
364
409
  onEdit: handleEditAction,
365
- translatedDescription: showTranslation ? translation?.actions[action.id] : void 0
410
+ translatedDescription: showTranslation ? translation?.actions[action.id] : void 0,
411
+ resolveDiscrepancyDescription
366
412
  }
367
413
  ),
368
414
  action.actionType === "draft_reply" && (action.status === "executed" || action.status === "accepted") && /* @__PURE__ */ jsx("div", { className: "mt-2 pl-7", children: /* @__PURE__ */ jsxs(
369
415
  Button,
370
416
  {
417
+ type: "button",
371
418
  size: "sm",
372
419
  variant: "outline",
373
420
  className: "h-11 md:h-9",