From 4d6c58c3d75a0ea058d31fd8de52224bef9a0527 Mon Sep 17 00:00:00 2001 From: tendomart Date: Wed, 29 Oct 2025 17:54:25 +0300 Subject: [PATCH] O3-5126:Should be able to select a cash point on the billing module --- .../billing-dashboard.component.tsx | 16 ++- .../billing-header.component.tsx | 35 +----- src/billing-header/billing-header.scss | 51 -------- src/bills-table/bills-table.component.tsx | 9 +- .../cash-point-selector.component.tsx | 103 ++++++++++++++++ .../cash-point-selector.scss | 112 ++++++++++++++++++ src/hooks/selectedCashPointContext.ts | 14 +++ src/metrics-cards/metrics-cards.component.tsx | 13 +- translations/en.json | 6 + 9 files changed, 264 insertions(+), 95 deletions(-) create mode 100644 src/cash-point-selector/cash-point-selector.component.tsx create mode 100644 src/cash-point-selector/cash-point-selector.scss create mode 100644 src/hooks/selectedCashPointContext.ts diff --git a/src/billing-dashboard/billing-dashboard.component.tsx b/src/billing-dashboard/billing-dashboard.component.tsx index 3393d479..ee9e6fc9 100644 --- a/src/billing-dashboard/billing-dashboard.component.tsx +++ b/src/billing-dashboard/billing-dashboard.component.tsx @@ -6,12 +6,15 @@ import { omrsDateFormat } from '../constants'; import BillingHeader from '../billing-header/billing-header.component'; import BillsTable from '../bills-table/bills-table.component'; import MetricsCards from '../metrics-cards/metrics-cards.component'; +import CashPointSelector from '../cash-point-selector/cash-point-selector.component'; import SelectedDateContext from '../hooks/selectedDateContext'; +import SelectedCashPointContext from '../hooks/selectedCashPointContext'; import styles from './billing-dashboard.scss'; export function BillingDashboard() { const { t } = useTranslation(); const [selectedDate, setSelectedDate] = useState(dayjs().startOf('day').format(omrsDateFormat)); + const [selectedCashPoint, setSelectedCashPoint] = useState(null); const params = useParams(); @@ -23,11 +26,14 @@ export function BillingDashboard() { return ( - - -
- -
+ + + + +
+ +
+
); } diff --git a/src/billing-header/billing-header.component.tsx b/src/billing-header/billing-header.component.tsx index 66fc2bc5..6a6cbce5 100644 --- a/src/billing-header/billing-header.component.tsx +++ b/src/billing-header/billing-header.component.tsx @@ -1,12 +1,6 @@ -import React, { useContext } from 'react'; -import dayjs from 'dayjs'; -import { DatePickerInput, DatePicker } from '@carbon/react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Location, UserFollow } from '@carbon/react/icons'; -import { useSession } from '@openmrs/esm-framework'; -import { omrsDateFormat } from '../constants'; import BillingIllustration from './billing-illustration.component'; -import SelectedDateContext from '../hooks/selectedDateContext'; import styles from './billing-header.scss'; interface BillingHeaderProps { @@ -15,9 +9,6 @@ interface BillingHeaderProps { const BillingHeader: React.FC = ({ title }) => { const { t } = useTranslation(); - const session = useSession(); - const location = session?.sessionLocation?.display; - const { selectedDate, setSelectedDate } = useContext(SelectedDateContext); return (
@@ -28,30 +19,6 @@ const BillingHeader: React.FC = ({ title }) => {

{title}

-
-
-

{session?.user?.person?.display}

- -
-
- - {location} - · - setSelectedDate(dayjs(date).startOf('day').format(omrsDateFormat))} - value={dayjs(selectedDate).format('DD MMM YYYY')} - dateFormat="d-M-Y" - datePickerType="single"> - - -
-
); }; diff --git a/src/billing-header/billing-header.scss b/src/billing-header/billing-header.scss index 8da0d4ea..fd6005f0 100644 --- a/src/billing-header/billing-header.scss +++ b/src/billing-header/billing-header.scss @@ -9,7 +9,6 @@ background-color: $ui-02; border-bottom: 1px solid $ui-03; display: flex; - justify-content: space-between; padding: layout.$spacing-05; } @@ -18,15 +17,6 @@ flex-direction: row; align-items: center; cursor: pointer; - align-items: center; -} - -.right-justified-items { - @include type.type-style('body-compact-02'); - color: $text-02; - display: flex; - flex-direction: column; - justify-content: space-between; } .page-name { @@ -40,44 +30,3 @@ margin-bottom: layout.$spacing-02; } } - -.date-and-location { - display: flex; - justify-content: flex-end; - align-items: center; -} - -.userContainer { - display: flex; - justify-content: flex-end; - gap: layout.$spacing-05; -} - -.value { - margin-left: layout.$spacing-02; -} - -.middot { - margin: 0 layout.$spacing-03; -} - -.view { - @include type.type-style('label-01'); -} - -// Overriding styles for RTL support -html[dir='rtl'] { - .date-and-location { - & > svg { - order: -1; - } - & > span:nth-child(2) { - order: -2; - } - } -} - -.userIcon { - fill: $ui-05; - margin: layout.$spacing-01; -} diff --git a/src/bills-table/bills-table.component.tsx b/src/bills-table/bills-table.component.tsx index 5231a91f..60efbadc 100644 --- a/src/bills-table/bills-table.component.tsx +++ b/src/bills-table/bills-table.component.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useId, useMemo, useState } from 'react'; +import React, { useCallback, useContext, useId, useMemo, useState } from 'react'; import classNames from 'classnames'; import { DataTable, @@ -28,6 +28,7 @@ import { } from '@openmrs/esm-framework'; import { EmptyDataIllustration } from '@openmrs/esm-patient-common-lib'; import { useBills } from '../billing.resource'; +import SelectedCashPointContext from '../hooks/selectedCashPointContext'; import styles from './bills-table.scss'; const BillsTable = () => { @@ -41,6 +42,7 @@ const BillsTable = () => { const [pageSize, setPageSize] = useState(config?.bills?.pageSize ?? 10); const { bills, isLoading, isValidating, error } = useBills('', ''); const [searchString, setSearchString] = useState(''); + const { selectedCashPoint } = useContext(SelectedCashPointContext); const headerData = [ { @@ -75,15 +77,16 @@ const BillsTable = () => { return bill; }) .filter((bill) => { + const cashPointMatch = !selectedCashPoint || bill.cashPointUuid === selectedCashPoint.uuid; const statusMatch = billPaymentStatus === '' ? true : bill.status === billPaymentStatus; const searchMatch = !searchString ? true : bill.patientName.toLowerCase().includes(searchString.toLowerCase()) || bill.identifier.toLowerCase().includes(searchString.toLowerCase()); - return statusMatch && searchMatch; + return cashPointMatch && statusMatch && searchMatch; }); - }, [bills, searchString, billPaymentStatus]); + }, [bills, searchString, billPaymentStatus, selectedCashPoint]); const { paginated, goTo, results, currentPage } = usePagination(searchResults, pageSize); diff --git a/src/cash-point-selector/cash-point-selector.component.tsx b/src/cash-point-selector/cash-point-selector.component.tsx new file mode 100644 index 00000000..8bb0e940 --- /dev/null +++ b/src/cash-point-selector/cash-point-selector.component.tsx @@ -0,0 +1,103 @@ +import React, { useState, useEffect, useContext } from 'react'; +import { InlineLoading, Tag, Dropdown } from '@carbon/react'; +import { Time, Location } from '@carbon/react/icons'; +import { useTranslation } from 'react-i18next'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; +import duration from 'dayjs/plugin/duration'; +import { getCoreTranslation, useSession } from '@openmrs/esm-framework'; +import { useCashPoint } from '../billing-form/billing-form.resource'; +import SelectedCashPointContext from '../hooks/selectedCashPointContext'; +import styles from './cash-point-selector.scss'; + +dayjs.extend(relativeTime); +dayjs.extend(duration); + +export default function CashPointSelector() { + const { t } = useTranslation(); + const session = useSession(); + const { cashPoints, isLoading, error } = useCashPoint(); + const { selectedCashPoint, setSelectedCashPoint } = useContext(SelectedCashPointContext); + const [clockInTime] = useState(dayjs()); + const [currentTime, setCurrentTime] = useState(dayjs()); + + const userName = session?.user?.person?.display; + const location = session?.sessionLocation?.display; + + useEffect(() => { + if (cashPoints && cashPoints.length > 0 && !selectedCashPoint) { + setSelectedCashPoint(cashPoints[0]); + } + }, [cashPoints, selectedCashPoint, setSelectedCashPoint]); + + useEffect(() => { + const interval = setInterval(() => { + setCurrentTime(dayjs()); + }, 60000); // Update every minute + + return () => clearInterval(interval); + }, []); + + const getDuration = () => { + const diff = currentTime.diff(clockInTime); + const dur = dayjs.duration(diff); + const hours = Math.floor(dur.asHours()); + return `+${hours}hs`; + }; + + const handleCashPointChange = ({ selectedItem }) => { + setSelectedCashPoint(selectedItem); + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error || !cashPoints || cashPoints.length === 0) { + return null; + } + + return ( +
+
+
+
+
+
+ {userName} +
+ + {location} +
+
+ (item ? item.name : '')} + selectedItem={selectedCashPoint} + onChange={handleCashPointChange} + label={t('selectCashPoint', 'Select cash point')} + titleText="" + size="md" + className={styles.cashPointDropdown} + /> +
+
+
+ ); +} diff --git a/src/cash-point-selector/cash-point-selector.scss b/src/cash-point-selector/cash-point-selector.scss new file mode 100644 index 00000000..49921433 --- /dev/null +++ b/src/cash-point-selector/cash-point-selector.scss @@ -0,0 +1,112 @@ +@use '@carbon/colors'; +@use '@carbon/layout'; +@use '@carbon/type'; + +.container { + background-color: colors.$white; + border-bottom: 1px solid colors.$gray-20; + padding: layout.$spacing-05 layout.$spacing-05; +} + +.content { + display: flex; + align-items: center; + justify-content: space-between; + gap: layout.$spacing-05; + max-width: 100%; +} + +.clockInInfo { + display: flex; + align-items: center; + gap: layout.$spacing-03; +} + +.clockIcon { + color: colors.$gray-70; + flex-shrink: 0; +} + +.clockInText { + @include type.type-style('body-compact-01'); + color: colors.$gray-100; + white-space: nowrap; +} + +.durationTag { + margin-left: layout.$spacing-02; +} + +.rightSection { + display: flex; + align-items: center; + gap: layout.$spacing-06; +} + +.userInfo { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: layout.$spacing-02; +} + +.userName { + @include type.type-style('body-compact-01'); + color: colors.$gray-100; + font-weight: 600; + white-space: nowrap; +} + +.locationInfo { + display: flex; + align-items: center; + gap: layout.$spacing-02; +} + +.locationIcon { + color: colors.$gray-70; + flex-shrink: 0; +} + +.locationText { + @include type.type-style('body-compact-01'); + color: colors.$gray-70; + white-space: nowrap; +} + +.cashPointDropdown { + min-width: 250px; + max-width: 350px; + + :global(.cds--list-box__field) { + background-color: colors.$white; + } +} + +@media (max-width: 768px) { + .content { + flex-direction: column; + align-items: flex-start; + gap: layout.$spacing-04; + } + + .clockInText { + white-space: normal; + } + + .rightSection { + width: 100%; + flex-direction: column; + align-items: flex-start; + gap: layout.$spacing-04; + } + + .userInfo { + align-items: flex-start; + } + + .cashPointDropdown { + width: 100%; + max-width: 100%; + } +} diff --git a/src/hooks/selectedCashPointContext.ts b/src/hooks/selectedCashPointContext.ts new file mode 100644 index 00000000..38d66d3f --- /dev/null +++ b/src/hooks/selectedCashPointContext.ts @@ -0,0 +1,14 @@ +import { createContext } from 'react'; +import type { OpenmrsResource } from '@openmrs/esm-framework'; + +interface SelectedCashPointContextType { + selectedCashPoint: OpenmrsResource | null; + setSelectedCashPoint: (cashPoint: OpenmrsResource | null) => void; +} + +const SelectedCashPointContext = createContext({ + selectedCashPoint: null, + setSelectedCashPoint: () => {}, +}); + +export default SelectedCashPointContext; diff --git a/src/metrics-cards/metrics-cards.component.tsx b/src/metrics-cards/metrics-cards.component.tsx index 21388868..be21e804 100644 --- a/src/metrics-cards/metrics-cards.component.tsx +++ b/src/metrics-cards/metrics-cards.component.tsx @@ -1,17 +1,26 @@ -import React, { useMemo } from 'react'; +import React, { useContext, useMemo } from 'react'; import { InlineLoading } from '@carbon/react'; import { useTranslation } from 'react-i18next'; import { ErrorState } from '@openmrs/esm-patient-common-lib'; import { getCoreTranslation } from '@openmrs/esm-framework'; import { useBills } from '../billing.resource'; import { useBillMetrics } from './metrics.resource'; +import SelectedCashPointContext from '../hooks/selectedCashPointContext'; import Card from './card.component'; import styles from './metrics-cards.scss'; export default function MetricsCards() { const { t } = useTranslation(); const { bills, isLoading, error } = useBills(''); - const { cumulativeBills, pendingBills, paidBills } = useBillMetrics(bills); + const { selectedCashPoint } = useContext(SelectedCashPointContext); + + // Filter bills by selected cash point + const filteredBills = useMemo(() => { + if (!selectedCashPoint || !bills) return bills; + return bills.filter((bill) => bill.cashPointUuid === selectedCashPoint.uuid); + }, [bills, selectedCashPoint]); + + const { cumulativeBills, pendingBills, paidBills } = useBillMetrics(filteredBills); const cards = useMemo( () => [ diff --git a/translations/en.json b/translations/en.json index 80b0b8ce..f994f436 100644 --- a/translations/en.json +++ b/translations/en.json @@ -66,6 +66,9 @@ "cashPointUuid": "Cash point UUID", "cashPointUuidPlaceholder": "Enter UUID", "checkFilters": "Check the filters above", + "chooseCashPoint": "Choose a cash point", + "clockedInAt": "Clocked in at", + "clockedInOn": "Clocked in on", "confirmDeleteMessage": "Are you sure you want to delete this payment mode? Proceed cautiously.", "cumulativeBills": "Cumulative bills", "currentPrice": "Current price", @@ -126,6 +129,7 @@ "loading": "Loading data", "loadingBillInfo": "Loading bill information", "loadingBillingServices": "Loading billing services", + "loadingCashPoints": "Loading cash points", "loadingBillItems": "Loading bill items", "loadingBillMetrics": "Loading bill metrics", "loadingDescription": "Loading", @@ -194,6 +198,7 @@ "searchItems": "Search items and services", "searchThisTable": "Search this table", "selectBillableService": "Select a billable service", + "selectCashPoint": "Select cash point", "selectedItems": "Selected items", "selectLocation": "Select location", "selectPatientCategory": "Select patient category", @@ -214,6 +219,7 @@ "status": "Service status", "submitting": "Submitting", "success": "Success", + "switchPaymentPoint": "Switch Payment Point", "total": "Total", "totalAmount": "Total amount", "totalTendered": "Total tendered",