Skip to content

release: v5.4.0 #442

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "CoreUI React Admin",
"image": "mcr.microsoft.com/devcontainers/javascript-node:20",
"features": {},
"postCreateCommand": "npm install",
"customizations": {
"vscode": {
"extensions": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint"
]
}
},
"workspaceFolder": "/workspace",
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached",
"remoteUser": "node"
}
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// No API keys needed for OpenStreetMap integration
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@

# dependencies
/node_modules
package-lock.json
yarn.lock

# testing
@@ -23,3 +22,4 @@ yarn.lock
npm-debug.log*
yarn-debug.log*
yarn-error.log*

1 change: 1 addition & 0 deletions coreui-free-react-admin-template
Submodule coreui-free-react-admin-template added at 2b38ce
9,223 changes: 9,223 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -14,9 +14,10 @@
"author": "The CoreUI Team (https://github.com/orgs/coreui/people)",
"scripts": {
"build": "vite build",
"dev": "vite",
"lint": "eslint",
"serve": "vite preview",
"start": "vite"
"start": "react-scripts start"
},
"dependencies": {
"@coreui/chartjs": "^4.1.0",
@@ -27,12 +28,19 @@
"@coreui/react-chartjs": "^3.0.0",
"@coreui/utils": "^2.0.2",
"@popperjs/core": "^2.11.8",
"@react-google-maps/api": "^2.20.6",
"chart.js": "^4.4.7",
"classnames": "^2.5.1",
"core-js": "^3.40.0",
"googleapis": "^148.0.0",
"leaflet": "^1.9.4",
"openrouteservice-js": "^0.4.1",
"papaparse": "^5.5.2",
"prop-types": "^15.8.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-leaflet": "^5.0.0",
"react-leaflet-cluster": "^2.1.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.1.5",
"redux": "5.0.1",
40 changes: 31 additions & 9 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import React, { Suspense, useEffect } from 'react'
import { HashRouter, Route, Routes } from 'react-router-dom'
import { HashRouter, Route, Routes, Navigate } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { AuthProvider } from './context/AuthContext'
import { useAuth } from './context/AuthContext'

import { CSpinner, useColorModes } from '@coreui/react'
import './scss/style.scss'

const PrivateRoute = ({ children }) => {
const { user } = useAuth();
return user ? children : <Navigate to="/login" />;
};

// We use those styles to show code examples, you should remove them in your application.
import './scss/examples.scss'

@@ -17,7 +24,7 @@ const Register = React.lazy(() => import('./views/pages/register/Register'))
const Page404 = React.lazy(() => import('./views/pages/page404/Page404'))
const Page500 = React.lazy(() => import('./views/pages/page500/Page500'))

const App = () => {
const AppContent = () => {
const { isColorModeSet, setColorMode } = useColorModes('coreui-free-react-admin-template-theme')
const storedTheme = useSelector((state) => state.theme)

@@ -43,17 +50,32 @@ const App = () => {
<CSpinner color="primary" variant="grow" />
</div>
}
>
<Routes>
<Route exact path="/login" name="Login Page" element={<Login />} />
<Route exact path="/register" name="Register Page" element={<Register />} />
<Route exact path="/404" name="Page 404" element={<Page404 />} />
<Route exact path="/500" name="Page 500" element={<Page500 />} />
<Route path="*" name="Home" element={<DefaultLayout />} />
> <Routes>
<Route exact path="/login" name="Login Page" element={<Login />} />
<Route exact path="/register" name="Register Page" element={<Register />} />
<Route exact path="/404" name="Page 404" element={<Page404 />} />
<Route exact path="/500" name="Page 500" element={<Page500 />} />
<Route
path="*"
name="Home"
element={
<PrivateRoute>
<DefaultLayout />
</PrivateRoute>
}
/>
</Routes>
</Suspense>
</HashRouter>
)
}

const App = () => {
return (
<AuthProvider>
<AppContent />
</AuthProvider>
);
};

export default App
7 changes: 7 additions & 0 deletions src/_nav.js
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ import {
cilExternalLink,
cilNotes,
cilPencil,
cilPeople,
cilPuzzle,
cilSpeedometer,
cilStar,
@@ -27,6 +28,12 @@ const _nav = [
text: 'NEW',
},
},
{
component: CNavItem,
name: 'Users',
to: '/users',
icon: <CIcon icon={cilPeople} customClassName="nav-icon" />,
},
{
component: CNavTitle,
name: 'Theme',
57 changes: 57 additions & 0 deletions src/components/ErrorBoundary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react'
import {
CAlert,
CCard,
CCardBody,
CCardHeader,
} from '@coreui/react'

class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false, error: null, errorInfo: null }
}

static getDerivedStateFromError(error) {
return { hasError: true }
}

componentDidCatch(error, errorInfo) {
console.error('Error caught by ErrorBoundary:', error, errorInfo)
this.setState({
error: error,
errorInfo: errorInfo
})
}

render() {
if (this.state.hasError) {
return (
<CCard className="mb-4">
<CCardHeader>
<h4>Something went wrong</h4>
</CCardHeader>
<CCardBody>
<CAlert color="danger">
<h4 className="alert-heading">Error Details</h4>
<p>{this.state.error && this.state.error.toString()}</p>
<hr />
<p className="mb-0">
Please try refreshing the page. If the problem persists, contact support.
</p>
</CAlert>
{process.env.NODE_ENV === 'development' && (
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.errorInfo && this.state.errorInfo.componentStack}
</details>
)}
</CCardBody>
</CCard>
)
}

return this.props.children
}
}

