diff --git a/package-lock.json b/package-lock.json index ee62573..ab03d75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -126,7 +126,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -545,7 +544,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.4.tgz", "integrity": "sha512-xMF6OfEAUVY5Waega4juo1QGACfNkNF+aJLqpd8oUJz96ms2zbfQ9Gh35/tI3y8akEV31FruKfj7hBnIU/nkqA==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -570,7 +568,6 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", - "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -1203,7 +1200,6 @@ "resolved": "https://registry.npmjs.org/@graphiql/react/-/react-0.37.3.tgz", "integrity": "sha512-rNJjwsYGhcZRdZ2FnyU6ss06xQaZ4UordyvOhp7+b/bEqQiEBpMOLJjuUr48Z6T7zEbZBnzCJpIJyXNqlcfQeA==", "license": "MIT", - "peer": true, "dependencies": { "@graphiql/toolkit": "^0.11.3", "@radix-ui/react-dialog": "^1.1", @@ -2631,7 +2627,6 @@ "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.5.tgz", "integrity": "sha512-/VNHWYhNu+BS7ktbYoVGrCmsXDh+chFMaONMwGNdIBcFHrWqk2jY8fNyr3DLdtQUIalvkPfM554ZSFa3dm3nxQ==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/Fuzzyma" @@ -2655,7 +2650,6 @@ "resolved": "https://registry.npmjs.org/@svgdotjs/svg.select.js/-/svg.select.js-4.0.3.tgz", "integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 14.18" }, @@ -2813,7 +2807,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -3030,7 +3025,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3096,7 +3090,6 @@ "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-4.7.0.tgz", "integrity": "sha512-iZSrrBGvVlL+nt2B1NpqfDuBZ9jX61X9I2+XV0hlYXHtTwhwLTHDKGXjNXAgFBDLuvSYCB/rq2nPWVPRv2DrGA==", "license": "MIT", - "peer": true, "dependencies": { "@svgdotjs/svg.draggable.js": "^3.0.4", "@svgdotjs/svg.filter.js": "^3.0.8", @@ -3346,7 +3339,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3732,7 +3724,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dom-serializer": { "version": "2.0.0", @@ -4084,7 +4077,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4719,7 +4711,6 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", "license": "MIT", - "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -4761,7 +4752,6 @@ "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.16.2.tgz", "integrity": "sha512-E1uccsZxt/96jH/OwmLPuXMACILs76pKF2i3W861LpKBCYtGIyPQGtWLuBLkND4ox1KHns70e83PS4te50nvPQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -5647,6 +5637,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -5740,8 +5731,7 @@ "version": "0.52.2", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/monaco-graphql": { "version": "1.7.3", @@ -6094,7 +6084,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6168,7 +6157,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -6185,6 +6173,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -6200,6 +6189,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -6212,7 +6202,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/prop-types": { "version": "15.8.1", @@ -6270,7 +6261,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6293,7 +6283,6 @@ "resolved": "https://registry.npmjs.org/react-compiler-runtime/-/react-compiler-runtime-19.1.0-rc.1.tgz", "integrity": "sha512-wCt6g+cRh8g32QT18/9blfQHywGjYu+4FlEc3CW1mx3pPxYzZZl1y+VtqxRgnKKBCFLIGUYxog4j4rs5YS86hw==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental" } @@ -6342,7 +6331,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -7202,8 +7190,7 @@ "version": "7.9.1", "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-7.9.1.tgz", "integrity": "sha512-zaOHwmiP1EqTeLRXAvVriDb00JYnfEjWGPdKEuac7MiZJ5aiDMZ4Unc98Gmajn+PBljOmO1GKV6G0KwWn3+k8A==", - "license": "GPL-2.0-or-later", - "peer": true + "license": "GPL-2.0-or-later" }, "node_modules/ts-invariant": { "version": "0.10.3", @@ -7473,7 +7460,6 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", diff --git a/src/components/copyString.jsx b/src/components/copyString.jsx new file mode 100644 index 0000000..88a3476 --- /dev/null +++ b/src/components/copyString.jsx @@ -0,0 +1,31 @@ +import React, { useState } from "react"; +import { useUniversalCopy } from '../hooks/useCopy.js'; + +export default function CopyExample({text}) { + const copy = useUniversalCopy(); + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + const ok = await copy(text); + setCopied(ok); + + // Auto-hide after 2 seconds + setTimeout(() => setCopied(false), 2000); + }; + + return ( + + + + + + + + {copied && ( +
+ Copied to clipboard +
+ )} +
+ ); +} diff --git a/src/features/widgets/components/profileWidget.jsx b/src/features/widgets/components/profileWidget.jsx index c439414..a81bd72 100644 --- a/src/features/widgets/components/profileWidget.jsx +++ b/src/features/widgets/components/profileWidget.jsx @@ -8,6 +8,7 @@ import FormatPhoneNumber from "../../../util/phoneNumberFormatter"; import Avatar from '../../../components/avatar'; import StatusPill from '../../../pages/customers/statusPill.jsx'; import SocialMediaLink from "../../../components/socialMediaLink"; +import CopyString from '../../../components/copyString.jsx'; var GET_DATA = gql`query { countries{ @@ -69,12 +70,12 @@ const ProfileWidget = ({ customer, widget }) => { Handle -
{customer.webAlias ?? customer.id}
+
{customer.webAlias ?? customer.id}
{true && <>
Email
-
{customer.emailAddress}
+
{customer.emailAddress}
diff --git a/src/hooks/useCopy.js b/src/hooks/useCopy.js new file mode 100644 index 0000000..f0f850b --- /dev/null +++ b/src/hooks/useCopy.js @@ -0,0 +1,38 @@ +import { useCallback } from "react"; + +export function useUniversalCopy() { + const copy = useCallback(async (text) => { + const isIOS = /ipad|iphone|ipod/i.test(navigator.userAgent); + + // Modern API (not reliable on iOS Safari) + if (!isIOS && navigator.clipboard && window.isSecureContext) { + try { + await navigator.clipboard.writeText(text); + return true; + } catch { + // fallback below + } + } + + // Fallback for iOS + older browsers + const textarea = document.createElement("textarea"); + textarea.value = text; + + // iOS requires visible-but-offscreen element + textarea.style.position = "fixed"; + textarea.style.left = "-9999px"; + + document.body.appendChild(textarea); + + textarea.focus(); + textarea.select(); + textarea.setSelectionRange(0, 99999); // iOS requirement + + const success = document.execCommand("copy"); + document.body.removeChild(textarea); + + return success; + }, []); + + return copy; +} diff --git a/src/pages/customers/summary.js b/src/pages/customers/summary.js new file mode 100644 index 0000000..13ab798 --- /dev/null +++ b/src/pages/customers/summary.js @@ -0,0 +1,418 @@ +import React, { useState } from 'react'; +import { useParams } from "react-router-dom" +import { useQuery, gql } from "@apollo/client"; +import { useFetch } from "../../hooks/useFetch"; +import { GetScope } from "../../features/authentication/hooks/useToken" +import PageHeader, { CardHeader } from '../../components/pageHeader'; +import ChangeStatusModal from './changeStatusModal'; +import DataLoading from '../../components/dataLoading'; +import DataError from '../../components/dataError'; +import Modal from '../../components/modal'; +import StatusPill from './statusPill'; +import Avatar from '../../components/avatar'; +import LocalDate from "../../util/LocalDate"; +import PeriodDatePicker from '../../components/periodDatePicker'; +import DataCard from '../../components/dataCard'; +import RankAdvance from '../../components/rankAdvance'; +import { SendRequest } from '../../hooks/usePost'; +import CopyString from '../../components/copyString.jsx'; + +import Widget from '../../features/widgets/components/widget'; + +var GET_CUSTOMER = gql`query ($nodeIds: [String]!, $periodDate: Date!) { + customers(idList: $nodeIds) { + id + fullName + enrollDate + profileImage + webAlias + status { + id + name + statusClass + } + emailAddress + customerType { + id + name + } + socialMedia { + name + value + } + language + customData + phoneNumbers { + type + number + } + addresses { + type + line1 + line2 + line3 + city + stateCode + zip + countryCode + } + cards(idList: ["Dashboard"], date: $periodDate) { + id + values { + value + valueName + valueId + } + } + widgets { + id + name + title + description + type + showDatePicker + headerColor + headerTextColor + headerAlignment + backgroundColor + textColor + borderColor + settings + css + panes { + imageUrl + title + text + description + values { + text + value + } + } + } + } + trees { + id + name + nodes(nodeIds: $nodeIds, date: $periodDate) { + nodeId + uplineId + uplineLeg + upline { + fullName + profileImage + } + } + } + compensationPlans { + period(date: $periodDate) { + rankAdvance(nodeIds: $nodeIds) { + nodeId + rankId + rankName + requirements { + maintanance + conditions { + legCap + legValue + required + value + valueId + } + } + } + } + } + customerStatuses { + id + name + statusClass + earningsClass + } +}`; + +Array.prototype.firstOrDefault = function () { + if (this.length == 0) return null; + return this[0]; +}; + + +const CustomerSummary = () => { + let params = useParams() + //const queryParams = new URLSearchParams(window.location.search) + //const periodParam = queryParams.get("periodId") ?? "0"; + const [status, setStatus] = useState({ id: "INIT", name: "INIT" }); + const [periodDate, setPeriodDate] = useState(new Date().toISOString()); + const [showDelete, setShowDelete] = useState(false); + const { data: dashboard, loading: dbLoading, error: dbError } = useFetch('/api/v1/dashboards/CSDB', {}, { id: 'CSDB', children: [] }); + + const { loading, error, data, refetch } = useQuery(GET_CUSTOMER, { + variables: { nodeIds: [params.customerId], periodDate: periodDate }, + }); + + const handlePeriodChange = (name, value) => { + setPeriodDate(value); + refetch({ nodeIds: [params.customerId], periodDate: value }); + }; + + if (loading || dbLoading) return ; + if (error) return ; + if (dbError) return ; + + const handleHide = () => setShowDelete(false); + const handleShow = () => setShowDelete(true); + + const handleDelete = () => { + SendRequest("DELETE", `/api/v1/Customers/${params.customerId}`, {}, () => { + window.location = '/'; + }, (error) => { + alert(error) + }) + } + + const hasScope = GetScope() != undefined; + + let customer = data.customers[0] ?? { id: params.customerId, cards: [] }; + let currentRank = customer.cards?.[0]?.values.find(v => v.valueId.toLowerCase() == 'rank')?.value ?? 0; + let rankAdvance = data.compensationPlans.flatMap(plan => plan.period || []).find(period => period.rankAdvance?.length > 0)?.rankAdvance || null; + let address = customer.addresses ? customer.addresses[0] : { line1: '' }; + let trees = data.trees; + let statuses = data.customerStatuses; + if (customer.status == undefined) customer.status = { id: "Active", name: "Active" }; + + let compensationPlans = data?.compensationPlans; + let widgets = customer?.widgets; + + if (status.id == "INIT") setStatus(customer.status); + + return <> + + + {!hasScope && <> + + } + +
+
+ {dashboard && dashboard?.children && dashboard.children.map((card) => { + return buildCard(card, widgets, customer, compensationPlans, trees, periodDate); + })} + {dashboard.children.length == 0 && <> +
+
+
+
+
+ +
+
+

+ {customer.fullName} +

+
+ {customer.customerType?.name ?? 'Unknown'} +
+
+
+ +
+
+
+
+
+
+ + Enroll date
+
+ +
+
+ + Handle +
+
{customer.webAlias ?? customer.id}
+ {!hasScope && <> +
+ + Email
+
{customer.emailAddress}
+ + +
+ + Phone
+
{customer.phoneNumbers && customer.phoneNumbers.length > 0 && customer.phoneNumbers[0].number}
+ +
+ + Language
+
{customer.language}
+ +
+ + Merchant
+
{customer.merchantId}
+ +
+ + Payment Status
+
{status.earningsClass}
+ +
+ + Shipping Address
+
+
+ {address?.line1} {address?.line2} {address?.line3}
+ {address?.city}, {address?.stateCode} {address?.postalCode}
+ {address?.countryCode} +
+
+ } +
+
+
+
+
+
+
+

Business Overview

+
+ +
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+

+ Organization +

+
+
+ {trees.map((tree) => { + return tree.nodes.map((node) => { + return + }) + })} +
+
+
+ } + {!hasScope && <> +
+
+
+

Account Activity

+
+
+ + + + + + + + + + {/* @foreach(var item in @Model.Customer.ActivityLog) + { */} + + + + + + {/* } */} + +
DateMessageSubmitted By
+
+
+
+ } + +
+ +
+
+ + +
+ + +
Are you sure?
+
Do you wish to delete '{customer.fullName}'?
+
+
+ + +
+
+ + +}; + +function buildCard(card, widgets, customer, compensationPlans, trees, date) { + if ((card?.widgetId || card?.children) && widgets !== undefined) { + let widget = widgets.find((w) => w.id === card?.widgetId ?? ''); + if (!widget && (!card.children || card.children.length == 0)) return <> + + return
6 ? '12' : '6'} col-xl-${card?.columns}`}> + {card?.widgetId && widget && } + {card.children && card.children.length > 0 && <> +
+
+ {card.children.map((c) => { + return buildCard(c, widgets, customer, compensationPlans, trees, date); + })} +
+
+ } +
+ } +} + +export default CustomerSummary; + + diff --git a/src/pages/customers/treeComponents/treeSideCard.jsx b/src/pages/customers/treeComponents/treeSideCard.jsx index 9143a13..c0429c8 100644 --- a/src/pages/customers/treeComponents/treeSideCard.jsx +++ b/src/pages/customers/treeComponents/treeSideCard.jsx @@ -8,6 +8,7 @@ import LocalDate from '../../../util/LocalDate.jsx'; import { GetScope } from "../../../features/authentication/hooks/useToken.jsx" import DataLoading from '../../../components/dataLoading.jsx'; import Widget from '../../../features/widgets/components/widget.jsx'; +import CopyString from '../../../components/copyString.jsx'; let GET_CUSTOMER = `query ($nodeIds: [String]!, $periodDate: Date!) { customers(idList: $nodeIds) { @@ -166,6 +167,7 @@ const TreeSideCard = ({ customerId, periodDate, treeId, showModal, dashboard })

{customer?.fullName}

{customer.webAlias ?? customer.id} +