diff --git a/Customer-chat b/Customer-chat
new file mode 100644
index 0000000..3e2c1c3
--- /dev/null
+++ b/Customer-chat
@@ -0,0 +1,13 @@
+#158 Customer Support Chat Integration
+Repo Avatar
+nathydre21/nepa
+Description*: No integrated support system. Add chat with ticketing functionality.
+
+Technical Requirements:
+
+Integrate third-party chat service (Intercom, Zendesk)
+Create ticket management system with SLA tracking
+Implement chatbot for common queries using NLP
+Add support agent dashboard with user context
+Create knowledge base integration
+Impact: Better customer support, reduced support costs
diff --git a/nepa-frontend/package.json b/nepa-frontend/package.json
index c0650de..8229326 100644
--- a/nepa-frontend/package.json
+++ b/nepa-frontend/package.json
@@ -18,6 +18,7 @@
"@walletconnect/sign-client": "^2.23.6",
"albedo": "^0.1.3",
"date-fns": "^4.1.0",
+ "lucide-react": "^0.479.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"recharts": "^3.7.0"
diff --git a/nepa-frontend/src/App.tsx b/nepa-frontend/src/App.tsx
index 73fef85..58a464d 100644
--- a/nepa-frontend/src/App.tsx
+++ b/nepa-frontend/src/App.tsx
@@ -166,9 +166,22 @@ const AppContent: React.FC = () => {
NEPA Platform
-
-
-
+
+
+
diff --git a/nepa-frontend/src/components/AdvancedDataTable.tsx b/nepa-frontend/src/components/AdvancedDataTable.tsx
index 6ee288f..63120bc 100644
--- a/nepa-frontend/src/components/AdvancedDataTable.tsx
+++ b/nepa-frontend/src/components/AdvancedDataTable.tsx
@@ -1 +1,646 @@
-import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';\nimport { Search, Filter, Download, ChevronUp, ChevronDown, MoreVertical, Eye, Edit, Trash2, ChevronLeft, ChevronRight, X, Check } from 'lucide-react';\n\ninterface TableColumn {\n key: string;\n label: string;\n sortable?: boolean;\n filterable?: boolean;\n width?: string;\n render?: (value: any, row: any) => React.ReactNode;\n type?: 'text' | 'number' | 'date' | 'boolean' | 'custom';\n format?: (value: any) => string;\n}\n\ninterface TableAction {\n key: string;\n label: string;\n icon: React.ReactNode;\n onClick: (row: any) => void;\n disabled?: (row: any) => boolean;\n variant?: 'primary' | 'secondary' | 'danger';\n}\n\ninterface FilterOption {\n key: string;\n label: string;\n value: any;\n type: 'text' | 'select' | 'date' | 'number';\n options?: { label: string; value: any }[];\n}\n\ninterface AdvancedTableProps {\n data: any[];\n columns: TableColumn[];\n actions?: TableAction[];\n loading?: boolean;\n pagination?: {\n page: number;\n pageSize: number;\n total: number;\n onPageChange: (page: number) => void;\n onPageSizeChange: (pageSize: number) => void;\n };\n sorting?: {\n field: string;\n direction: 'asc' | 'desc';\n onSort: (field: string, direction: 'asc' | 'desc') => void;\n };\n filtering?: {\n filters: FilterOption[];\n values: { [key: string]: any };\n onFilterChange: (values: { [key: string]: any }) => void;\n };\n selection?: {\n selectedRows: any[];\n onSelectionChange: (selectedRows: any[]) => void;\n };\n exportOptions?: {\n csv?: boolean;\n excel?: boolean;\n pdf?: boolean;\n onExport: (format: 'csv' | 'excel' | 'pdf') => void;\n };\n className?: string;\n emptyMessage?: string;\n virtualScrolling?: boolean;\n rowHeight?: number;\n maxHeight?: number;\n}\n\nexport const AdvancedDataTable: React.FC = ({\n data,\n columns,\n actions = [],\n loading = false,\n pagination,\n sorting,\n filtering,\n selection,\n exportOptions,\n className = '',\n emptyMessage = 'No data available',\n virtualScrolling = false,\n rowHeight = 50,\n maxHeight = 400\n}) => {\n const [searchQuery, setSearchQuery] = useState('');\n const [showFilters, setShowFilters] = useState(false);\n const [showColumnSelector, setShowColumnSelector] = useState(false);\n const [visibleColumns, setVisibleColumns] = useState(columns.map(col => col.key));\n const [expandedRows, setExpandedRows] = useState>(new Set());\n const [actionMenuOpen, setActionMenuOpen] = useState(null);\n const tableRef = useRef(null);\n const actionMenuRef = useRef(null);\n\n // Filter and search data\n const filteredData = useMemo(() => {\n let filtered = [...data];\n\n // Apply search query\n if (searchQuery) {\n filtered = filtered.filter(row => {\n return columns.some(column => {\n const value = row[column.key];\n if (value === null || value === undefined) return false;\n return value.toString().toLowerCase().includes(searchQuery.toLowerCase());\n });\n });\n }\n\n // Apply filters\n if (filtering?.values) {\n filtered = filtered.filter(row => {\n return Object.entries(filtering.values).every(([key, value]) => {\n if (value === '' || value === null || value === undefined) return true;\n const rowValue = row[key];\n if (rowValue === null || rowValue === undefined) return false;\n return rowValue.toString().toLowerCase().includes(value.toString().toLowerCase());\n });\n });\n }\n\n return filtered;\n }, [data, searchQuery, filtering?.values, columns]);\n\n // Sort data\n const sortedData = useMemo(() => {\n if (!sorting?.field) return filteredData;\n\n return [...filteredData].sort((a, b) => {\n const aValue = a[sorting.field];\n const bValue = b[sorting.field];\n\n if (aValue === null || aValue === undefined) return 1;\n if (bValue === null || bValue === undefined) return -1;\n\n let comparison = 0;\n if (typeof aValue === 'number' && typeof bValue === 'number') {\n comparison = aValue - bValue;\n } else {\n comparison = aValue.toString().localeCompare(bValue.toString());\n }\n\n return sorting.direction === 'desc' ? -comparison : comparison;\n });\n }, [filteredData, sorting]);\n\n // Paginate data\n const paginatedData = useMemo(() => {\n if (!pagination) return sortedData;\n\n const startIndex = (pagination.page - 1) * pagination.pageSize;\n return sortedData.slice(startIndex, startIndex + pagination.pageSize);\n }, [sortedData, pagination]);\n\n // Get current page data for rendering\n const currentData = virtualScrolling ? sortedData : paginatedData;\n\n // Handle column sorting\n const handleSort = useCallback((column: TableColumn) => {\n if (!column.sortable || !sorting) return;\n\n const newDirection = sorting.field === column.key && sorting.direction === 'asc' ? 'desc' : 'asc';\n sorting.onSort(column.key, newDirection);\n }, [sorting]);\n\n // Handle row selection\n const handleRowSelection = useCallback((row: any, checked: boolean) => {\n if (!selection) return;\n\n let newSelection;\n if (checked) {\n newSelection = [...selection.selectedRows, row];\n } else {\n newSelection = selection.selectedRows.filter(r => r !== row);\n }\n\n selection.onSelectionChange(newSelection);\n }, [selection]);\n\n // Handle select all\n const handleSelectAll = useCallback((checked: boolean) => {\n if (!selection) return;\n\n selection.onSelectionChange(checked ? [...currentData] : []);\n }, [selection, currentData]);\n\n // Handle export\n const handleExport = useCallback((format: 'csv' | 'excel' | 'pdf') => {\n exportOptions?.onExport(format);\n }, [exportOptions]);\n\n // Format cell value\n const formatValue = useCallback((value: any, column: TableColumn) => {\n if (column.render) {\n return column.render(value, data.find(row => row[column.key] === value));\n }\n\n if (column.format) {\n return column.format(value);\n }\n\n if (column.type === 'date' && value) {\n return new Date(value).toLocaleDateString();\n }\n\n if (column.type === 'boolean') {\n return value ? 'Yes' : 'No';\n }\n\n if (column.type === 'number') {\n return new Intl.NumberFormat().format(value);\n }\n\n return value;\n }, [data]);\n\n // Close action menu when clicking outside\n useEffect(() => {\n const handleClickOutside = (event: MouseEvent) => {\n if (actionMenuRef.current && !actionMenuRef.current.contains(event.target as Node)) {\n setActionMenuOpen(null);\n }\n };\n\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, []);\n\n const totalPages = pagination ? Math.ceil(pagination.total / pagination.pageSize) : 1;\n const isAllSelected = selection ? selection.selectedRows.length === currentData.length : false;\n const isIndeterminate = selection ? selection.selectedRows.length > 0 && selection.selectedRows.length < currentData.length : false;\n\n return (\n \n {/* Header */}\n
\n
\n {/* Search */}\n
\n \n setSearchQuery(e.target.value)}\n className=\"w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n />\n
\n\n {/* Actions */}\n
\n {/* Filters */}\n {filtering && (\n
\n
\n\n {/* Filter Dropdown */}\n {showFilters && (\n
\n
\n
Filters
\n \n \n
\n {filtering.filters.map(filter => (\n
\n \n {filter.type === 'text' && (\n filtering.onFilterChange({\n ...filtering.values,\n [filter.key]: e.target.value\n })}\n className=\"w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500\"\n />\n )}\n {filter.type === 'select' && (\n \n )}\n
\n ))}\n
\n
\n \n \n
\n
\n )}\n
\n )}\n\n {/* Export */}\n {exportOptions && (\n
\n \n
\n )}\n\n {/* Column Selector */}\n
\n
\n
\n
\n\n {/* Table */}\n
\n {loading ? (\n
\n ) : currentData.length === 0 ? (\n
\n ) : (\n
\n )}\n
\n\n {/* Pagination */}\n {pagination && (\n
\n
\n
\n \n Showing {((pagination.page - 1) * pagination.pageSize) + 1} to{' '}\n {Math.min(pagination.page * pagination.pageSize, pagination.total)} of{' '}\n {pagination.total} results\n \n
\n \n
\n {/* Page Size Selector */}\n
\n \n {/* Pagination Controls */}\n
\n \n \n \n \n \n Page {pagination.page} of {totalPages}\n \n \n \n \n \n
\n
\n
\n
\n )}\n
\n );\n};
+import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
+import { Search, Filter, Download, ChevronUp, ChevronDown, MoreVertical, Eye, Edit, Trash2, ChevronLeft, ChevronRight, X, Check } from 'lucide-react';
+
+interface TableColumn {
+ key: string;
+ label: string;
+ sortable?: boolean;
+ filterable?: boolean;
+ width?: string;
+ render?: (value: any, row: any) => React.ReactNode;
+ type?: 'text' | 'number' | 'date' | 'boolean' | 'custom';
+ format?: (value: any) => string;
+}
+
+interface TableAction {
+ key: string;
+ label: string;
+ icon: React.ReactNode;
+ onClick: (row: any) => void;
+ disabled?: (row: any) => boolean;
+ variant?: 'primary' | 'secondary' | 'danger';
+}
+
+interface FilterOption {
+ key: string;
+ label: string;
+ value: any;
+ type: 'text' | 'select' | 'date' | 'number';
+ options?: { label: string; value: any }[];
+}
+
+interface AdvancedTableProps {
+ data: any[];
+ columns: TableColumn[];
+ actions?: TableAction[];
+ loading?: boolean;
+ pagination?: {
+ page: number;
+ pageSize: number;
+ total: number;
+ onPageChange: (page: number) => void;
+ onPageSizeChange: (pageSize: number) => void;
+ };
+ sorting?: {
+ field: string;
+ direction: 'asc' | 'desc';
+ onSort: (field: string, direction: 'asc' | 'desc') => void;
+ };
+ filtering?: {
+ filters: FilterOption[];
+ values: { [key: string]: any };
+ onFilterChange: (values: { [key: string]: any }) => void;
+ };
+ selection?: {
+ selectedRows: any[];
+ onSelectionChange: (selectedRows: any[]) => void;
+ };
+ bulkActions?: {
+ key: string;
+ label: string;
+ icon: React.ReactNode;
+ onClick: (selectedRows: any[]) => void;
+ variant?: 'primary' | 'secondary' | 'danger';
+ }[];
+ exportOptions?: {
+ csv?: boolean;
+ excel?: boolean;
+ pdf?: boolean;
+ onExport: (format: 'csv' | 'excel' | 'pdf') => void;
+ };
+ className?: string;
+ emptyMessage?: string;
+ virtualScrolling?: boolean;
+ rowHeight?: number;
+ maxHeight?: number;
+}
+
+export const AdvancedDataTable = ({
+ data,
+ columns,
+ actions = [],
+ loading = false,
+ pagination,
+ sorting,
+ filtering,
+ selection,
+ bulkActions = [],
+ exportOptions,
+ className = '',
+ emptyMessage = 'No data available',
+ virtualScrolling = false,
+ rowHeight = 50,
+ maxHeight = 400
+}: AdvancedTableProps) => {
+ const [searchQuery, setSearchQuery] = useState('');
+ const [showFilters, setShowFilters] = useState(false);
+ const [showColumnSelector, setShowColumnSelector] = useState(false);
+ const [visibleColumns, setVisibleColumns] = useState(columns.map(col => col.key));
+ const [expandedRows, setExpandedRows] = useState>(new Set());
+ const [actionMenuOpen, setActionMenuOpen] = useState(null);
+ const tableRef = useRef(null);
+ const actionMenuRef = useRef(null);
+
+ // Filter and search data
+ const filteredData = useMemo(() => {
+ let filtered = [...data];
+
+ // Apply search query
+ if (searchQuery) {
+ filtered = filtered.filter(row => {
+ return columns.some(column => {
+ const value = row[column.key];
+ if (value === null || value === undefined) return false;
+ return value.toString().toLowerCase().includes(searchQuery.toLowerCase());
+ });
+ });
+ }
+
+ // Apply filters
+ if (filtering?.values) {
+ filtered = filtered.filter(row => {
+ return Object.entries(filtering.values).every(([key, value]) => {
+ if (value === '' || value === null || value === undefined) return true;
+ const rowValue = row[key];
+ if (rowValue === null || rowValue === undefined) return false;
+ return rowValue.toString().toLowerCase().includes(value.toString().toLowerCase());
+ });
+ });
+ }
+
+ return filtered;
+ }, [data, searchQuery, filtering?.values, columns]);
+
+ // Sort data
+ const sortedData = useMemo(() => {
+ if (!sorting?.field) return filteredData;
+
+ return [...filteredData].sort((a, b) => {
+ const aValue = a[sorting.field];
+ const bValue = b[sorting.field];
+
+ if (aValue === null || aValue === undefined) return 1;
+ if (bValue === null || bValue === undefined) return -1;
+
+ let comparison = 0;
+ if (typeof aValue === 'number' && typeof bValue === 'number') {
+ comparison = aValue - bValue;
+ } else {
+ comparison = aValue.toString().localeCompare(bValue.toString());
+ }
+
+ return sorting.direction === 'desc' ? -comparison : comparison;
+ });
+ }, [filteredData, sorting]);
+
+ // Paginate data
+ const paginatedData = useMemo(() => {
+ if (!pagination) return sortedData;
+
+ const startIndex = (pagination.page - 1) * pagination.pageSize;
+ return sortedData.slice(startIndex, startIndex + pagination.pageSize);
+ }, [sortedData, pagination]);
+
+ // Get current page data for rendering
+ const currentData = virtualScrolling ? sortedData : paginatedData;
+
+ // Handle column sorting
+ const handleSort = useCallback((column: TableColumn) => {
+ if (!column.sortable || !sorting) return;
+
+ const newDirection = sorting.field === column.key && sorting.direction === 'asc' ? 'desc' : 'asc';
+ sorting.onSort(column.key, newDirection);
+ }, [sorting]);
+
+ // Handle row selection
+ const handleRowSelection = useCallback((row: any, checked: boolean) => {
+ if (!selection) return;
+
+ let newSelection;
+ if (checked) {
+ newSelection = [...selection.selectedRows, row];
+ } else {
+ newSelection = selection.selectedRows.filter(r => r !== row);
+ }
+
+ selection.onSelectionChange(newSelection);
+ }, [selection]);
+
+ // Handle select all
+ const handleSelectAll = useCallback((checked: boolean) => {
+ if (!selection) return;
+
+ selection.onSelectionChange(checked ? [...currentData] : []);
+ }, [selection, currentData]);
+
+ // Handle export
+ const handleExport = useCallback((format: 'csv' | 'excel' | 'pdf') => {
+ exportOptions?.onExport(format);
+ }, [exportOptions]);
+
+ // Format cell value
+ const formatValue = useCallback((value: any, column: TableColumn) => {
+ if (column.render) {
+ return column.render(value, data.find(row => row[column.key] === value));
+ }
+
+ if (column.format) {
+ return column.format(value);
+ }
+
+ if (column.type === 'date' && value) {
+ return new Date(value).toLocaleDateString();
+ }
+
+ if (column.type === 'boolean') {
+ return value ? 'Yes' : 'No';
+ }
+
+ if (column.type === 'number') {
+ return new Intl.NumberFormat().format(value);
+ }
+
+ return value;
+ }, [data]);
+
+ // Close action menu when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (actionMenuRef.current && !actionMenuRef.current.contains(event.target as Node)) {
+ setActionMenuOpen(null);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ const totalPages = pagination ? Math.ceil(pagination.total / pagination.pageSize) : 1;
+ const isAllSelected = selection ? selection.selectedRows.length === currentData.length : false;
+ const isIndeterminate = selection ? selection.selectedRows.length > 0 && selection.selectedRows.length < currentData.length : false;
+
+ return (
+
+ {/* Header */}
+
+ {selection && selection.selectedRows.length > 0 && bulkActions.length > 0 && (
+
({
+ ...action,
+ onClick: () => action.onClick(selection.selectedRows)
+ }))}
+ onClear={() => selection.onSelectionChange([])}
+ />
+ )}
+
+ {/* Search */}
+
+
+ setSearchQuery(e.target.value)}
+ className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+ />
+
+
+ {/* Actions */}
+
+ {/* Filters */}
+ {filtering && (
+
+
+
+ {/* Filter Dropdown */}
+ {showFilters && (
+
+
+
Filters
+
+
+
+ {filtering.filters.map(filter => (
+
+
+ {filter.type === 'text' && (
+ filtering.onFilterChange({
+ ...filtering.values,
+ [filter.key]: e.target.value
+ })}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
+ />
+ )}
+ {filter.type === 'select' && (
+
+ )}
+
+ ))}
+
+
+
+
+
+
+ )}
+
+ )}
+
+ {/* Export */}
+ {exportOptions && (
+
+
+
+ )}
+
+ {/* Column Selector */}
+
+
+
+
+
+ {/* Table */}
+
+ {loading ? (
+
+ ) : currentData.length === 0 ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Pagination */}
+ {pagination && (
+
+
+
+
+ Showing {((pagination.page - 1) * pagination.pageSize) + 1} to{' '}
+ {Math.min(pagination.page * pagination.pageSize, pagination.total)} of{' '}
+ {pagination.total} results
+
+
+
+
+ {/* Page Size Selector */}
+
+
+ {/* Pagination Controls */}
+
+
+
+
+
+
+ Page {pagination.page} of {totalPages}
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+interface BulkActionBarProps {
+ selectedCount: number;
+ actions: {
+ key: string;
+ label: string;
+ icon: React.ReactNode;
+ onClick: () => void;
+ variant?: 'primary' | 'secondary' | 'danger';
+ }[];
+ onClear: () => void;
+}
+
+const BulkActionBar: React.FC = ({ selectedCount, actions, onClear }) => {
+ return (
+
+
+
+ {selectedCount}
+
+ Selected Items
+
+
+
+ {actions.map(action => (
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/nepa-frontend/src/components/TransactionHistory.tsx b/nepa-frontend/src/components/TransactionHistory.tsx
index 8ba38e3..4476723 100644
--- a/nepa-frontend/src/components/TransactionHistory.tsx
+++ b/nepa-frontend/src/components/TransactionHistory.tsx
@@ -1,6 +1,9 @@
import React, { useState, useEffect, useMemo } from 'react';
import { Transaction, TransactionHistory, TransactionFilters, PaymentStatus } from '../types';
import TransactionService from '../services/transactionService';
+import BookmarkService from '../services/bookmarkService';
+import { Star, Trash2, CheckCircle, FileText, Download } from 'lucide-react';
+import { AdvancedDataTable } from './AdvancedDataTable';
interface Props {
className?: string;
@@ -20,6 +23,14 @@ export const TransactionHistoryComponent: React.FC = ({ className = '' })
totalCount: 0,
hasNextPage: false,
});
+ const [bookmarkedIds, setBookmarkedIds] = useState>(new Set());
+ const [selectedRows, setSelectedRows] = useState([]);
+
+ // Check bookmarks on mount
+ useEffect(() => {
+ const bookmarks = BookmarkService.getBookmarks();
+ setBookmarkedIds(new Set(bookmarks.map(b => b.id)));
+ }, []);
// Load transactions on component mount and filter changes
useEffect(() => {
@@ -100,6 +111,25 @@ export const TransactionHistoryComponent: React.FC = ({ className = '' })
}
};
+ const handleToggleBookmark = (transaction: Transaction) => {
+ const isBookmarked = BookmarkService.toggleBookmark({
+ id: transaction.id,
+ type: 'transaction',
+ title: `Transaction ${transaction.id}`,
+ data: transaction
+ });
+
+ setBookmarkedIds(prev => {
+ const next = new Set(prev);
+ if (isBookmarked) {
+ next.add(transaction.id);
+ } else {
+ next.delete(transaction.id);
+ }
+ return next;
+ });
+ };
+
const clearFilters = () => {
setFilters({});
};
@@ -128,7 +158,7 @@ export const TransactionHistoryComponent: React.FC = ({ className = '' })
@@ -176,7 +206,7 @@ export const TransactionHistoryComponent: React.FC = ({ className = '' })
handleFilterChange('dateTo', e.target.value)}
+ onChange={(e: React.ChangeEvent) => handleFilterChange('dateTo', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
@@ -188,7 +218,7 @@ export const TransactionHistoryComponent: React.FC = ({ className = '' })
type="text"
placeholder="METER-123"
value={filters.meterId || ''}
- onChange={(e) => handleFilterChange('meterId', e.target.value)}
+ onChange={(e: React.ChangeEvent) => handleFilterChange('meterId', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
@@ -198,7 +228,7 @@ export const TransactionHistoryComponent: React.FC = ({ className = '' })