From 2e593e4b9d6107efb292c3d96924f329b555b7ae Mon Sep 17 00:00:00 2001 From: larryro <371767072@qq.com> Date: Sun, 15 Mar 2026 19:10:57 +0800 Subject: [PATCH 1/2] feat: remove standalone approvals page and consolidate into chat Remove the dedicated approvals route, components, hooks, and types. Move approval mutation/query logic into the chat feature and update navigation items accordingly. --- .../components/approval-detail-dialog.tsx | 251 -------------- .../components/approvals-navigation.tsx | 33 -- .../approvals/components/approvals.tsx | 221 ------------ .../approvals/get-approval-detail.ts | 103 ------ .../approvals/product-list-cell.tsx | 138 -------- .../approvals/use-approval-columns.tsx | 317 ------------------ .../approvals/components/product-card.tsx | 144 -------- .../app/features/approvals/hooks/actions.ts | 20 -- .../app/features/approvals/hooks/mutations.ts | 10 - .../app/features/approvals/hooks/queries.ts | 54 --- .../approvals/types/approval-detail.ts | 50 --- .../components/integration-approval-card.tsx | 6 +- .../workflow-creation-approval-card.tsx | 6 +- .../components/workflow-run-approval-card.tsx | 6 +- .../workflow-update-approval-card.tsx | 6 +- .../app/features/chat/hooks/mutations.ts | 23 ++ .../app/features/chat/hooks/queries.ts | 13 +- .../app/hooks/use-navigation-items.ts | 34 +- services/platform/app/routeTree.gen.ts | 52 --- .../app/routes/dashboard/$id/approvals.tsx | 65 ---- .../dashboard/$id/approvals/$status.tsx | 85 ----- services/platform/messages/en.json | 80 ----- 22 files changed, 53 insertions(+), 1664 deletions(-) delete mode 100644 services/platform/app/features/approvals/components/approval-detail-dialog.tsx delete mode 100644 services/platform/app/features/approvals/components/approvals-navigation.tsx delete mode 100644 services/platform/app/features/approvals/components/approvals.tsx delete mode 100644 services/platform/app/features/approvals/components/approvals/get-approval-detail.ts delete mode 100644 services/platform/app/features/approvals/components/approvals/product-list-cell.tsx delete mode 100644 services/platform/app/features/approvals/components/approvals/use-approval-columns.tsx delete mode 100644 services/platform/app/features/approvals/components/product-card.tsx delete mode 100644 services/platform/app/features/approvals/hooks/actions.ts delete mode 100644 services/platform/app/features/approvals/hooks/mutations.ts delete mode 100644 services/platform/app/features/approvals/hooks/queries.ts delete mode 100644 services/platform/app/features/approvals/types/approval-detail.ts delete mode 100644 services/platform/app/routes/dashboard/$id/approvals.tsx delete mode 100644 services/platform/app/routes/dashboard/$id/approvals/$status.tsx diff --git a/services/platform/app/features/approvals/components/approval-detail-dialog.tsx b/services/platform/app/features/approvals/components/approval-detail-dialog.tsx deleted file mode 100644 index aae872e4d2..0000000000 --- a/services/platform/app/features/approvals/components/approval-detail-dialog.tsx +++ /dev/null @@ -1,251 +0,0 @@ -'use client'; - -import { Sparkles } from 'lucide-react'; -import { useState, useMemo } from 'react'; - -import { - type StatGridItem, - StatGrid, -} from '@/app/components/ui/data-display/stat-grid'; -import { Dialog } from '@/app/components/ui/dialog/dialog'; -import { Badge } from '@/app/components/ui/feedback/badge'; -import { HStack, Stack } from '@/app/components/ui/layout/layout'; -import { PageSection } from '@/app/components/ui/layout/page-section'; -import { SectionHeader } from '@/app/components/ui/layout/section-header'; -import { Button } from '@/app/components/ui/primitives/button'; -import { Text } from '@/app/components/ui/typography/text'; -import { CustomerInfoDialog } from '@/app/features/customers/components/customer-info-dialog'; -import { - useCustomerByEmail, - useCustomers, -} from '@/app/features/customers/hooks/queries'; -import { useFormatDate } from '@/app/hooks/use-format-date'; -import { useT } from '@/lib/i18n/client'; -import { cn } from '@/lib/utils/cn'; - -import { ApprovalDetail } from '../types/approval-detail'; -import { ProductCard } from './product-card'; - -const RecommendationIcon = () => ( - -); - -interface ApprovalDetailDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - approvalDetail: ApprovalDetail | null; - onApprove?: (approvalId: string) => void; - onReject?: (approvalId: string) => void; - isApproving?: boolean; - isRejecting?: boolean; - onRemoveRecommendation?: (approvalId: string, productId: string) => void; - removingProductId?: string | null; -} - -export function ApprovalDetailDialog({ - open, - onOpenChange, - approvalDetail, - onApprove, - onReject, - isApproving, - isRejecting, - onRemoveRecommendation, - removingProductId, -}: ApprovalDetailDialogProps) { - const { t } = useT('approvals'); - const { formatDate } = useFormatDate(); - const [customerInfoOpen, setCustomerInfoOpen] = useState(false); - - const { customers } = useCustomers(approvalDetail?.organizationId ?? ''); - const customerRecord = useCustomerByEmail( - customers, - approvalDetail?.customer.email, - ); - - // Sort products by confidence (high to low) and get first product - const approvalStatItems = useMemo( - () => [ - { - label: t('detail.status'), - value: ( - - {(approvalDetail?.status === 'pending' && - t('detail.statusPending')) || - (approvalDetail?.status === 'approved' && - t('detail.statusApproved')) || - (approvalDetail?.status === 'rejected' && - t('detail.statusRejected')) || - t('detail.statusPending')} - - ), - }, - { - label: t('detail.type'), - value: ( - - {t('detail.typeProductRecommendation')} - - ), - }, - { - label: t('detail.createdAt'), - value: ( - - {approvalDetail?.createdAt - ? formatDate(new Date(approvalDetail.createdAt), 'long') - : ''} - - ), - }, - ...(approvalDetail?.confidence !== undefined - ? [ - { - label: t('detail.confidence'), - value: ( - {approvalDetail.confidence}% - ), - }, - ] - : []), - ], - [approvalDetail, t, formatDate], - ); - - const sortedProducts = useMemo(() => { - if (!approvalDetail) return []; - return [...approvalDetail.recommendedProducts].sort((a, b) => { - const confA = a.confidence ?? 0; - const confB = b.confidence ?? 0; - return confB - confA; - }); - }, [approvalDetail]); - - if (!approvalDetail) return null; - - const handleRemoveRecommendation = (productId: string) => { - if (!onRemoveRecommendation) return; - onRemoveRecommendation(approvalDetail._id, productId); - }; - - const visibleProducts = sortedProducts.slice(0, 3); - - const footer = - approvalDetail.status === 'pending' ? ( - - - - - ) : undefined; - - return ( - <> - - - - } - > - {/* Content */} - - {/* Customer Info */} - - - - - - - {/* Recommended Products */} - {visibleProducts.length > 0 && ( - -
- {visibleProducts.map((product) => ( - - ))} -
-
- )} - - {/* Previous Purchases */} - {approvalDetail.previousPurchases.length > 0 && ( - -
- {approvalDetail.previousPurchases.map((purchase) => ( - - ))} -
-
- )} -
-
- - {/* Nested dialog for Customer Information */} - {customerRecord && ( - - )} - - ); -} diff --git a/services/platform/app/features/approvals/components/approvals-navigation.tsx b/services/platform/app/features/approvals/components/approvals-navigation.tsx deleted file mode 100644 index a2c7c4201f..0000000000 --- a/services/platform/app/features/approvals/components/approvals-navigation.tsx +++ /dev/null @@ -1,33 +0,0 @@ -'use client'; - -import { - TabNavigation, - type TabNavigationItem, -} from '@/app/components/ui/navigation/tab-navigation'; -import { useT } from '@/lib/i18n/client'; - -interface ApprovalsNavigationProps { - organizationId: string; -} - -const STATUSES = ['pending', 'resolved'] as const; - -export function ApprovalsNavigation({ - organizationId, -}: ApprovalsNavigationProps) { - const { t } = useT('approvals'); - - const navigationItems: TabNavigationItem[] = STATUSES.map((status) => ({ - label: t(`status.${status}`), - href: `/dashboard/${organizationId}/approvals/${status}`, - })); - - return ( - - ); -} diff --git a/services/platform/app/features/approvals/components/approvals.tsx b/services/platform/app/features/approvals/components/approvals.tsx deleted file mode 100644 index ddd05b184a..0000000000 --- a/services/platform/app/features/approvals/components/approvals.tsx +++ /dev/null @@ -1,221 +0,0 @@ -'use client'; - -import type { UsePaginatedQueryResult } from 'convex/react'; - -import { GitCompare } from 'lucide-react'; -import { useState, useCallback, useMemo } from 'react'; - -import type { Doc } from '@/convex/_generated/dataModel'; - -import { DataTable } from '@/app/components/ui/data-table/data-table'; -import { useListPage } from '@/app/hooks/use-list-page'; -import { toast } from '@/app/hooks/use-toast'; -import { toId } from '@/convex/lib/type_cast_helpers'; -import { useT } from '@/lib/i18n/client'; - -import { - useRemoveRecommendedProduct, - useUpdateApprovalStatus, -} from '../hooks/mutations'; -import { ApprovalDetailDialog } from './approval-detail-dialog'; -import { getApprovalDetail } from './approvals/get-approval-detail'; -import { useApprovalColumns } from './approvals/use-approval-columns'; - -type ApprovalItem = Doc<'approvals'>; - -interface ApprovalsProps { - status?: 'pending' | 'resolved'; - search?: string; - paginatedResult: UsePaginatedQueryResult; - approxCount?: number; -} - -export function Approvals({ - status, - search, - paginatedResult, - approxCount, -}: ApprovalsProps) { - const { t } = useT('approvals'); - - const [approving, setApproving] = useState(null); - const [rejecting, setRejecting] = useState(null); - const [selectedApprovalId, setSelectedApprovalId] = useState( - null, - ); - const [removingProductId, setRemovingProductId] = useState( - null, - ); - const [approvalDetailDialogOpen, setApprovalDetailDialogOpen] = - useState(false); - const pageSize = 30; - - const allApprovals = useMemo(() => { - if (!search) return paginatedResult.results; - const lowerSearch = search.toLowerCase(); - return paginatedResult.results.filter((a) => { - const metadata = a.metadata ?? {}; - const customerName = - typeof metadata['customerName'] === 'string' - ? metadata['customerName'] - : ''; - const customerEmail = - typeof metadata['customerEmail'] === 'string' - ? metadata['customerEmail'] - : ''; - return ( - customerName.toLowerCase().includes(lowerSearch) || - customerEmail.toLowerCase().includes(lowerSearch) - ); - }); - }, [paginatedResult.results, search]); - - const list = useListPage({ - dataSource: { - type: 'paginated', - results: allApprovals, - status: paginatedResult.status, - loadMore: paginatedResult.loadMore, - isLoading: paginatedResult.isLoading, - }, - pageSize, - getRowId: (row) => row._id, - approxRowCount: approxCount, - }); - - const { mutateAsync: updateApprovalStatus } = useUpdateApprovalStatus(); - const { mutateAsync: removeRecommendedProduct } = - useRemoveRecommendedProduct(); - - const handleApprove = useCallback( - async (approvalId: string) => { - setApproving(approvalId); - - try { - await updateApprovalStatus({ - approvalId: toId<'approvals'>(approvalId), - status: 'approved', - comments: 'Approved via UI', - }); - } catch (error) { - console.error('Failed to approve:', error); - toast({ - title: t('toast.approveFailed'), - variant: 'destructive', - }); - } finally { - setApproving(null); - } - }, - [updateApprovalStatus, t], - ); - - const handleReject = useCallback( - async (approvalId: string) => { - setRejecting(approvalId); - - try { - await updateApprovalStatus({ - approvalId: toId<'approvals'>(approvalId), - status: 'rejected', - comments: 'Rejected via UI', - }); - } catch (error) { - console.error('Failed to reject:', error); - toast({ - title: t('toast.rejectFailed'), - variant: 'destructive', - }); - } finally { - setRejecting(null); - } - }, - [updateApprovalStatus, t], - ); - - const handleRemoveRecommendation = useCallback( - async (approvalId: string, productId: string) => { - setRemovingProductId(productId); - - try { - await removeRecommendedProduct({ - approvalId: toId<'approvals'>(approvalId), - productId, - }); - } catch (error) { - console.error('Failed to remove recommendation:', error); - toast({ - title: t('toast.removeRecommendationFailed'), - variant: 'destructive', - }); - } finally { - setRemovingProductId(null); - } - }, - [removeRecommendedProduct, t], - ); - - const handleApprovalRowClick = useCallback((approvalId: string) => { - setSelectedApprovalId(approvalId); - setApprovalDetailDialogOpen(true); - }, []); - - const handleApprovalDetailOpenChange = useCallback((open: boolean) => { - setApprovalDetailDialogOpen(open); - if (!open) { - setSelectedApprovalId(null); - } - }, []); - - const { pendingColumns, resolvedColumns } = useApprovalColumns({ - approving, - rejecting, - onApprove: handleApprove, - onReject: handleReject, - }); - - const selectedApprovalDetail = useMemo(() => { - if (!selectedApprovalId || !allApprovals) return null; - const approval = allApprovals.find( - (a: ApprovalItem) => a._id === selectedApprovalId, - ); - if (!approval) return null; - return getApprovalDetail(approval); - }, [selectedApprovalId, allApprovals]); - - const columns = status === 'pending' ? pendingColumns : resolvedColumns; - - return ( - <> - handleApprovalRowClick(row.original._id)} - rowClassName="cursor-pointer" - emptyState={{ - icon: GitCompare, - title: - status === 'pending' - ? t('emptyState.pending.title') - : t('emptyState.resolved.title'), - description: - status === 'pending' - ? t('emptyState.pending.description') - : undefined, - }} - {...list.tableProps} - /> - - - ); -} diff --git a/services/platform/app/features/approvals/components/approvals/get-approval-detail.ts b/services/platform/app/features/approvals/components/approvals/get-approval-detail.ts deleted file mode 100644 index d41557ff2d..0000000000 --- a/services/platform/app/features/approvals/components/approvals/get-approval-detail.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { ApprovalDetail } from '@/app/features/approvals/types/approval-detail'; -import type { Doc } from '@/convex/_generated/dataModel'; - -import { toId } from '@/convex/lib/type_cast_helpers'; -import { - safeGetString, - safeGetNumber, - safeGetArray, -} from '@/lib/utils/safe-parsers'; - -type ApprovalItem = Doc<'approvals'>; - -export function getApprovalDetail(approval: ApprovalItem): ApprovalDetail { - const metadata: Record = approval.metadata ?? {}; - - const recommendedProducts = safeGetArray( - metadata, - 'recommendedProducts', - [], - ).map((product, index: number) => { - const id = safeGetString(product, 'productId', `rec-${index}`); - const name = safeGetString(product, 'productName', ''); - const image = safeGetString( - product, - 'imageUrl', - '/assets/placeholder-image.png', - ); - const relationshipType = safeGetString( - product, - 'relationshipType', - undefined, - ); - const reasoning = safeGetString(product, 'reasoning', undefined); - const confidence = safeGetNumber(product, 'confidence', undefined); - return { id, name, image, relationshipType, reasoning, confidence }; - }); - - const previousPurchases = safeGetArray(metadata, 'eventProducts', []).map( - (product, index: number) => { - const id = safeGetString(product, 'id', `prev-${index}`); - const productName = - safeGetString(product, 'productName', '') || - safeGetString(product, 'name', '') || - safeGetString(product, 'product_name', ''); - const image = - safeGetString(product, 'image', '') || - safeGetString(product, 'imageUrl', '') || - safeGetString(product, 'image_url', '') || - '/assets/placeholder-image.png'; - const purchaseDate = safeGetString(product, 'purchaseDate', undefined); - const statusValue = safeGetString(product, 'status', undefined); - const purchaseStatus: 'active' | 'cancelled' | undefined = - statusValue === 'active' || statusValue === 'cancelled' - ? statusValue - : undefined; - return { - id, - productName, - image, - purchaseDate, - status: purchaseStatus, - }; - }, - ); - - const metaConfidence = (() => { - const raw = - typeof metadata['confidence'] === 'number' - ? metadata['confidence'] - : undefined; - if (typeof raw !== 'number' || !Number.isFinite(raw)) return undefined; - return raw <= 1 ? Math.round(raw * 100) : Math.round(raw); - })(); - - const customerId = safeGetString(metadata, 'customerId', undefined); - - return { - _id: approval._id, - organizationId: approval.organizationId, - customer: { - id: customerId ? toId<'customers'>(customerId) : undefined, - name: - typeof metadata['customerName'] === 'string' - ? metadata['customerName'].trim() - : '', - email: - typeof metadata['customerEmail'] === 'string' - ? metadata['customerEmail'] - : '', - }, - resourceType: approval.resourceType, - status: approval.status, - priority: approval.priority, - confidence: metaConfidence, - createdAt: approval._creationTime, - reviewer: safeGetString(metadata, 'approverName', undefined), - reviewedAt: approval.reviewedAt, - decidedAt: approval.reviewedAt, - comments: safeGetString(metadata, 'comments', undefined), - recommendedProducts, - previousPurchases, - }; -} diff --git a/services/platform/app/features/approvals/components/approvals/product-list-cell.tsx b/services/platform/app/features/approvals/components/approvals/product-list-cell.tsx deleted file mode 100644 index 44c7dbb87a..0000000000 --- a/services/platform/app/features/approvals/components/approvals/product-list-cell.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { Image } from '@/app/components/ui/data-display/image'; -import { Stack, HStack } from '@/app/components/ui/layout/layout'; -import { Text } from '@/app/components/ui/typography/text'; -import { useT } from '@/lib/i18n/client'; -import { safeGetString, safeGetNumber } from '@/lib/utils/safe-parsers'; - -interface ProductListCellProps { - products: unknown; - isRecommendation?: boolean; -} - -export function ProductListCell({ - products, - isRecommendation = false, -}: ProductListCellProps) { - const { t } = useT('approvals'); - const list = Array.isArray(products) ? products : []; - - if (list.length === 0) return null; - - if (isRecommendation) { - return ; - } - - return ( - - {list.map((p, index) => { - const id = - safeGetString(p, 'productId', '') || - safeGetString(p, 'id', '') || - String(index); - const name = - safeGetString(p, 'name', '') || safeGetString(p, 'productName', ''); - const image = - safeGetString(p, 'image', '') || - safeGetString(p, 'imageUrl', '') || - '/assets/placeholder-image.png'; - - return ( - -
- {name} -
- - {name} - -
- ); - })} -
- ); -} - -function RecommendationProductList({ - products, - t, -}: { - products: unknown[]; - t: ReturnType['t']; -}) { - const sortedList = [...products].sort((a, b) => { - const confA = safeGetNumber(a, 'confidence', 0) ?? 0; - const confB = safeGetNumber(b, 'confidence', 0) ?? 0; - return confB - confA; - }); - - const firstProduct = sortedList[0]; - const remainingCount = sortedList.length - 1; - const secondProduct = sortedList[1]; - - const firstName = - safeGetString(firstProduct, 'name', '') || - safeGetString(firstProduct, 'productName', ''); - const firstImage = - safeGetString(firstProduct, 'image', '') || - safeGetString(firstProduct, 'imageUrl', '') || - '/assets/placeholder-image.png'; - - return ( - - -
- {firstName} -
- - {firstName} - -
- {remainingCount > 0 && secondProduct != null && ( - -
- { -
- - {t('labels.otherProducts', { count: remainingCount })} - -
- )} -
- ); -} diff --git a/services/platform/app/features/approvals/components/approvals/use-approval-columns.tsx b/services/platform/app/features/approvals/components/approvals/use-approval-columns.tsx deleted file mode 100644 index 91cd864728..0000000000 --- a/services/platform/app/features/approvals/components/approvals/use-approval-columns.tsx +++ /dev/null @@ -1,317 +0,0 @@ -import type { ColumnDef } from '@tanstack/react-table'; - -import { CheckIcon, Info, Loader2, X } from 'lucide-react'; -import { useMemo, useCallback } from 'react'; - -import type { Doc } from '@/convex/_generated/dataModel'; - -import { CellErrorBoundary } from '@/app/components/error-boundaries/boundaries/cell-error-boundary'; -import { Button } from '@/app/components/ui/primitives/button'; -import { Text } from '@/app/components/ui/typography/text'; -import { useFormatDate } from '@/app/hooks/use-format-date'; -import { useT } from '@/lib/i18n/client'; -import { - safeGetString, - safeGetNumber, - safeGetArray, -} from '@/lib/utils/safe-parsers'; - -import { ProductListCell } from './product-list-cell'; - -type ApprovalItem = Doc<'approvals'>; -type ApprovalsT = ReturnType>['t']; - -interface UseApprovalColumnsParams { - approving: string | null; - rejecting: string | null; - onApprove: (approvalId: string) => void; - onReject: (approvalId: string) => void; -} - -function createSharedColumns( - t: ApprovalsT, - getCustomerLabel: (approval: ApprovalItem) => string, - getLabel: (resourceType: string) => string, -): ColumnDef[] { - return [ - { - id: 'approval', - header: t('columns.approvalRecipient'), - size: 256, - meta: { skeleton: { type: 'avatar-text' as const } }, - cell: ({ row }) => ( -
-
- - {getLabel(row.original.resourceType)} - - -
- - {getCustomerLabel(row.original)} - -
- ), - }, - { - id: 'event', - header: t('columns.event'), - size: 256, - meta: { skeleton: { type: 'text' as const } }, - cell: ({ row }) => { - const metadata = row.original.metadata ?? {}; - return ( -
- - {t('labels.purchase')} - - - — - - } - > - - -
- ); - }, - }, - { - id: 'action', - header: t('columns.action'), - size: 256, - meta: { skeleton: { type: 'text' as const } }, - cell: ({ row }) => { - const metadata = row.original.metadata ?? {}; - return ( -
- - {t('labels.recommendation')} - - - — - - } - > - - -
- ); - }, - }, - ]; -} - -export function useApprovalColumns({ - approving, - rejecting, - onApprove, - onReject, -}: UseApprovalColumnsParams) { - const { t } = useT('approvals'); - const { formatDate } = useFormatDate(); - - const getApprovalTypeLabel = useCallback( - (resourceType: string): string => { - switch (resourceType) { - case 'conversations': - return t('types.reviewReply'); - case 'product_recommendation': - return t('types.recommendProduct'); - case 'workflow_run': - return t('types.runWorkflow'); - case 'workflow_update': - return t('types.updateWorkflow'); - default: - return t('types.review'); - } - }, - [t], - ); - - const getCustomerLabel = useCallback( - (approval: ApprovalItem) => { - const metadata = approval.metadata ?? {}; - return ( - safeGetString(metadata, 'customerName', '').trim() || - safeGetString(metadata, 'customerEmail', '').trim() || - t('columns.unknownCustomer') - ); - }, - [t], - ); - - const getConfidencePercent = useCallback((approval: ApprovalItem) => { - const metadata = approval.metadata ?? {}; - const recs = safeGetArray(metadata, 'recommendedProducts', []); - const firstConf = - recs.length > 0 ? safeGetNumber(recs[0], 'confidence', 0) : 0; - const raw = safeGetNumber(metadata, 'confidence', firstConf); - const n = Number(raw); - return !Number.isFinite(n) - ? 0 - : n <= 1 - ? Math.round(n * 100) - : Math.round(n); - }, []); - - const pendingColumns = useMemo( - (): ColumnDef[] => [ - ...createSharedColumns(t, getCustomerLabel, getApprovalTypeLabel), - { - id: 'confidence', - header: () => ( - - {t('columns.confidence')} - - ), - size: 100, - meta: { headerLabel: t('columns.confidence') }, - cell: ({ row }) => ( - - {getConfidencePercent(row.original)}% - - ), - }, - { - id: 'actions', - header: () => ( - - {t('columns.approved')} - - ), - size: 100, - meta: { - headerLabel: t('columns.approved'), - skeleton: { type: 'action' as const }, - }, - cell: ({ row }) => ( -
- - -
- ), - }, - ], - [ - t, - getApprovalTypeLabel, - getCustomerLabel, - getConfidencePercent, - approving, - rejecting, - onApprove, - onReject, - ], - ); - - const resolvedColumns = useMemo( - (): ColumnDef[] => [ - ...createSharedColumns(t, getCustomerLabel, () => - t('types.recommendProduct'), - ), - { - id: 'reviewer', - header: t('columns.reviewer'), - meta: { skeleton: { type: 'text' as const } }, - cell: ({ row }) => { - const metadata = row.original.metadata ?? {}; - return ( - - {safeGetString(metadata, 'approverName', '') || - t('columns.unknown')} - - ); - }, - }, - { - id: 'reviewedAt', - header: () => ( - - {t('columns.reviewedAt')} - - ), - meta: { headerLabel: t('columns.reviewedAt') }, - cell: ({ row }) => ( - - {row.original.reviewedAt - ? formatDate(new Date(row.original.reviewedAt), 'short') - : ''} - - ), - }, - { - id: 'status', - header: () => ( - - {t('columns.approved')} - - ), - size: 100, - meta: { - headerLabel: t('columns.approved'), - skeleton: { type: 'action' as const }, - }, - cell: ({ row }) => ( -
- {row.original.status === 'approved' ? ( - - ) : ( - - )} -
- ), - }, - ], - [t, getCustomerLabel, formatDate], - ); - - return { pendingColumns, resolvedColumns }; -} diff --git a/services/platform/app/features/approvals/components/product-card.tsx b/services/platform/app/features/approvals/components/product-card.tsx deleted file mode 100644 index 87ac74f944..0000000000 --- a/services/platform/app/features/approvals/components/product-card.tsx +++ /dev/null @@ -1,144 +0,0 @@ -'use client'; - -import { X } from 'lucide-react'; - -import { Image } from '@/app/components/ui/data-display/image'; -import { Badge } from '@/app/components/ui/feedback/badge'; -import { Stack, HStack } from '@/app/components/ui/layout/layout'; -import { Button } from '@/app/components/ui/primitives/button'; -import { Heading } from '@/app/components/ui/typography/heading'; -import { Text } from '@/app/components/ui/typography/text'; -import { useFormatDate } from '@/app/hooks/use-format-date'; -import { useT } from '@/lib/i18n/client'; - -import { RecommendedProduct, PreviousPurchase } from '../types/approval-detail'; - -interface ProductCardProps { - product?: RecommendedProduct; - purchase?: PreviousPurchase; - type: 'recommended' | 'purchase'; - onRemove?: (productId: string) => void; - isRemoving?: boolean; - canRemove?: boolean; -} - -export function ProductCard({ - product, - purchase, - type, - onRemove, - isRemoving, - canRemove, -}: ProductCardProps) { - const { formatDate } = useFormatDate(); - const { t } = useT('approvals'); - - if (type === 'recommended' && product) { - return ( - -
- {product.name} -
-
- - - - {product.name} - - {product.description && ( - - {product.description} - - )} - {product.reasoning && ( - - {product.reasoning} - - )} - - - {product.relationshipType && ( - {product.relationshipType} - )} - {product.confidence !== undefined && ( - - {t('confidenceBadge', { - percent: Math.round(product.confidence * 100), - })} - - )} - - -
- {canRemove && onRemove && ( - - )} -
- ); - } - - if (type === 'purchase' && purchase) { - return ( - - -
- {purchase.productName} -
- - - {purchase.productName} - - {purchase.purchaseDate && ( - {formatDate(purchase.purchaseDate)} - )} - -
- {purchase.status && ( - - {purchase.status === 'active' - ? t('productStatus.active') - : t('productStatus.cancelled')} - - )} -
- ); - } - - return null; -} diff --git a/services/platform/app/features/approvals/hooks/actions.ts b/services/platform/app/features/approvals/hooks/actions.ts deleted file mode 100644 index 6cad229539..0000000000 --- a/services/platform/app/features/approvals/hooks/actions.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useConvexAction } from '@/app/hooks/use-convex-action'; -import { api } from '@/convex/_generated/api'; - -export function useExecuteApprovedIntegrationOperation() { - return useConvexAction( - api.approvals.actions.executeApprovedIntegrationOperation, - ); -} - -export function useExecuteApprovedWorkflowCreation() { - return useConvexAction(api.approvals.actions.executeApprovedWorkflowCreation); -} - -export function useExecuteApprovedWorkflowRun() { - return useConvexAction(api.approvals.actions.executeApprovedWorkflowRun); -} - -export function useExecuteApprovedWorkflowUpdate() { - return useConvexAction(api.approvals.actions.executeApprovedWorkflowUpdate); -} diff --git a/services/platform/app/features/approvals/hooks/mutations.ts b/services/platform/app/features/approvals/hooks/mutations.ts deleted file mode 100644 index f34667d35c..0000000000 --- a/services/platform/app/features/approvals/hooks/mutations.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useConvexMutation } from '@/app/hooks/use-convex-mutation'; -import { api } from '@/convex/_generated/api'; - -export function useRemoveRecommendedProduct() { - return useConvexMutation(api.approvals.mutations.removeRecommendedProduct); -} - -export function useUpdateApprovalStatus() { - return useConvexMutation(api.approvals.mutations.updateApprovalStatus); -} diff --git a/services/platform/app/features/approvals/hooks/queries.ts b/services/platform/app/features/approvals/hooks/queries.ts deleted file mode 100644 index 8469469276..0000000000 --- a/services/platform/app/features/approvals/hooks/queries.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { ConvexItemOf } from '@/lib/types/convex-helpers'; - -import { useCachedPaginatedQuery } from '@/app/hooks/use-cached-paginated-query'; -import { useConvexQuery } from '@/app/hooks/use-convex-query'; -import { api } from '@/convex/_generated/api'; - -export type Approval = ConvexItemOf< - typeof api.approvals.queries.listApprovalsByOrganization ->; - -export function useApproxApprovalCountByStatus( - organizationId: string, - status: 'pending' | 'resolved', -) { - return useConvexQuery(api.approvals.queries.approxCountApprovalsByStatus, { - organizationId, - status, - }); -} - -export function useApprovals(organizationId: string) { - const { data, isLoading } = useConvexQuery( - api.approvals.queries.listApprovalsByOrganization, - { organizationId }, - ); - - return { - approvals: data ?? [], - isLoading, - }; -} - -interface ListApprovalsPaginatedArgs { - organizationId: string; - status?: 'pending' | 'approved' | 'rejected'; - resourceType?: - | 'conversations' - | 'product_recommendation' - | 'integration_operation' - | 'workflow_creation' - | 'workflow_run' - | 'human_input_request'; - excludeStatus?: 'pending' | 'approved' | 'rejected'; - initialNumItems: number; -} - -export function useListApprovalsPaginated(args: ListApprovalsPaginatedArgs) { - const { initialNumItems, ...queryArgs } = args; - return useCachedPaginatedQuery( - api.approvals.queries.listApprovalsPaginated, - queryArgs, - { initialNumItems }, - ); -} diff --git a/services/platform/app/features/approvals/types/approval-detail.ts b/services/platform/app/features/approvals/types/approval-detail.ts deleted file mode 100644 index db882e558b..0000000000 --- a/services/platform/app/features/approvals/types/approval-detail.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { Id } from '@/convex/_generated/dataModel'; - -/** - * Recommended product for UI display - */ -export interface RecommendedProduct { - id: string; - name: string; - image: string; - description?: string; - relationshipType?: string; - reasoning?: string; - confidence?: number; -} - -/** - * Previous purchase for UI display - */ -export interface PreviousPurchase { - id: string; - productName: string; - image: string; - purchaseDate?: string; - status?: 'active' | 'cancelled'; -} - -/** - * Enriched approval detail for UI display. - * This is derived from Doc<'approvals'> with processed/formatted data. - */ -export interface ApprovalDetail { - _id: string; - organizationId: string; - customer: { - id?: Id<'customers'>; - name: string; - email: string; - }; - resourceType: string; - status: 'pending' | 'approved' | 'rejected'; - priority: 'low' | 'medium' | 'high' | 'urgent'; - confidence?: number; - createdAt: number; - reviewer?: string; - reviewedAt?: number; - decidedAt?: number; - comments?: string; - recommendedProducts: RecommendedProduct[]; - previousPurchases: PreviousPurchase[]; -} diff --git a/services/platform/app/features/chat/components/integration-approval-card.tsx b/services/platform/app/features/chat/components/integration-approval-card.tsx index 048ce8e31a..5c35f59a4f 100644 --- a/services/platform/app/features/chat/components/integration-approval-card.tsx +++ b/services/platform/app/features/chat/components/integration-approval-card.tsx @@ -16,8 +16,10 @@ import { HStack, Stack } from '@/app/components/ui/layout/layout'; import { Tooltip } from '@/app/components/ui/overlays/tooltip'; import { Button } from '@/app/components/ui/primitives/button'; import { Text } from '@/app/components/ui/typography/text'; -import { useExecuteApprovedIntegrationOperation } from '@/app/features/approvals/hooks/actions'; -import { useUpdateApprovalStatus } from '@/app/features/approvals/hooks/mutations'; +import { + useExecuteApprovedIntegrationOperation, + useUpdateApprovalStatus, +} from '@/app/features/chat/hooks/mutations'; import { useAuth } from '@/app/hooks/use-convex-auth'; import { Id } from '@/convex/_generated/dataModel'; import { IntegrationOperationMetadata } from '@/convex/approvals/types'; diff --git a/services/platform/app/features/chat/components/workflow-creation-approval-card.tsx b/services/platform/app/features/chat/components/workflow-creation-approval-card.tsx index 4905d0a7a6..de88130903 100644 --- a/services/platform/app/features/chat/components/workflow-creation-approval-card.tsx +++ b/services/platform/app/features/chat/components/workflow-creation-approval-card.tsx @@ -20,8 +20,10 @@ import { HStack, Stack } from '@/app/components/ui/layout/layout'; import { Tooltip } from '@/app/components/ui/overlays/tooltip'; import { Button } from '@/app/components/ui/primitives/button'; import { Text } from '@/app/components/ui/typography/text'; -import { useExecuteApprovedWorkflowCreation } from '@/app/features/approvals/hooks/actions'; -import { useUpdateApprovalStatus } from '@/app/features/approvals/hooks/mutations'; +import { + useExecuteApprovedWorkflowCreation, + useUpdateApprovalStatus, +} from '@/app/features/chat/hooks/mutations'; import { useAuth } from '@/app/hooks/use-convex-auth'; import { useCopyButton } from '@/app/hooks/use-copy'; import { Id } from '@/convex/_generated/dataModel'; diff --git a/services/platform/app/features/chat/components/workflow-run-approval-card.tsx b/services/platform/app/features/chat/components/workflow-run-approval-card.tsx index 8c30ccf426..f714216a2f 100644 --- a/services/platform/app/features/chat/components/workflow-run-approval-card.tsx +++ b/services/platform/app/features/chat/components/workflow-run-approval-card.tsx @@ -22,8 +22,10 @@ import { HStack, Stack } from '@/app/components/ui/layout/layout'; import { Tooltip } from '@/app/components/ui/overlays/tooltip'; import { Button } from '@/app/components/ui/primitives/button'; import { Text } from '@/app/components/ui/typography/text'; -import { useExecuteApprovedWorkflowRun } from '@/app/features/approvals/hooks/actions'; -import { useUpdateApprovalStatus } from '@/app/features/approvals/hooks/mutations'; +import { + useExecuteApprovedWorkflowRun, + useUpdateApprovalStatus, +} from '@/app/features/chat/hooks/mutations'; import { useCancelExecution, useExecutionStatus, diff --git a/services/platform/app/features/chat/components/workflow-update-approval-card.tsx b/services/platform/app/features/chat/components/workflow-update-approval-card.tsx index cbf22d51c3..b8a596f0e4 100644 --- a/services/platform/app/features/chat/components/workflow-update-approval-card.tsx +++ b/services/platform/app/features/chat/components/workflow-update-approval-card.tsx @@ -22,8 +22,10 @@ import { HStack, Stack } from '@/app/components/ui/layout/layout'; import { Tooltip } from '@/app/components/ui/overlays/tooltip'; import { Button } from '@/app/components/ui/primitives/button'; import { Text } from '@/app/components/ui/typography/text'; -import { useExecuteApprovedWorkflowUpdate } from '@/app/features/approvals/hooks/actions'; -import { useUpdateApprovalStatus } from '@/app/features/approvals/hooks/mutations'; +import { + useExecuteApprovedWorkflowUpdate, + useUpdateApprovalStatus, +} from '@/app/features/chat/hooks/mutations'; import { useAuth } from '@/app/hooks/use-convex-auth'; import { useCopyButton } from '@/app/hooks/use-copy'; import { Id } from '@/convex/_generated/dataModel'; diff --git a/services/platform/app/features/chat/hooks/mutations.ts b/services/platform/app/features/chat/hooks/mutations.ts index 5da99958b3..5d5818a327 100644 --- a/services/platform/app/features/chat/hooks/mutations.ts +++ b/services/platform/app/features/chat/hooks/mutations.ts @@ -1,3 +1,4 @@ +import { useConvexAction } from '@/app/hooks/use-convex-action'; import { useConvexMutation } from '@/app/hooks/use-convex-mutation'; import { api } from '@/convex/_generated/api'; @@ -11,6 +12,28 @@ export function useSubmitHumanInputResponse() { ); } +export function useUpdateApprovalStatus() { + return useConvexMutation(api.approvals.mutations.updateApprovalStatus); +} + +export function useExecuteApprovedIntegrationOperation() { + return useConvexAction( + api.approvals.actions.executeApprovedIntegrationOperation, + ); +} + +export function useExecuteApprovedWorkflowCreation() { + return useConvexAction(api.approvals.actions.executeApprovedWorkflowCreation); +} + +export function useExecuteApprovedWorkflowRun() { + return useConvexAction(api.approvals.actions.executeApprovedWorkflowRun); +} + +export function useExecuteApprovedWorkflowUpdate() { + return useConvexAction(api.approvals.actions.executeApprovedWorkflowUpdate); +} + export function useCreateThread() { return useConvexMutation(api.threads.mutations.createChatThread); } diff --git a/services/platform/app/features/chat/hooks/queries.ts b/services/platform/app/features/chat/hooks/queries.ts index 33426d19b9..febf078d9a 100644 --- a/services/platform/app/features/chat/hooks/queries.ts +++ b/services/platform/app/features/chat/hooks/queries.ts @@ -10,7 +10,6 @@ import type { import type { HumanInputRequestMetadata } from '@/lib/shared/schemas/approvals'; import type { ConvexItemOf } from '@/lib/types/convex-helpers'; -import { useApprovals } from '@/app/features/approvals/hooks/queries'; import { useCachedPaginatedQuery } from '@/app/hooks/use-cached-paginated-query'; import { useConvexQuery } from '@/app/hooks/use-convex-query'; import { useTeamFilter } from '@/app/hooks/use-team-filter'; @@ -112,6 +111,18 @@ export function useThreadMessages(threadId: string | null) { return results; } +function useApprovals(organizationId: string) { + const { data, isLoading } = useConvexQuery( + api.approvals.queries.listApprovalsByOrganization, + { organizationId }, + ); + + return { + approvals: data ?? [], + isLoading, + }; +} + export interface HumanInputRequest { _id: Id<'approvals'>; status: 'pending' | 'approved' | 'rejected'; diff --git a/services/platform/app/hooks/use-navigation-items.ts b/services/platform/app/hooks/use-navigation-items.ts index 80e00b9502..2194a5717c 100644 --- a/services/platform/app/hooks/use-navigation-items.ts +++ b/services/platform/app/hooks/use-navigation-items.ts @@ -1,13 +1,6 @@ 'use client'; -import { - MessageCircle, - CircleCheck, - Inbox, - BrainIcon, - Network, - Bot, -} from 'lucide-react'; +import { MessageCircle, Inbox, BrainIcon, Network, Bot } from 'lucide-react'; import { useMemo } from 'react'; import { useT } from '@/lib/i18n/client'; @@ -29,8 +22,6 @@ export function useNavigationItems(businessId: string): NavItem[] { const { t: tNav } = useT('navigation'); const { t: tKnowledge } = useT('knowledge'); const { t: tConversations } = useT('conversations'); - const { t: tApprovals } = useT('approvals'); - return useMemo( (): NavItem[] => [ { @@ -112,27 +103,6 @@ export function useNavigationItems(businessId: string): NavItem[] { }, ], }, - { - label: tNav('approvals'), - to: '/dashboard/$id/approvals/$status', - params: { id: businessId, status: 'pending' }, - href: `/dashboard/${businessId}/approvals/pending`, - icon: CircleCheck, - subItems: [ - { - label: tApprovals('status.pending'), - to: '/dashboard/$id/approvals/$status', - params: { id: businessId, status: 'pending' }, - href: `/dashboard/${businessId}/approvals/pending`, - }, - { - label: tApprovals('status.resolved'), - to: '/dashboard/$id/approvals/$status', - params: { id: businessId, status: 'resolved' }, - href: `/dashboard/${businessId}/approvals/resolved`, - }, - ], - }, { label: tNav('customAgents'), to: '/dashboard/$id/custom-agents', @@ -150,6 +120,6 @@ export function useNavigationItems(businessId: string): NavItem[] { can: ['write', 'wfDefinitions'], }, ], - [businessId, tNav, tKnowledge, tConversations, tApprovals], + [businessId, tNav, tKnowledge, tConversations], ); } diff --git a/services/platform/app/routeTree.gen.ts b/services/platform/app/routeTree.gen.ts index e963cd4bc4..0f61895b8f 100644 --- a/services/platform/app/routeTree.gen.ts +++ b/services/platform/app/routeTree.gen.ts @@ -25,7 +25,6 @@ import { Route as DashboardIdCustomAgentsRouteImport } from './routes/dashboard/ import { Route as DashboardIdConversationsRouteImport } from './routes/dashboard/$id/conversations' import { Route as DashboardIdChatRouteImport } from './routes/dashboard/$id/chat' import { Route as DashboardIdAutomationsRouteImport } from './routes/dashboard/$id/automations' -import { Route as DashboardIdApprovalsRouteImport } from './routes/dashboard/$id/approvals' import { Route as DashboardIdKnowledgeRouteImport } from './routes/dashboard/$id/_knowledge' import { Route as DashboardIdSettingsIndexRouteImport } from './routes/dashboard/$id/settings/index' import { Route as DashboardIdCustomAgentsIndexRouteImport } from './routes/dashboard/$id/custom-agents/index' @@ -43,7 +42,6 @@ import { Route as DashboardIdCustomAgentsAgentIdRouteImport } from './routes/das import { Route as DashboardIdConversationsStatusRouteImport } from './routes/dashboard/$id/conversations/$status' import { Route as DashboardIdChatThreadIdRouteImport } from './routes/dashboard/$id/chat/$threadId' import { Route as DashboardIdAutomationsAmIdRouteImport } from './routes/dashboard/$id/automations/$amId' -import { Route as DashboardIdApprovalsStatusRouteImport } from './routes/dashboard/$id/approvals/$status' import { Route as DashboardIdKnowledgeWebsitesRouteImport } from './routes/dashboard/$id/_knowledge/websites' import { Route as DashboardIdKnowledgeVendorsRouteImport } from './routes/dashboard/$id/_knowledge/vendors' import { Route as DashboardIdKnowledgeProductsRouteImport } from './routes/dashboard/$id/_knowledge/products' @@ -140,11 +138,6 @@ const DashboardIdAutomationsRoute = DashboardIdAutomationsRouteImport.update({ path: '/automations', getParentRoute: () => DashboardIdRoute, } as any) -const DashboardIdApprovalsRoute = DashboardIdApprovalsRouteImport.update({ - id: '/approvals', - path: '/approvals', - getParentRoute: () => DashboardIdRoute, -} as any) const DashboardIdKnowledgeRoute = DashboardIdKnowledgeRouteImport.update({ id: '/_knowledge', getParentRoute: () => DashboardIdRoute, @@ -242,12 +235,6 @@ const DashboardIdAutomationsAmIdRoute = path: '/$amId', getParentRoute: () => DashboardIdAutomationsRoute, } as any) -const DashboardIdApprovalsStatusRoute = - DashboardIdApprovalsStatusRouteImport.update({ - id: '/$status', - path: '/$status', - getParentRoute: () => DashboardIdApprovalsRoute, - } as any) const DashboardIdKnowledgeWebsitesRoute = DashboardIdKnowledgeWebsitesRouteImport.update({ id: '/websites', @@ -343,7 +330,6 @@ export interface FileRoutesByFullPath { '/dashboard/$id': typeof DashboardIdKnowledgeRouteWithChildren '/dashboard/create-organization': typeof DashboardCreateOrganizationRoute '/dashboard/': typeof DashboardIndexRoute - '/dashboard/$id/approvals': typeof DashboardIdApprovalsRouteWithChildren '/dashboard/$id/automations': typeof DashboardIdAutomationsRouteWithChildren '/dashboard/$id/chat': typeof DashboardIdChatRouteWithChildren '/dashboard/$id/conversations': typeof DashboardIdConversationsRouteWithChildren @@ -355,7 +341,6 @@ export interface FileRoutesByFullPath { '/dashboard/$id/products': typeof DashboardIdKnowledgeProductsRoute '/dashboard/$id/vendors': typeof DashboardIdKnowledgeVendorsRoute '/dashboard/$id/websites': typeof DashboardIdKnowledgeWebsitesRoute - '/dashboard/$id/approvals/$status': typeof DashboardIdApprovalsStatusRoute '/dashboard/$id/automations/$amId': typeof DashboardIdAutomationsAmIdRouteWithChildren '/dashboard/$id/chat/$threadId': typeof DashboardIdChatThreadIdRoute '/dashboard/$id/conversations/$status': typeof DashboardIdConversationsStatusRoute @@ -391,14 +376,12 @@ export interface FileRoutesByTo { '/dashboard/create-organization': typeof DashboardCreateOrganizationRoute '/dashboard': typeof DashboardIndexRoute '/dashboard/$id': typeof DashboardIdIndexRoute - '/dashboard/$id/approvals': typeof DashboardIdApprovalsRouteWithChildren '/dashboard/$id/conversations': typeof DashboardIdConversationsRouteWithChildren '/dashboard/$id/customers': typeof DashboardIdKnowledgeCustomersRoute '/dashboard/$id/documents': typeof DashboardIdKnowledgeDocumentsRoute '/dashboard/$id/products': typeof DashboardIdKnowledgeProductsRoute '/dashboard/$id/vendors': typeof DashboardIdKnowledgeVendorsRoute '/dashboard/$id/websites': typeof DashboardIdKnowledgeWebsitesRoute - '/dashboard/$id/approvals/$status': typeof DashboardIdApprovalsStatusRoute '/dashboard/$id/automations/$amId': typeof DashboardIdAutomationsAmIdRouteWithChildren '/dashboard/$id/chat/$threadId': typeof DashboardIdChatThreadIdRoute '/dashboard/$id/conversations/$status': typeof DashboardIdConversationsStatusRoute @@ -437,7 +420,6 @@ export interface FileRoutesById { '/dashboard/create-organization': typeof DashboardCreateOrganizationRoute '/dashboard/': typeof DashboardIndexRoute '/dashboard/$id/_knowledge': typeof DashboardIdKnowledgeRouteWithChildren - '/dashboard/$id/approvals': typeof DashboardIdApprovalsRouteWithChildren '/dashboard/$id/automations': typeof DashboardIdAutomationsRouteWithChildren '/dashboard/$id/chat': typeof DashboardIdChatRouteWithChildren '/dashboard/$id/conversations': typeof DashboardIdConversationsRouteWithChildren @@ -449,7 +431,6 @@ export interface FileRoutesById { '/dashboard/$id/_knowledge/products': typeof DashboardIdKnowledgeProductsRoute '/dashboard/$id/_knowledge/vendors': typeof DashboardIdKnowledgeVendorsRoute '/dashboard/$id/_knowledge/websites': typeof DashboardIdKnowledgeWebsitesRoute - '/dashboard/$id/approvals/$status': typeof DashboardIdApprovalsStatusRoute '/dashboard/$id/automations/$amId': typeof DashboardIdAutomationsAmIdRouteWithChildren '/dashboard/$id/chat/$threadId': typeof DashboardIdChatThreadIdRoute '/dashboard/$id/conversations/$status': typeof DashboardIdConversationsStatusRoute @@ -488,7 +469,6 @@ export interface FileRouteTypes { | '/dashboard/$id' | '/dashboard/create-organization' | '/dashboard/' - | '/dashboard/$id/approvals' | '/dashboard/$id/automations' | '/dashboard/$id/chat' | '/dashboard/$id/conversations' @@ -500,7 +480,6 @@ export interface FileRouteTypes { | '/dashboard/$id/products' | '/dashboard/$id/vendors' | '/dashboard/$id/websites' - | '/dashboard/$id/approvals/$status' | '/dashboard/$id/automations/$amId' | '/dashboard/$id/chat/$threadId' | '/dashboard/$id/conversations/$status' @@ -536,14 +515,12 @@ export interface FileRouteTypes { | '/dashboard/create-organization' | '/dashboard' | '/dashboard/$id' - | '/dashboard/$id/approvals' | '/dashboard/$id/conversations' | '/dashboard/$id/customers' | '/dashboard/$id/documents' | '/dashboard/$id/products' | '/dashboard/$id/vendors' | '/dashboard/$id/websites' - | '/dashboard/$id/approvals/$status' | '/dashboard/$id/automations/$amId' | '/dashboard/$id/chat/$threadId' | '/dashboard/$id/conversations/$status' @@ -581,7 +558,6 @@ export interface FileRouteTypes { | '/dashboard/create-organization' | '/dashboard/' | '/dashboard/$id/_knowledge' - | '/dashboard/$id/approvals' | '/dashboard/$id/automations' | '/dashboard/$id/chat' | '/dashboard/$id/conversations' @@ -593,7 +569,6 @@ export interface FileRouteTypes { | '/dashboard/$id/_knowledge/products' | '/dashboard/$id/_knowledge/vendors' | '/dashboard/$id/_knowledge/websites' - | '/dashboard/$id/approvals/$status' | '/dashboard/$id/automations/$amId' | '/dashboard/$id/chat/$threadId' | '/dashboard/$id/conversations/$status' @@ -743,13 +718,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DashboardIdAutomationsRouteImport parentRoute: typeof DashboardIdRoute } - '/dashboard/$id/approvals': { - id: '/dashboard/$id/approvals' - path: '/approvals' - fullPath: '/dashboard/$id/approvals' - preLoaderRoute: typeof DashboardIdApprovalsRouteImport - parentRoute: typeof DashboardIdRoute - } '/dashboard/$id/_knowledge': { id: '/dashboard/$id/_knowledge' path: '' @@ -869,13 +837,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DashboardIdAutomationsAmIdRouteImport parentRoute: typeof DashboardIdAutomationsRoute } - '/dashboard/$id/approvals/$status': { - id: '/dashboard/$id/approvals/$status' - path: '/$status' - fullPath: '/dashboard/$id/approvals/$status' - preLoaderRoute: typeof DashboardIdApprovalsStatusRouteImport - parentRoute: typeof DashboardIdApprovalsRoute - } '/dashboard/$id/_knowledge/websites': { id: '/dashboard/$id/_knowledge/websites' path: '/websites' @@ -1008,17 +969,6 @@ const DashboardIdKnowledgeRouteChildren: DashboardIdKnowledgeRouteChildren = { const DashboardIdKnowledgeRouteWithChildren = DashboardIdKnowledgeRoute._addFileChildren(DashboardIdKnowledgeRouteChildren) -interface DashboardIdApprovalsRouteChildren { - DashboardIdApprovalsStatusRoute: typeof DashboardIdApprovalsStatusRoute -} - -const DashboardIdApprovalsRouteChildren: DashboardIdApprovalsRouteChildren = { - DashboardIdApprovalsStatusRoute: DashboardIdApprovalsStatusRoute, -} - -const DashboardIdApprovalsRouteWithChildren = - DashboardIdApprovalsRoute._addFileChildren(DashboardIdApprovalsRouteChildren) - interface DashboardIdAutomationsAmIdRouteChildren { DashboardIdAutomationsAmIdConfigurationRoute: typeof DashboardIdAutomationsAmIdConfigurationRoute DashboardIdAutomationsAmIdExecutionsRoute: typeof DashboardIdAutomationsAmIdExecutionsRoute @@ -1161,7 +1111,6 @@ const DashboardIdSettingsRouteWithChildren = interface DashboardIdRouteChildren { DashboardIdKnowledgeRoute: typeof DashboardIdKnowledgeRouteWithChildren - DashboardIdApprovalsRoute: typeof DashboardIdApprovalsRouteWithChildren DashboardIdAutomationsRoute: typeof DashboardIdAutomationsRouteWithChildren DashboardIdChatRoute: typeof DashboardIdChatRouteWithChildren DashboardIdConversationsRoute: typeof DashboardIdConversationsRouteWithChildren @@ -1172,7 +1121,6 @@ interface DashboardIdRouteChildren { const DashboardIdRouteChildren: DashboardIdRouteChildren = { DashboardIdKnowledgeRoute: DashboardIdKnowledgeRouteWithChildren, - DashboardIdApprovalsRoute: DashboardIdApprovalsRouteWithChildren, DashboardIdAutomationsRoute: DashboardIdAutomationsRouteWithChildren, DashboardIdChatRoute: DashboardIdChatRouteWithChildren, DashboardIdConversationsRoute: DashboardIdConversationsRouteWithChildren, diff --git a/services/platform/app/routes/dashboard/$id/approvals.tsx b/services/platform/app/routes/dashboard/$id/approvals.tsx deleted file mode 100644 index 40284d801c..0000000000 --- a/services/platform/app/routes/dashboard/$id/approvals.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { convexQuery } from '@convex-dev/react-query'; -import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'; - -import { - AdaptiveHeaderRoot, - AdaptiveHeaderTitle, -} from '@/app/components/layout/adaptive-header'; -import { ContentArea } from '@/app/components/layout/content-area'; -import { PageLayout } from '@/app/components/layout/page-layout'; -import { ApprovalsNavigation } from '@/app/features/approvals/components/approvals-navigation'; -import { api } from '@/convex/_generated/api'; -import { useT } from '@/lib/i18n/client'; -import { seo } from '@/lib/utils/seo'; - -export const Route = createFileRoute('/dashboard/$id/approvals')({ - head: () => ({ - meta: seo('approvals'), - }), - beforeLoad: ({ params, location }) => { - if (location.pathname === `/dashboard/${params.id}/approvals`) { - throw redirect({ - to: '/dashboard/$id/approvals/$status', - params: { id: params.id, status: 'pending' }, - }); - } - }, - loader: ({ context, params }) => { - void context.queryClient.prefetchQuery( - convexQuery(api.approvals.queries.approxCountApprovalsByStatus, { - organizationId: params.id, - status: 'resolved', - }), - ); - void context.queryClient.prefetchQuery( - convexQuery(api.approvals.queries.approxCountApprovalsByStatus, { - organizationId: params.id, - status: 'pending', - }), - ); - }, - component: ApprovalsLayout, -}); - -function ApprovalsLayout() { - const { id: organizationId } = Route.useParams(); - const { t } = useT('approvals'); - - return ( - - - {t('title')} - - - - } - > - - - - - ); -} diff --git a/services/platform/app/routes/dashboard/$id/approvals/$status.tsx b/services/platform/app/routes/dashboard/$id/approvals/$status.tsx deleted file mode 100644 index 7bbb60c7d8..0000000000 --- a/services/platform/app/routes/dashboard/$id/approvals/$status.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { convexQuery } from '@convex-dev/react-query'; -import { createFileRoute, notFound } from '@tanstack/react-router'; -import { z } from 'zod'; - -import { Approvals } from '@/app/features/approvals/components/approvals'; -import { - useApproxApprovalCountByStatus, - useListApprovalsPaginated, -} from '@/app/features/approvals/hooks/queries'; -import { api } from '@/convex/_generated/api'; - -const VALID_STATUSES = ['pending', 'resolved'] as const; -type ValidStatus = (typeof VALID_STATUSES)[number]; - -function isValidStatus(value: string): value is ValidStatus { - return VALID_STATUSES.some((s) => s === value); -} - -const searchSchema = z.object({ - search: z.string().optional(), -}); - -export const Route = createFileRoute('/dashboard/$id/approvals/$status')({ - validateSearch: searchSchema, - beforeLoad: ({ params }) => { - if (!isValidStatus(params.status)) { - throw notFound(); - } - }, - loader: ({ context, params }) => { - if (isValidStatus(params.status)) { - const isPending = params.status === 'pending'; - - void context.queryClient.prefetchQuery( - convexQuery(api.approvals.queries.listApprovalsPaginated, { - organizationId: params.id, - ...(isPending - ? { status: 'pending', resourceType: 'product_recommendation' } - : { - resourceType: 'product_recommendation', - excludeStatus: 'pending', - }), - paginationOpts: { numItems: 30, cursor: null }, - }), - ); - void context.queryClient.prefetchQuery( - convexQuery(api.approvals.queries.approxCountApprovalsByStatus, { - organizationId: params.id, - status: params.status, - }), - ); - } - }, - component: ApprovalsStatusPage, -}); - -function ApprovalsStatusPage() { - const { id: organizationId, status } = Route.useParams(); - const { search } = Route.useSearch(); - const isPending = status === 'pending'; - const resolvedStatus: ValidStatus = isPending ? 'pending' : 'resolved'; - - const { data: approxCount } = useApproxApprovalCountByStatus( - organizationId, - resolvedStatus, - ); - - const paginatedResult = useListApprovalsPaginated({ - organizationId, - ...(isPending - ? { status: 'pending', resourceType: 'product_recommendation' } - : { resourceType: 'product_recommendation', excludeStatus: 'pending' }), - initialNumItems: 30, - }); - - return ( - - ); -} diff --git a/services/platform/messages/en.json b/services/platform/messages/en.json index ae2fd12508..d08a5c9687 100644 --- a/services/platform/messages/en.json +++ b/services/platform/messages/en.json @@ -218,7 +218,6 @@ "chatWithAI": "Chat with AI", "conversations": "Conversations", "knowledge": "Knowledge", - "approvals": "Approvals", "automations": "Automations", "settings": "Settings", "settingsAndMore": "Settings and more", @@ -277,10 +276,6 @@ "title": "No examples yet", "description": "Add example messages to train the AI on your tone" }, - "approvals": { - "title": "No approvals found", - "description": "There are no approvals matching your criteria" - }, "conversations": { "title": "No conversations yet", "description": "Conversations will appear here" @@ -2044,77 +2039,6 @@ "connectEmail": "Connect email" } }, - "approvals": { - "title": "Approvals", - "noApprovalsFound": "No approvals found", - "columns": { - "approvalRecipient": "Approval / Recipient", - "event": "Event", - "action": "Action", - "reviewer": "Reviewer", - "reviewedAt": "Reviewed at", - "approved": "Approved", - "confidence": "Confidence", - "unknown": "Unknown", - "unknownCustomer": "Unknown customer" - }, - "types": { - "reviewReply": "Review reply", - "recommendProduct": "Recommend product", - "review": "Review", - "runWorkflow": "Run workflow", - "updateWorkflow": "Update workflow" - }, - "labels": { - "purchase": "Purchase", - "recommendation": "Recommendation", - "otherProducts": "{count, plural, one {# other product} other {# other products}}" - }, - "actions": { - "approve": "Approve", - "reject": "Reject", - "removeProduct": "Remove {name} from recommendations" - }, - "confidenceBadge": "{percent}% confidence", - "productStatus": { - "active": "Active", - "cancelled": "Cancelled" - }, - "status": { - "pending": "Pending", - "resolved": "Resolved" - }, - "emptyState": { - "pending": { - "title": "No pending approvals", - "description": "When human input is needed, your AI will request it here" - }, - "resolved": { - "title": "No resolved approvals" - } - }, - "toast": { - "loginRequired": "You must be logged in to perform this action.", - "approveFailed": "Failed to approve. Please try again.", - "rejectFailed": "Failed to reject. Please try again.", - "removeRecommendationFailed": "Failed to remove recommendation. Please try again." - }, - "detail": { - "title": "Approval details", - "status": "Status", - "statusPending": "Pending", - "statusApproved": "Approved", - "statusRejected": "Rejected", - "type": "Type", - "typeProductRecommendation": "Product recommendation", - "createdAt": "Created at", - "confidence": "Confidence", - "recommendedProducts": "Recommended products", - "userPurchased": "User purchased", - "reject": "Reject", - "approve": "Approve" - } - }, "workflowRunApproval": { "approve": "Run workflow", "reject": "Cancel", @@ -2956,10 +2880,6 @@ "title": "Conversations", "description": "View and manage customer conversations." }, - "approvals": { - "title": "Approvals", - "description": "Review and approve pending actions." - }, "automations": { "title": "Automations", "description": "Manage your automation workflows." From a59037b13617bbead89560f424b6ef2de4c40293 Mon Sep 17 00:00:00 2001 From: larryro <371767072@qq.com> Date: Sun, 15 Mar 2026 19:14:05 +0800 Subject: [PATCH 2/2] fix: remove unused safe-parsers utility No longer referenced after approvals feature removal. --- services/platform/lib/utils/safe-parsers.ts | 73 --------------------- 1 file changed, 73 deletions(-) delete mode 100644 services/platform/lib/utils/safe-parsers.ts diff --git a/services/platform/lib/utils/safe-parsers.ts b/services/platform/lib/utils/safe-parsers.ts deleted file mode 100644 index e7bae3dd99..0000000000 --- a/services/platform/lib/utils/safe-parsers.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Safe parsing utilities for metadata, JSON, and complex data structures. - * - * These functions prevent runtime errors from malformed or unexpected data - * by providing type-safe extraction with fallback values. - * - * Used primarily in: - * - Approvals metadata parsing - * - Complex table cell renderers - * - API response handling - * - * @example - * const metadata = approval.metadata as unknown; - * const customerName = safeGetString(metadata, 'customerName', 'Unknown'); - * const products = safeParseProductList(metadata.recommendedProducts); - */ - -import { isRecord } from './type-guards'; - -/** - * Safely extract a string value from an object. - * - * @param obj - Object to extract from - * @param key - Property key - * @param fallback - Fallback value if extraction fails - * @returns The string value or fallback - */ -export function safeGetString( - obj: unknown, - key: string, - fallback = '', -): string { - if (!isRecord(obj)) return fallback; - const value = obj[key]; - return typeof value === 'string' ? value : fallback; -} - -/** - * Safely extract a number value from an object. - * - * @param obj - Object to extract from - * @param key - Property key - * @param fallback - Fallback value if extraction fails - * @returns The number value or fallback - */ -export function safeGetNumber( - obj: unknown, - key: string, - fallback?: number, -): number | undefined { - if (!isRecord(obj)) return fallback; - const value = obj[key]; - return typeof value === 'number' && Number.isFinite(value) ? value : fallback; -} - -/** - * Safely extract an array from an object. - * - * @param obj - Object to extract from - * @param key - Property key - * @param fallback - Fallback array if extraction fails - * @returns The array value or fallback - */ -export function safeGetArray( - obj: unknown, - key: string, - fallback: T[] = [], -): T[] { - if (!isRecord(obj)) return fallback; - const value = obj[key]; - // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Array.isArray narrows to unknown[]; T[] not inferrable - return Array.isArray(value) ? (value as T[]) : fallback; -}