export default ErrorBoundary
29 changes: 17 additions & 12 deletions src/components/header/AppHeaderDropdown.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react'
import { useNavigate } from 'react-router-dom'
import {
CAvatar,
CBadge,
@@ -9,6 +10,7 @@ import {
CDropdownMenu,
CDropdownToggle,
} from '@coreui/react'
import { useAuth } from '../../context/AuthContext'
import {
cilBell,
cilCreditCard,
@@ -25,6 +27,14 @@ import CIcon from '@coreui/icons-react'
import avatar8 from './../../assets/images/avatars/8.jpg'

const AppHeaderDropdown = () => {
const { user, logout } = useAuth();
const navigate = useNavigate();

const handleLogout = () => {
logout();
navigate('/login');
};

return (
<CDropdown variant="nav-item">
<CDropdownToggle placement="bottom-end" className="py-0 pe-0" caret={false}>
@@ -46,19 +56,14 @@ const AppHeaderDropdown = () => {
42
</CBadge>
</CDropdownItem>
<CDropdownItem href="#">
<CIcon icon={cilTask} className="me-2" />
Tasks
<CBadge color="danger" className="ms-2">
42
</CBadge>
<CDropdownItem>
<CIcon icon={cilUser} className="me-2" />
{user?.username || 'User'}
</CDropdownItem>
<CDropdownItem href="#">
<CIcon icon={cilCommentSquare} className="me-2" />
Comments
<CBadge color="warning" className="ms-2">
42
</CBadge>
<CDropdownDivider />
<CDropdownItem onClick={handleLogout}>
<CIcon icon={cilLockLocked} className="me-2" />
Logout
</CDropdownItem>
<CDropdownHeader className="bg-body-secondary fw-semibold my-2">Settings</CDropdownHeader>
<CDropdownItem href="#">
9 changes: 9 additions & 0 deletions src/constants/map.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const SHEET_BASE_URL =
'https://docs.google.com/spreadsheets/d/1c0-6pkvhvFntApsvgjCrLJrs0oU9e-L1wjfpsG80Ftk/export?format=csv'

export const TABS = [
{ gid: '0', color: 'green', label: 'Current Active Customers' },
{ gid: '364360587', color: 'red', label: 'New Customers' },
]

export const CUSTOMER_LOCATION_FIELD = 'موقع العميل'
51 changes: 51 additions & 0 deletions src/context/AuthContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { createContext, useContext, useState, useEffect } from 'react';

const AuthContext = createContext(null);

const ADMIN_USER = {
username: 'Admin',
password: 'admin123',
role: 'admin'
};

export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);

useEffect(() => {
// Check if user is stored in localStorage on load
const storedUser = localStorage.getItem('user');
if (storedUser) {
setUser(JSON.parse(storedUser));
}
}, []);

const login = (username, password) => {
// For now, we'll just check against the hardcoded admin user
if (username === ADMIN_USER.username && password === ADMIN_USER.password) {
const userData = { username, role: ADMIN_USER.role };
setUser(userData);
localStorage.setItem('user', JSON.stringify(userData));
return true;
}
return false;
};

const logout = () => {
setUser(null);
localStorage.removeItem('user');
};

return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
};

export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
12 changes: 11 additions & 1 deletion src/layout/DefaultLayout.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import React from 'react'
import React, { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { AppContent, AppSidebar, AppFooter, AppHeader } from '../components/index'
import { useAuth } from '../context/AuthContext'

const DefaultLayout = () => {
const { user } = useAuth()
const navigate = useNavigate()

useEffect(() => {
if (!user) {
navigate('/login')
}
}, [user, navigate])
return (
<div>
<AppSidebar />
4 changes: 4 additions & 0 deletions src/routes.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react'

const Dashboard = React.lazy(() => import('./views/dashboard/Dashboard'))
const Users = React.lazy(() => import('./views/pages/users/Users'))
const Colors = React.lazy(() => import('./views/theme/colors/Colors'))
const Typography = React.lazy(() => import('./views/theme/typography/Typography'))

@@ -35,6 +36,7 @@ const Layout = React.lazy(() => import('./views/forms/layout/Layout'))
const Range = React.lazy(() => import('./views/forms/range/Range'))
const Select = React.lazy(() => import('./views/forms/select/Select'))
const Validation = React.lazy(() => import('./views/forms/validation/Validation'))
const CustomerForm = React.lazy(() => import('./views/forms/CustomerForm'))

const Charts = React.lazy(() => import('./views/charts/Charts'))

@@ -87,6 +89,7 @@ const routes = [
{ path: '/forms/floating-labels', name: 'Floating Labels', element: FloatingLabels },
{ path: '/forms/layout', name: 'Layout', element: Layout },
{ path: '/forms/validation', name: 'Validation', element: Validation },
{ path: '/customer-form', name: 'Customer Form', element: CustomerForm },
{ path: '/icons', exact: true, name: 'Icons', element: CoreUIIcons },
{ path: '/icons/coreui-icons', name: 'CoreUI Icons', element: CoreUIIcons },
{ path: '/icons/flags', name: 'Flags', element: Flags },
@@ -97,6 +100,7 @@ const routes = [
{ path: '/notifications/modals', name: 'Modals', element: Modals },
{ path: '/notifications/toasts', name: 'Toasts', element: Toasts },
{ path: '/widgets', name: 'Widgets', element: Widgets },
{ path: '/users', name: 'Users', element: Users },
]

export default routes
13 changes: 13 additions & 0 deletions src/utils/googleSheets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Utility to fetch and parse Google Sheets CSV as array of objects
import Papa from 'papaparse'

export async function fetchGoogleSheetData(sheetCsvUrl, tabGid) {
let url = sheetCsvUrl
if (tabGid) {
url += `&gid=${tabGid}`
}
const res = await fetch(url)
const csv = await res.text()
const parsed = Papa.parse(csv, { header: true, skipEmptyLines: true })
return parsed.data
}
15 changes: 15 additions & 0 deletions src/utils/mapIcons.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import L from 'leaflet'

export const createIcon = (color) =>
new L.Icon({
iconUrl: `https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-${color}.png`,
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [20, 32],
iconAnchor: [10, 32],
popupAnchor: [1, -34],
shadowSize: [32, 32],
shadowAnchor: [10, 32],
})

export const greenIcon = createIcon('green')
export const redIcon = createIcon('red')
181 changes: 35 additions & 146 deletions src/views/dashboard/Dashboard.js
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@ import React from 'react'
import classNames from 'classnames'

import {
CAvatar,
CButton,
CButtonGroup,
CCard,
@@ -12,12 +11,6 @@ import {
CCol,
CProgress,
CRow,
CTable,
CTableBody,
CTableDataCell,
CTableHead,
CTableHeaderCell,
CTableRow,
} from '@coreui/react'
import CIcon from '@coreui/icons-react'
import {
@@ -43,16 +36,10 @@ import {
cilUserFemale,
} from '@coreui/icons'

import avatar1 from 'src/assets/images/avatars/1.jpg'
import avatar2 from 'src/assets/images/avatars/2.jpg'
import avatar3 from 'src/assets/images/avatars/3.jpg'
import avatar4 from 'src/assets/images/avatars/4.jpg'
import avatar5 from 'src/assets/images/avatars/5.jpg'
import avatar6 from 'src/assets/images/avatars/6.jpg'

import WidgetsBrand from '../widgets/WidgetsBrand'
import WidgetsDropdown from '../widgets/WidgetsDropdown'
import MainChart from './MainChart'
import OpenStreetMapView from './OpenStreetMapView'

const Dashboard = () => {
const progressExample = [
@@ -85,94 +72,38 @@ const Dashboard = () => {
{ title: 'LinkedIn', icon: cibLinkedin, percent: 8, value: '27,319' },
]

const tableExample = [
// If tableData is supposed to be a prop, add it to the function signature:
// const Dashboard = ({ tableData }) => {

// For now, define a sample tableData array here:
const tableData = [
{
avatar: { src: avatar1, status: 'success' },
user: {
name: 'Yiorgos Avraamu',
new: true,
registered: 'Jan 1, 2023',
},
country: { name: 'USA', flag: cifUs },
usage: {
value: 50,
period: 'Jun 11, 2023 - Jul 10, 2023',
color: 'success',
},
payment: { name: 'Mastercard', icon: cibCcMastercard },
user: { name: 'John Doe', new: true, registered: 'Jan 1, 2023' },
country: { flag: cifUs, name: 'USA' },
usage: { value: 50, period: 'Jun 2023', color: 'success' },
payment: { icon: cibCcVisa },
activity: '10 sec ago',
},
{
avatar: { src: avatar2, status: 'danger' },
user: {
name: 'Avram Tarasios',
new: false,
registered: 'Jan 1, 2023',
},
country: { name: 'Brazil', flag: cifBr },
usage: {
value: 22,
period: 'Jun 11, 2023 - Jul 10, 2023',
color: 'info',
},
payment: { name: 'Visa', icon: cibCcVisa },
activity: '5 minutes ago',
user: { name: 'Jane Smith', new: false, registered: 'Feb 15, 2023' },
country: { flag: cifFr, name: 'France' },
usage: { value: 80, period: 'Jun 2023', color: 'info' },
payment: { icon: cibCcMastercard },
activity: '5 min ago',
},
{
avatar: { src: avatar3, status: 'warning' },
user: { name: 'Quintin Ed', new: true, registered: 'Jan 1, 2023' },
country: { name: 'India', flag: cifIn },
usage: {
value: 74,
period: 'Jun 11, 2023 - Jul 10, 2023',
color: 'warning',
},
payment: { name: 'Stripe', icon: cibCcStripe },
user: { name: 'Mike Brown', new: false, registered: 'Mar 10, 2023' },
country: { flag: cifIn, name: 'India' },
usage: { value: 30, period: 'Jun 2023', color: 'warning' },
payment: { icon: cibCcPaypal },
activity: '1 hour ago',
},
{
avatar: { src: avatar4, status: 'secondary' },
user: { name: 'Enéas Kwadwo', new: true, registered: 'Jan 1, 2023' },
country: { name: 'France', flag: cifFr },
usage: {
value: 98,
period: 'Jun 11, 2023 - Jul 10, 2023',
color: 'danger',
},
payment: { name: 'PayPal', icon: cibCcPaypal },
activity: 'Last month',
},
{
avatar: { src: avatar5, status: 'success' },
user: {
name: 'Agapetus Tadeáš',
new: true,
registered: 'Jan 1, 2023',
},
country: { name: 'Spain', flag: cifEs },
usage: {
value: 22,
period: 'Jun 11, 2023 - Jul 10, 2023',
color: 'primary',
},
payment: { name: 'Google Wallet', icon: cibCcApplePay },
activity: 'Last week',
},
{
avatar: { src: avatar6, status: 'danger' },
user: {
name: 'Friderik Dávid',
new: true,
registered: 'Jan 1, 2023',
},
country: { name: 'Poland', flag: cifPl },
usage: {
value: 43,
period: 'Jun 11, 2023 - Jul 10, 2023',
color: 'success',
},
payment: { name: 'Amex', icon: cibCcAmex },
activity: 'Last week',
user: { name: 'Emily White', new: true, registered: 'Apr 20, 2023' },
country: { flag: cifEs, name: 'Spain' },
usage: { value: 65, period: 'Jun 2023', color: 'danger' },
payment: { icon: cibCcStripe },
activity: 'Yesterday',
},
]

@@ -322,60 +253,18 @@ const Dashboard = () => {
</CRow>

<br />
</CCardBody>
</CCard>
</CCol>
</CRow>

<CTable align="middle" className="mb-0 border" hover responsive>
<CTableHead className="text-nowrap">
<CTableRow>
<CTableHeaderCell className="bg-body-tertiary text-center">
<CIcon icon={cilPeople} />
</CTableHeaderCell>
<CTableHeaderCell className="bg-body-tertiary">User</CTableHeaderCell>
<CTableHeaderCell className="bg-body-tertiary text-center">
Country
</CTableHeaderCell>
<CTableHeaderCell className="bg-body-tertiary">Usage</CTableHeaderCell>
<CTableHeaderCell className="bg-body-tertiary text-center">
Payment Method
</CTableHeaderCell>
<CTableHeaderCell className="bg-body-tertiary">Activity</CTableHeaderCell>
</CTableRow>
</CTableHead>
<CTableBody>
{tableExample.map((item, index) => (
<CTableRow v-for="item in tableItems" key={index}>
<CTableDataCell className="text-center">
<CAvatar size="md" src={item.avatar.src} status={item.avatar.status} />
</CTableDataCell>
<CTableDataCell>
<div>{item.user.name}</div>
<div className="small text-body-secondary text-nowrap">
<span>{item.user.new ? 'New' : 'Recurring'}</span> | Registered:{' '}
{item.user.registered}
</div>
</CTableDataCell>
<CTableDataCell className="text-center">
<CIcon size="xl" icon={item.country.flag} title={item.country.name} />
</CTableDataCell>
<CTableDataCell>
<div className="d-flex justify-content-between text-nowrap">
<div className="fw-semibold">{item.usage.value}%</div>
<div className="ms-3">
<small className="text-body-secondary">{item.usage.period}</small>
</div>
</div>
<CProgress thin color={item.usage.color} value={item.usage.value} />
</CTableDataCell>
<CTableDataCell className="text-center">
<CIcon size="xl" icon={item.payment.icon} />
</CTableDataCell>
<CTableDataCell>
<div className="small text-body-secondary text-nowrap">Last login</div>
<div className="fw-semibold text-nowrap">{item.activity}</div>
</CTableDataCell>
</CTableRow>
))}
</CTableBody>
</CTable>
<CRow>
<CCol xs={12}>
<CCard className="mb-4">
<CCardHeader>OpenStreetMap Example (No Card Required)</CCardHeader>
<CCardBody>
<OpenStreetMapView />
<div className="small text-body-secondary mt-2"></div>
</CCardBody>
</CCard>
</CCol>
24 changes: 24 additions & 0 deletions src/views/dashboard/GoogleMapView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react'
import { GoogleMap, LoadScript } from '@react-google-maps/api'

const containerStyle = {
width: '100%',
height: '400px',
}

const center = {
lat: 40.7128, // Example: New York City
lng: -74.006,
}

const GoogleMapView = () => {
return (
<LoadScript googleMapsApiKey={process.env.REACT_APP_GOOGLE_MAPS_API_KEY || 'YOUR_API_KEY_HERE'}>
<GoogleMap mapContainerStyle={containerStyle} center={center} zoom={10}>
{/* Add markers or other map features here */}
</GoogleMap>
</LoadScript>
)
}

export default GoogleMapView
266 changes: 266 additions & 0 deletions src/views/dashboard/OpenStreetMapView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
import React, { useEffect, useState, useMemo } from 'react'
import { MapContainer, TileLayer, Marker, Popup, Polyline } from 'react-leaflet'
import MarkerClusterGroup from 'react-leaflet-cluster'
import 'leaflet/dist/leaflet.css'
import 'react-leaflet-cluster/lib/assets/MarkerCluster.Default.css'
import openrouteservice from 'openrouteservice-js'
import { ORS_API_KEY } from './ors.config'
import L from 'leaflet'
import { useNavigate } from 'react-router-dom'
import { useAuth } from '../../context/AuthContext'
import { fetchGoogleSheetData } from '../../utils/googleSheets'
import { createIcon, greenIcon, redIcon } from '../../utils/mapIcons'
import { SHEET_BASE_URL, TABS, CUSTOMER_LOCATION_FIELD } from '../../constants/map'

// Import marker icons
import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png'
import markerIcon from 'leaflet/dist/images/marker-icon.png'
import markerShadow from 'leaflet/dist/images/marker-shadow.png'

// Delete default icon's reference since we'll use custom icons
delete L.Icon.Default.prototype._getIconUrl
L.Icon.Default.mergeOptions({
iconRetinaUrl: markerIcon2x,
iconUrl: markerIcon,
shadowUrl: markerShadow,
})

const OpenStreetMapView = () => {
const [customersByTab, setCustomersByTab] = useState([])
const [userLocation, setUserLocation] = useState(null)
const [route, setRoute] = useState([])
const [navigatingTo, setNavigatingTo] = useState(null)
const navigate = useNavigate()
const { user } = useAuth()

// Fetch data from both tabs
useEffect(() => {
Promise.all(TABS.map((tab) => fetchGoogleSheetData(SHEET_BASE_URL, tab.gid))).then(
(results) => {
console.log('Fetched customer data:', results)
setCustomersByTab(results)
},
)
}, [])

// Example: Use first two valid customers (from any tab) for routing
useEffect(() => {
const all = customersByTab.flat().filter((c) => c[CUSTOMER_LOCATION_FIELD])
if (all.length >= 2) {
const parseLatLng = (str) => str.split(',').map(Number)
const start = parseLatLng(all[0][CUSTOMER_LOCATION_FIELD])
const end = parseLatLng(all[1][CUSTOMER_LOCATION_FIELD])
const Directions = new openrouteservice.Directions({ api_key: ORS_API_KEY })
Directions.calculate({
coordinates: [
[start[1], start[0]],
[end[1], end[0]],
],
profile: 'driving-car',
format: 'geojson',
})
.then((geojson) => {
const coords = geojson.features[0].geometry.coordinates.map(([lng, lat]) => [lat, lng])
setRoute(coords)
})
.catch((err) => console.error(err))
}
}, [customersByTab])

// On map load, get user location
useEffect(() => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition((pos) => {
setUserLocation([pos.coords.latitude, pos.coords.longitude])
})
}
}, [])

// Default center
const allCustomers = customersByTab.flat().filter((c) => c[CUSTOMER_LOCATION_FIELD])
console.log('All customers with location:', allCustomers)
const center = allCustomers.length
? allCustomers[0][CUSTOMER_LOCATION_FIELD].split(',').map(Number)
: [30.05487296, 31.3385129]
// Memoize markers to prevent unnecessary re-renders
const markers = useMemo(
() =>
customersByTab
.map((customers, tabIdx) =>
customers
.filter((c) => {
const loc = c[CUSTOMER_LOCATION_FIELD]
if (!loc) return false
const parts = loc.split(',').map(Number)
// Check if customer is assigned to current user
const assignee = c['Assignee']
const isAssignedToUser = assignee === user?.username

// Only show if coordinates are valid AND assigned to current user
return parts.length === 2 && parts.every((n) => !isNaN(n)) && isAssignedToUser
})
.map((cust, idx) => {
const [lat, lng] = cust[CUSTOMER_LOCATION_FIELD].split(',').map(Number)
const assignee = cust['Assignee']
return {
id: `${tabIdx}-${idx}`,
position: [lat, lng],
isCurrentCustomer: tabIdx === 0,
assignee: assignee,
}
}),
)
.flat(),
[customersByTab, user],
)

return (
<MapContainer
center={center}
zoom={12}
style={{ height: '400px', width: '100%' }}
preferCanvas={true}
updateWhenZooming={false}
updateWhenIdle={true}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
maxZoom={19}
minZoom={3}
/>
<MarkerClusterGroup chunkedLoading={true} maxClusterRadius={50} spiderfyOnMaxZoom={true}>
{markers.map((marker) => {
// Find the customer object for this marker
const tabCustomers = customersByTab[marker.isCurrentCustomer ? 0 : 1] || []
const customer = tabCustomers.find((c) => {
const [lat, lng] = (c[CUSTOMER_LOCATION_FIELD] || '').split(',').map(Number)
return lat === marker.position[0] && lng === marker.position[1]
})
return (
<Marker
key={marker.id}
position={marker.position}
icon={marker.isCurrentCustomer ? greenIcon : redIcon}
zIndexOffset={marker.isCurrentCustomer ? 1000 : 0}
>
<Popup>
<div style={{ minWidth: 200 }}>
{' '}
<h6 style={{ marginBottom: 8 }}>
{marker.isCurrentCustomer ? 'Current Active Customer' : 'New Customer'}
<span
style={{
marginLeft: '8px',
padding: '2px 6px',
backgroundColor: '#28a745',
color: '#fff',
borderRadius: '4px',
fontSize: '12px',
}}
>
Assigned to: {marker.assignee}
</span>
</h6>
{customer ? (
<div>
{Object.entries(customer)
.filter(([key]) => key !== 'Assignee') // Hide the Assignee field since we show it above
.map(
([key, value]) =>
value && (
<div key={key} style={{ marginBottom: 4 }}>
<strong>{key}:</strong> {value}
</div>
),
)}
<div style={{ marginTop: 12, display: 'flex', gap: 8 }}>
<button
style={{
padding: '6px 12px',
background: '#007bff',
color: '#fff',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
}}
onClick={async () => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
async (pos) => {
const userLat = pos.coords.latitude
const userLng = pos.coords.longitude
const destLat = marker.position[0]
const destLng = marker.position[1]
// Use user's actual current location as origin
const gmapsUrl = `https://www.google.com/maps/dir/?api=1&origin=${userLat},${userLng}&destination=${destLat},${destLng}&travelmode=driving`
window.open(gmapsUrl, '_blank')
},
() => alert('Failed to get your current location.'),
)
} else {
alert('Geolocation is not supported by your browser.')
}
}}
>
Get Destination
</button>
<button
style={{
padding: '6px 12px',
background: '#28a745',
color: '#fff',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
}}
onClick={() => {
// Encode customer data in URL parameters
const params = new URLSearchParams({
name: customer['اسم العميل'] || '',
phone: customer['رقم الموبايل'] || '',
notes: customer['Notes'] || '',
lat: marker.position[0],
lng: marker.position[1],
})
navigate(`/customer-form?${params.toString()}`)
}}
>
Submit a form
</button>
</div>
</div>
) : (
<span>No details available.</span>
)}
</div>
</Popup>
</Marker>
)
})}
</MarkerClusterGroup>
{userLocation && (
<Marker
position={userLocation}
icon={L.icon({
iconUrl:
'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-blue.png',
iconSize: [20, 32],
iconAnchor: [10, 32],
popupAnchor: [1, -34],
shadowUrl: markerShadow,
shadowSize: [32, 32],
shadowAnchor: [10, 32],
})}
>
<Popup>Your Location</Popup>
</Marker>
)}
{route.length > 0 && (
<Polyline positions={route} color={'#007bff'} weight={4} opacity={0.8} />
)}
</MapContainer>
)
}

export default OpenStreetMapView
67 changes: 67 additions & 0 deletions src/views/dashboard/google-apps-script-updated.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
function doGet(e) {
try {
// Get the active spreadsheet
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet()
var sheet = spreadsheet.getSheetByName('Forms')

// If Forms sheet doesn't exist, create it with headers
if (!sheet) {
sheet = spreadsheet.insertSheet('Forms')
sheet.appendRow([
'Timestamp',
'Name',
'Phone',
'Notes',
'Preferred Products',
'Made Order',
'Order GMV',
'Latitude',
'Longitude',
'Location Stamp',
])
}

// Get parameters from the request
var params = e.parameter

// Append data to sheet
sheet.appendRow([
params.timestamp || new Date().toLocaleString(),
params.name || '',
params.phone || '',
params.notes || '',
params.preferredProducts || '',
params.madeOrder || 'No',
params.orderGMV || '0',
params.lat || '',
params.lng || '',
params.locationstamp || '',
])

// Create response
return ContentService.createTextOutput(
JSON.stringify({ success: true, message: 'Data added successfully' }),
)
.setMimeType(ContentService.MimeType.JSON)
.addHeader('Access-Control-Allow-Origin', '*')
} catch (error) {
// Handle errors
var errorResponse = ContentService.createTextOutput(
JSON.stringify({
success: false,
error: error.toString(),
}),
)

errorResponse.setMimeType(ContentService.MimeType.JSON)
errorResponse.setHeader('Access-Control-Allow-Origin', '*')
errorResponse.setHeader('Access-Control-Allow-Methods', 'GET')

return errorResponse
}
}

// Handle POST requests by redirecting to GET
function doPost(e) {
return doGet(e)
}
55 changes: 55 additions & 0 deletions src/views/dashboard/google-apps-script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
function doGet(e) {
// Set CORS headers
var headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET',
'Content-Type': 'application/json',
}
try {
// Get the active spreadsheet
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet()
var sheet = spreadsheet.getSheetByName('Forms') || spreadsheet.insertSheet('Forms')

// Get parameters from the request
var params = e.parameter

// Append data to sheet
sheet.appendRow([
params.timestamp || new Date().toLocaleString(),
params.name || '',
params.phone || '',
params.notes || '',
params.preferredProducts || '',
params.madeOrder || 'No',
params.orderGMV || '0',
params.lat || '',
params.lng || '',
params.locationstamp || '',
])

// Return success response
return ContentService.createTextOutput(
JSON.stringify({
success: true,
message: 'Data added successfully',
}),
)
.setMimeType(ContentService.MimeType.JSON)
.setHeaders(headers)
} catch (error) {
// Return error response
return ContentService.createTextOutput(
JSON.stringify({
success: false,
error: error.toString(),
}),
)
.setMimeType(ContentService.MimeType.JSON)
.setHeaders(headers)
}
}

function doPost(e) {
// Redirect POST requests to GET
return doGet(e)
}
32 changes: 32 additions & 0 deletions src/views/dashboard/googleSheets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// googleSheets.js
import Papa from 'papaparse'

const SHEET_BASE_URL =
'https://docs.google.com/spreadsheets/d/1c0-6pkvhvFntApsvgjCrLJrs0oU9e-L1wjfpsG80Ftk/export?format=csv'

async function getSheetData(_, sheetName) {
try {
const gid = sheetName === 'Current Active Customers' ? '0' : '364360587'
const url = `${SHEET_BASE_URL}&gid=${gid}`

const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const csv = await response.text()

return new Promise((resolve) => {
Papa.parse(csv, {
header: true,
complete: (results) => {
resolve(results.data)
},
})
})
} catch (error) {
console.error('Error fetching sheet data:', error)
return []
}
}

export default { getSheetData }
219 changes: 219 additions & 0 deletions src/views/forms/CustomerForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import React, { useState, useEffect } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import {
CCard,
CCardBody,
CCardHeader,
CCol,
CRow,
CForm,
CFormInput,
CFormLabel,
CFormSelect,
CButton,
CAlert,
CSpinner
} from '@coreui/react'
import { useAuth } from "../../context/AuthContext"

// Google Apps Script Web App URL - Replace with your actual deployment URL
const GOOGLE_APPS_SCRIPT_URL = 'https://script.google.com/macros/s/AKfycbxsBrKuKOlKJVvOCUR4dAxuKLfM3eGjw3rPZIcsLYNVD1lgTip6xcYJgBBf_yi7r9d3/exec'

const CustomerForm = () => {
const location = useLocation()
const { user } = useAuth();
const navigate = useNavigate();

useEffect(() => {
if (!user) {
navigate('/login');
}
}, [user, navigate]);

const [formData, setFormData] = useState({
name: '',
phone: '',
notes: '',
lat: '',
lng: '',
preferredProducts: '',
madeOrder: 'No',
orderGMV: '0'
})
const [isSubmitting, setIsSubmitting] = useState(false)
const [success, setSuccess] = useState(false)
const [error, setError] = useState(null)

useEffect(() => {
const params = new URLSearchParams(location.search)
setFormData(f => ({
...f,
name: decodeURIComponent(params.get('name') || ''),
phone: decodeURIComponent(params.get('phone') || ''),
notes: decodeURIComponent(params.get('notes') || ''),
lat: params.get('lat') || '',
lng: params.get('lng') || ''
}))
}, [location.search])

const handleSubmit = async (e) => {
e.preventDefault()
setIsSubmitting(true)
setError(null)
setSuccess(false)

try {
// Get current location
let locationstamp = ''
if (navigator.geolocation) {
try {
const position = await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
timeout: 10000,
maximumAge: 0
})
})
locationstamp = `${position.coords.latitude},${position.coords.longitude}`
} catch (err) {
console.warn('Failed to get location:', err)
// Continue without location
}
}

const formPayload = {
...formData,
locationstamp,
assignee: user.username // Add the current user as assignee
}

// Using fetch with no-cors mode and URL parameters
const params = new URLSearchParams({
name: formPayload.name,
phone: formPayload.phone,
notes: formPayload.notes,
preferredProducts: formPayload.preferredProducts,
madeOrder: formPayload.madeOrder,
orderGMV: formPayload.orderGMV,
lat: formPayload.lat,
lng: formPayload.lng,
locationstamp: formPayload.locationstamp,
assignee: formPayload.assignee
});

const response = await fetch(`${GOOGLE_APPS_SCRIPT_URL}?${params.toString()}`, {
method: 'GET',
mode: 'no-cors',
});

// Since we're using no-cors, we won't get a JSON response
// Instead, we'll assume success if the request didn't throw an error
setSuccess(true);

// Reset only certain fields
setFormData(f => ({
...f,
preferredProducts: '',
madeOrder: 'No',
orderGMV: '0'
}))
} catch (err) {
console.error('Form submission error:', err)
setError('Failed to submit form. Please try again.')
} finally {
setIsSubmitting(false)
}
}

return (
<CRow className="justify-content-center">
<CCol xs={12} md={8} lg={6}>
<CCard className="mb-4">
<CCardHeader>Submit Customer Form</CCardHeader>
<CCardBody>
{success && (
<CAlert color="success" dismissible>
Form submitted successfully!
</CAlert>
)}
{error && (
<CAlert color="danger" dismissible>
{error}
</CAlert>
)}
<CForm onSubmit={handleSubmit}>
<CFormLabel htmlFor="name">Name</CFormLabel>
<CFormInput
id="name"
name="name"
value={formData.name}
onChange={(e) => setFormData(f => ({ ...f, name: e.target.value }))}
required
/>

<CFormLabel htmlFor="phone" className="mt-3">Phone</CFormLabel>
<CFormInput
id="phone"
name="phone"
value={formData.phone}
onChange={(e) => setFormData(f => ({ ...f, phone: e.target.value }))}
required
/>

<CFormLabel htmlFor="notes" className="mt-3">Notes</CFormLabel>
<CFormInput
id="notes"
name="notes"
value={formData.notes}
onChange={(e) => setFormData(f => ({ ...f, notes: e.target.value }))}
/>

<CFormLabel htmlFor="preferredProducts" className="mt-3">Preferred Products</CFormLabel>
<CFormInput
id="preferredProducts"
name="preferredProducts"
value={formData.preferredProducts}
onChange={(e) => setFormData(f => ({ ...f, preferredProducts: e.target.value }))}
/>

<CFormLabel htmlFor="madeOrder" className="mt-3">Made an Order?</CFormLabel>
<CFormSelect
id="madeOrder"
name="madeOrder"
value={formData.madeOrder}
onChange={(e) => setFormData(f => ({ ...f, madeOrder: e.target.value }))}
>
<option value="No">No</option>
<option value="Yes">Yes</option>
</CFormSelect>

<CFormLabel htmlFor="orderGMV" className="mt-3">Created Order GMV</CFormLabel>
<CFormInput
id="orderGMV"
name="orderGMV"
type="number"
min="0"
value={formData.orderGMV}
onChange={(e) => setFormData(f => ({ ...f, orderGMV: e.target.value }))}
/>

<CFormLabel htmlFor="location" className="mt-3">Customer Location</CFormLabel>
<CFormInput
id="location"
value={`${formData.lat}, ${formData.lng}`}
readOnly
/>

<div className="mt-4 d-flex justify-content-end">
<CButton type="submit" color="primary" disabled={isSubmitting}>
{isSubmitting ? <CSpinner size="sm" /> : 'Submit'}
</CButton>
</div>
</CForm>
</CCardBody>
</CCard>
</CCol>
</CRow>
)
}

export default CustomerForm
44 changes: 39 additions & 5 deletions src/views/pages/login/Login.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import { Link } from 'react-router-dom'
import React, { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import {
CButton,
CCard,
@@ -12,11 +12,31 @@ import {
CInputGroup,
CInputGroupText,
CRow,
CAlert,
} from '@coreui/react'
import { useAuth } from '../../../context/AuthContext'
import CIcon from '@coreui/icons-react'
import { cilLockLocked, cilUser } from '@coreui/icons'

const Login = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const { login } = useAuth();
const navigate = useNavigate();

const handleSubmit = (e) => {
e.preventDefault();
setError('');

const success = login(username, password);
if (success) {
navigate('/');
} else {
setError('Invalid username or password');
}
};

return (
<div className="bg-body-tertiary min-vh-100 d-flex flex-row align-items-center">
<CContainer>
@@ -25,14 +45,25 @@ const Login = () => {
<CCardGroup>
<CCard className="p-4">
<CCardBody>
<CForm>
<CForm onSubmit={handleSubmit}>
<h1>Login</h1>
<p className="text-body-secondary">Sign In to your account</p>
{error && (
<CAlert color="danger" className="mb-3">
{error}
</CAlert>
)}
<CInputGroup className="mb-3">
<CInputGroupText>
<CIcon icon={cilUser} />
</CInputGroupText>
<CFormInput placeholder="Username" autoComplete="username" />
<CFormInput
placeholder="Username"
autoComplete="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</CInputGroup>
<CInputGroup className="mb-4">
<CInputGroupText>
@@ -42,11 +73,14 @@ const Login = () => {
type="password"
placeholder="Password"
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</CInputGroup>
<CRow>
<CCol xs={6}>
<CButton color="primary" className="px-4">
<CButton type="submit" color="primary" className="px-4">
Login
</CButton>
</CCol>
113 changes: 113 additions & 0 deletions src/views/pages/users/MultiPagesTables.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React, { useState, useEffect } from 'react'
import {
CCard, CCardBody, CCardHeader, CCol, CRow, CTable, CTableBody,
CTableDataCell, CTableHead, CTableHeaderCell, CTableRow, CPagination, CPaginationItem, CSpinner, CAlert
} from '@coreui/react'
import googleSheets from '../../dashboard/googleSheets'

const PAGE_SIZE = 20

const MultiPagesTables = () => {
const [currentPage, setCurrentPage] = useState(1)
const [tableData, setTableData] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)

useEffect(() => {
const fetchCustomerData = async () => {
try {
setIsLoading(true)
const spreadsheetId = '1c0-6pkvhvFntApsvgjCrLJrs0oU9e-L1wjfpsG80Ftk'
const activeCustomers = await googleSheets.getSheetData(spreadsheetId, 'Current Active Customers')
const newCustomers = await googleSheets.getSheetData(spreadsheetId, 'New Customers')
// Add page name based on source
const activeCustomersWithPage = activeCustomers.map(row => ({
...row,
PageName: 'Current Active Customers',
}))
const newCustomersWithPage = newCustomers.map(row => ({
...row,
PageName: 'New Customers',
}))
const formattedData = [...activeCustomersWithPage, ...newCustomersWithPage]
.filter(row => row && row['اسم العميل'])
.map(row => ({
CustomerID: row['CustomerID'] || '',
CustomerName: row['اسم العميل'] || '',
ManagerName: row['اسم المسؤول'] || '',
Mobile: row['رقم الموبايل'] || '',
Area: row['منطقة العميل'] || '',
BusinessType: row['نوع النشاط'] || '',
PageName: row['PageName'] || ''
}))
setTableData(formattedData)
if (formattedData.length === 0) {
setError('No mapped data. Check field names. Raw data: ' + JSON.stringify([...activeCustomers, ...newCustomers].slice(0, 3)))
}
} catch (error) {
setError('Error fetching customer data')
setTableData([])
} finally {
setIsLoading(false)
}
}
fetchCustomerData()
}, [])

const totalPages = Math.ceil(tableData.length / PAGE_SIZE)
const pagedData = tableData.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE)

return (
<CRow>
<CCol xs>
<CCard className="mb-4">
<CCardHeader>Customer Information (Paginated)</CCardHeader>
<CCardBody>
{isLoading && <CSpinner className="my-4" />}
{error && <CAlert color="danger">{error}</CAlert>}
{!isLoading && !error && pagedData.length > 0 && (
<CTable align="middle" className="mb-0 border" hover responsive>
<CTableHead className="text-nowrap">
<CTableRow>
<CTableHeaderCell className="bg-body-tertiary text-center">CustomerID</CTableHeaderCell>
<CTableHeaderCell className="bg-body-tertiary text-center">اسم العميل</CTableHeaderCell>
<CTableHeaderCell className="bg-body-tertiary text-center">اسم المسؤول</CTableHeaderCell>
<CTableHeaderCell className="bg-body-tertiary text-center">رقم الموبايل</CTableHeaderCell>
<CTableHeaderCell className="bg-body-tertiary text-center">منطقة العميل</CTableHeaderCell>
<CTableHeaderCell className="bg-body-tertiary text-center">نوع النشاط</CTableHeaderCell>
<CTableHeaderCell className="bg-body-tertiary text-center">Page Name</CTableHeaderCell>
</CTableRow>
</CTableHead>
<CTableBody>
{pagedData.map((item, index) => (
<CTableRow key={index}>
<CTableDataCell className="text-center">{item.CustomerID}</CTableDataCell>
<CTableDataCell className="text-center">{item.CustomerName}</CTableDataCell>
<CTableDataCell className="text-center">{item.ManagerName}</CTableDataCell>
<CTableDataCell className="text-center">{item.Mobile}</CTableDataCell>
<CTableDataCell className="text-center">{item.Area}</CTableDataCell>
<CTableDataCell className="text-center">{item.BusinessType}</CTableDataCell>
<CTableDataCell className="text-center">{item.PageName}</CTableDataCell>
</CTableRow>
))}
</CTableBody>
</CTable>
)}
<div className="d-flex justify-content-center mt-3">
<CPagination aria-label="Page navigation example">
{[...Array(totalPages)].map((_, idx) => (
<CPaginationItem key={idx} active={currentPage === idx + 1} onClick={() => setCurrentPage(idx + 1)}>{idx + 1}</CPaginationItem>
))}
</CPagination>
</div>
{!isLoading && !error && pagedData.length === 0 && (
<div className="text-center text-body-secondary my-4">No customer data found.</div>
)}
</CCardBody>
</CCard>
</CCol>
</CRow>
)
}

export default MultiPagesTables
147 changes: 147 additions & 0 deletions src/views/pages/users/Users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React, { useState, useEffect } from 'react'
import {
CCard, CCardBody, CCardHeader, CCol, CRow, CTable, CTableBody,
CTableDataCell, CTableHead, CTableHeaderCell, CTableRow, CSpinner, CAlert, CPagination, CPaginationItem
} from '@coreui/react'
import { cilPeople } from '@coreui/icons'
import { cibCcMastercard } from '@coreui/icons'
import googleSheets from '../../dashboard/googleSheets'
import avatar1 from 'src/assets/images/avatars/1.jpg'

const PAGE_SIZE = 20
const Users = () => {
const [tableData, setTableData] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
const [currentPage, setCurrentPage] = useState(1)

useEffect(() => {
const fetchCustomerData = async () => {
try {
setIsLoading(true)
const spreadsheetId = '1c0-6pkvhvFntApsvgjCrLJrs0oU9e-L1wjfpsG80Ftk'
const activeCustomers = await googleSheets.getSheetData(spreadsheetId, 'Current Active Customers')
const newCustomers = await googleSheets.getSheetData(spreadsheetId, 'New Customers')
const activeCustomersWithPage = activeCustomers.map(row => ({ ...row, PageName: 'Current Active Customers' }))
const newCustomersWithPage = newCustomers.map(row => ({ ...row, PageName: 'New Customers' }))
const formattedData = [...activeCustomersWithPage, ...newCustomersWithPage]
.filter(row => row && row['اسم العميل'])
.map(row => ({
CustomerID: row['CustomerID'] || '',
CustomerName: row['اسم العميل'] || '',
ManagerName: row['اسم المسؤول'] || '',
Mobile: row['رقم الموبايل'] || '',
Area: row['منطقة العميل'] || '',
BusinessType: row['نوع النشاط'] || '',
PageName: row['PageName'] || ''
}))
setTableData(formattedData)
if (formattedData.length === 0) {
setError('No mapped data. Check field names. Raw data: ' + JSON.stringify([...activeCustomers, ...newCustomers].slice(0, 3)))
}
} catch (error) {
setError('Error fetching customer data')
setTableData([])
} finally {
setIsLoading(false)
}
}
fetchCustomerData()
}, [])

const totalPages = Math.ceil(tableData.length / PAGE_SIZE)
const pagedData = tableData.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE)

const getPaginationItems = (currentPage, totalPages) => {
const items = []
if (totalPages <= 7) {
for (let i = 1; i <= totalPages; i++) {
items.push(i)
}
} else {
items.push(1)
if (currentPage > 4) {
items.push('ellipsis-prev')
}
let start = Math.max(2, currentPage - 2)
let end = Math.min(totalPages - 1, currentPage + 2)
for (let i = start; i <= end; i++) {
items.push(i)
}
if (currentPage < totalPages - 3) {
items.push('ellipsis-next')
}
items.push(totalPages)
}
return items
}

return (
<CRow>
<CCol xs>
<CCard className="mb-4">
<CCardHeader>Customer Information</CCardHeader>
<CCardBody>
{isLoading && <CSpinner className="my-4" />}
{error && <CAlert color="danger">{error}</CAlert>}
{!isLoading && !error && pagedData.length > 0 && (
<>
<CTable align="middle" className="mb-0 border" hover responsive>
<CTableHead className="text-nowrap">
<CTableRow>
<CTableHeaderCell className="bg-body-tertiary text-center">CustomerID</CTableHeaderCell>
<CTableHeaderCell className="bg-body-tertiary text-center">اسم العميل</CTableHeaderCell>
<CTableHeaderCell className="bg-body-tertiary text-center">اسم المسؤول</CTableHeaderCell>
<CTableHeaderCell className="bg-body-tertiary text-center">رقم الموبايل</CTableHeaderCell>
<CTableHeaderCell className="bg-body-tertiary text-center">منطقة العميل</CTableHeaderCell>
<CTableHeaderCell className="bg-body-tertiary text-center">نوع النشاط</CTableHeaderCell>
<CTableHeaderCell className="bg-body-tertiary text-center">Page Name</CTableHeaderCell>
</CTableRow>
</CTableHead>
<CTableBody>
{pagedData.map((item, index) => (
<CTableRow key={index}>
<CTableDataCell className="text-center">{item.CustomerID}</CTableDataCell>
<CTableDataCell className="text-center">{item.CustomerName}</CTableDataCell>
<CTableDataCell className="text-center">{item.ManagerName}</CTableDataCell>
<CTableDataCell className="text-center">{item.Mobile}</CTableDataCell>
<CTableDataCell className="text-center">{item.Area}</CTableDataCell>
<CTableDataCell className="text-center">{item.BusinessType}</CTableDataCell>
<CTableDataCell className="text-center">{item.PageName}</CTableDataCell>
</CTableRow>
))}
</CTableBody>
</CTable>
<div className="d-flex align-items-center justify-content-between mt-3">
<CPagination aria-label="Page navigation example">
<CPaginationItem disabled={currentPage === 1} onClick={() => setCurrentPage(currentPage - 1)}>&lt;</CPaginationItem>
{getPaginationItems(currentPage, totalPages).map((item, idx) => {
if (item === 'ellipsis-prev' || item === 'ellipsis-next') {
return <CPaginationItem key={item + idx} disabled>...</CPaginationItem>
}
return (
<CPaginationItem
key={item}
active={currentPage === item}
onClick={() => setCurrentPage(item)}
>
{item}
</CPaginationItem>
)
})}
<CPaginationItem disabled={currentPage === totalPages} onClick={() => setCurrentPage(currentPage + 1)}>&gt;</CPaginationItem>
</CPagination>
</div>
</>
)}
{!isLoading && !error && tableData.length === 0 && (
<div className="text-center text-body-secondary my-4">No customer data found.</div>
)}
</CCardBody>
</CCard>
</CCol>
</CRow>
)
}

export default Users