CRM.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import * as React from 'react';
  2. import { AnalyticsData, Visitor, Conversation } from '../types';
  3. import { format, parseISO } from 'date-fns';
  4. import { Icon } from './ui/Icon';
  5. import { useTranslation } from '../hooks/useI18n';
  6. interface CRMProps {
  7. analyticsData: AnalyticsData;
  8. }
  9. export const CRM: React.FC<CRMProps> = ({ analyticsData }) => {
  10. const { t } = useTranslation();
  11. const [sortConfig, setSortConfig] = React.useState<{ key: keyof Visitor; direction: 'asc' | 'desc' } | null>({ key: 'lastSeen', direction: 'desc' });
  12. const visitors = React.useMemo((): Visitor[] => {
  13. const visitorMap = new Map<string, { lastSeen: Date; visits: Set<string>; conversationCount: number }>();
  14. analyticsData.conversations.forEach(conv => {
  15. if (!visitorMap.has(conv.visitorId)) {
  16. visitorMap.set(conv.visitorId, { lastSeen: new Date(0), visits: new Set(), conversationCount: 0 });
  17. }
  18. const data = visitorMap.get(conv.visitorId)!;
  19. const convDate = new Date(conv.timestamp);
  20. if (convDate > data.lastSeen) {
  21. data.lastSeen = convDate;
  22. }
  23. data.visits.add(conv.timestamp);
  24. data.conversationCount += 1;
  25. });
  26. const visitorList = Array.from(visitorMap.entries()).map(([id, data]) => ({
  27. id,
  28. lastSeen: data.lastSeen.toISOString(),
  29. visitCount: data.visits.size,
  30. conversationCount: data.conversationCount,
  31. }));
  32. return visitorList;
  33. }, [analyticsData.conversations]);
  34. const sortedVisitors = React.useMemo(() => {
  35. const sortableItems = [...visitors];
  36. if (sortConfig !== null) {
  37. sortableItems.sort((a, b) => {
  38. if (a[sortConfig.key] < b[sortConfig.key]) {
  39. return sortConfig.direction === 'asc' ? -1 : 1;
  40. }
  41. if (a[sortConfig.key] > b[sortConfig.key]) {
  42. return sortConfig.direction === 'asc' ? 1 : -1;
  43. }
  44. return 0;
  45. });
  46. }
  47. return sortableItems;
  48. }, [visitors, sortConfig]);
  49. const requestSort = (key: keyof Visitor) => {
  50. let direction: 'asc' | 'desc' = 'asc';
  51. if (sortConfig && sortConfig.key === key && sortConfig.direction === 'asc') {
  52. direction = 'desc';
  53. }
  54. setSortConfig({ key, direction });
  55. };
  56. const SortableHeader: React.FC<{ sortKey: keyof Visitor; children?: React.ReactNode }> = ({ sortKey, children }) => {
  57. const isSorted = sortConfig?.key === sortKey;
  58. const directionIcon = sortConfig?.direction === 'asc' ? '▲' : '▼';
  59. return (
  60. // FIX: The className was truncated. It has been completed.
  61. <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer" onClick={() => requestSort(sortKey)}>
  62. <div className="flex items-center">
  63. {children}
  64. {isSorted && <span className="ml-2">{directionIcon}</span>}
  65. </div>
  66. </th>
  67. );
  68. };
  69. // FIX: The component was not returning any JSX, causing a type error. A return statement with a table has been added.
  70. return (
  71. <div className="p-6 bg-white dark:bg-gray-800 rounded-lg">
  72. <h2 className="text-xl font-bold text-gray-900 dark:text-white mb-4">{t('analytics.crm.title')}</h2>
  73. <div className="overflow-x-auto">
  74. <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
  75. <thead className="bg-gray-50 dark:bg-gray-700">
  76. <tr>
  77. <SortableHeader sortKey="id">{t('analytics.crm.visitor_id')}</SortableHeader>
  78. <SortableHeader sortKey="lastSeen">{t('analytics.crm.last_seen')}</SortableHeader>
  79. <SortableHeader sortKey="visitCount">{t('analytics.crm.visits')}</SortableHeader>
  80. <SortableHeader sortKey="conversationCount">{t('analytics.interactions.conversations')}</SortableHeader>
  81. </tr>
  82. </thead>
  83. <tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
  84. {sortedVisitors.map((visitor) => (
  85. <tr key={visitor.id}>
  86. <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-brand-primary">{visitor.id}</td>
  87. <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{format(parseISO(visitor.lastSeen), 'MMM d, yyyy h:mm a')}</td>
  88. <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{visitor.visitCount}</td>
  89. <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{visitor.conversationCount}</td>
  90. </tr>
  91. ))}
  92. </tbody>
  93. </table>
  94. </div>
  95. </div>
  96. );
  97. };