From 1c5513a59588c2f5d48e51527011d94c36d091a4 Mon Sep 17 00:00:00 2001 From: Munyua123 Date: Wed, 11 Dec 2024 12:26:20 +0300 Subject: [PATCH 1/4] SJT-119 (feat) Add Payment History Module to Billing --- .../bill-item-actions/edit-bill-item.test.tsx | 1 + .../applied-filter-tages.component.tsx | 36 ++++ .../cashier-filter.component.tsx | 48 ++++++ .../payment-history/filter.component.tsx | 90 ++++++++++ .../payment-history-table.component.tsx | 97 +++++++++++ .../payment-history-viewer.component.tsx | 159 ++++++++++++++++++ .../payment-history.component.tsx | 14 ++ .../payment-history/payment-history.scss | 86 ++++++++++ .../payment-history/payment-totals.tsx | 62 +++++++ .../payment-type-filter.component.tsx | 48 ++++++ .../service-type-filter.component.tsx | 53 ++++++ .../table-toolbar-date-range.tsx | 38 +++++ .../timesheets-filter.component.tsx | 93 ++++++++++ .../use-bills-service-types.tsx | 15 ++ .../payment-history/use-payment-totals.tsx | 34 ++++ .../esm-billing-app/src/billing.resource.ts | 2 + packages/esm-billing-app/src/constants.ts | 15 ++ .../src/hooks/use-rendered-rows.ts | 89 ++++++++++ .../src/invoice/invoice-table.test.tsx | 1 + .../payment-history/payment-history.test.tsx | 2 + .../payments/payments.component.test.tsx | 1 + .../esm-billing-app/src/root.component.tsx | 11 ++ packages/esm-billing-app/src/types/index.ts | 11 ++ 23 files changed, 1006 insertions(+) create mode 100644 packages/esm-billing-app/src/billable-services/payment-history/applied-filter-tages.component.tsx create mode 100644 packages/esm-billing-app/src/billable-services/payment-history/cashier-filter.component.tsx create mode 100644 packages/esm-billing-app/src/billable-services/payment-history/filter.component.tsx create mode 100644 packages/esm-billing-app/src/billable-services/payment-history/payment-history-table.component.tsx create mode 100644 packages/esm-billing-app/src/billable-services/payment-history/payment-history-viewer.component.tsx create mode 100644 packages/esm-billing-app/src/billable-services/payment-history/payment-history.component.tsx create mode 100644 packages/esm-billing-app/src/billable-services/payment-history/payment-history.scss create mode 100644 packages/esm-billing-app/src/billable-services/payment-history/payment-totals.tsx create mode 100644 packages/esm-billing-app/src/billable-services/payment-history/payment-type-filter.component.tsx create mode 100644 packages/esm-billing-app/src/billable-services/payment-history/service-type-filter.component.tsx create mode 100644 packages/esm-billing-app/src/billable-services/payment-history/table-toolbar-date-range.tsx create mode 100644 packages/esm-billing-app/src/billable-services/payment-history/timesheets-filter.component.tsx create mode 100644 packages/esm-billing-app/src/billable-services/payment-history/use-bills-service-types.tsx create mode 100644 packages/esm-billing-app/src/billable-services/payment-history/use-payment-totals.tsx create mode 100644 packages/esm-billing-app/src/hooks/use-rendered-rows.ts diff --git a/packages/esm-billing-app/src/bill-item-actions/edit-bill-item.test.tsx b/packages/esm-billing-app/src/bill-item-actions/edit-bill-item.test.tsx index d1e3e8a..ad04ff2 100644 --- a/packages/esm-billing-app/src/bill-item-actions/edit-bill-item.test.tsx +++ b/packages/esm-billing-app/src/bill-item-actions/edit-bill-item.test.tsx @@ -50,6 +50,7 @@ const mockBill: MappedBill = { }, ], dateCreated: new Date().toISOString(), + dateCreatedUnformatted: '2023-09-01T12:00:00Z.000+0300', billingService: 'billing-service-uuid', payments: [], patientName: 'John Doe', diff --git a/packages/esm-billing-app/src/billable-services/payment-history/applied-filter-tages.component.tsx b/packages/esm-billing-app/src/billable-services/payment-history/applied-filter-tages.component.tsx new file mode 100644 index 0000000..fbbb98f --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/payment-history/applied-filter-tages.component.tsx @@ -0,0 +1,36 @@ +import { Tag } from '@carbon/react'; +import React from 'react'; +import { colorsArray } from '../../constants'; +import { useServiceTypes } from '../billable-service.resource'; +import styles from './payment-history.scss'; + +export const AppliedFilterTags = ({ appliedFilters }: { appliedFilters: Array }) => { + const { serviceTypes } = useServiceTypes(); + + const getRandomTagColor = () => { + const randomIndex = Math.floor(Math.random() * colorsArray.length); + return colorsArray[randomIndex]; + }; + + const tags = appliedFilters + .filter((f) => Boolean(f)) + .map((f) => { + const serviceType = serviceTypes.find((sT) => sT.uuid === f); + const isServiceTypeUUID = Boolean(serviceType); + return isServiceTypeUUID ? serviceType.display : f; + }); + + if (tags.length === 0) { + return; + } + + return ( +
+ {tags.map((tag, index) => ( + + {tag} + + ))} +
+ ); +}; diff --git a/packages/esm-billing-app/src/billable-services/payment-history/cashier-filter.component.tsx b/packages/esm-billing-app/src/billable-services/payment-history/cashier-filter.component.tsx new file mode 100644 index 0000000..4afc371 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/payment-history/cashier-filter.component.tsx @@ -0,0 +1,48 @@ +import { Checkbox, usePrefix } from '@carbon/react'; +import React, { ChangeEvent } from 'react'; +import { MappedBill } from '../../types'; +import styles from './payment-history.scss'; + +export const CashierFilter = ({ + selectedCashierCheckboxes, + bills, + applyCashierFilter, +}: { + selectedCashierCheckboxes: Array; + bills: MappedBill[]; + applyCashierFilter: (filters: Array) => void; +}) => { + const cashiers = Array.from(new Map(bills.map((bill) => [bill.cashier.uuid, bill.cashier])).values()); + + const prefix = usePrefix(); + + const handleCheckboxChange = (e: ChangeEvent) => { + const checkboxId = e.target.id; + const isChecked = e.target.checked; + + const checkboxValue: HTMLSpanElement | null = document.querySelector(`label[for="${checkboxId}"]`); + + if (isChecked && checkboxValue) { + applyCashierFilter([...selectedCashierCheckboxes, checkboxValue.innerText]); + } else { + applyCashierFilter(selectedCashierCheckboxes.filter((item) => item !== checkboxValue?.innerText)); + } + }; + + return ( +
+
+ Cashiers + {cashiers.length === 0 &&

No Cashiers In Bills Range

} + {cashiers.map((cashier) => ( + + ))} +
+
+ ); +}; diff --git a/packages/esm-billing-app/src/billable-services/payment-history/filter.component.tsx b/packages/esm-billing-app/src/billable-services/payment-history/filter.component.tsx new file mode 100644 index 0000000..f9f6a06 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/payment-history/filter.component.tsx @@ -0,0 +1,90 @@ +import { Button, Popover, PopoverContent, usePrefix } from '@carbon/react'; +import { Filter as FilterIcon } from '@carbon/react/icons'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { MappedBill } from '../../types'; +import { CashierFilter } from './cashier-filter.component'; +import styles from './payment-history.scss'; +import { PaymentTypeFilter } from './payment-type-filter.component'; +import { ServiceTypeFilter } from './service-type-filter.component'; + +interface FilterProps { + applyFilters: (filters: Array) => void; + resetFilters: () => void; + bills: MappedBill[]; +} + +export const Filter = ({ applyFilters, resetFilters, bills }: FilterProps) => { + const [isOpen, setIsOpen] = useState(false); + + const [selectedPaymentTypeCheckBoxes, setSelectedPaymentTypeCheckBoxes] = useState>([]); + const [selectedCashierCheckboxes, setSelectedCashierCheckboxes] = useState>([]); + const [selectedServiceTypeCheckboxes, setSelectedServiceTypeCheckboxes] = useState>([]); + + const { t } = useTranslation(); + const prefix = usePrefix(); + + const handleApplyFilter = () => { + applyFilters([...selectedCashierCheckboxes, ...selectedPaymentTypeCheckBoxes, ...selectedServiceTypeCheckboxes]); + setIsOpen(false); + }; + + const handleResetFilter = () => { + resetFilters(); + setSelectedPaymentTypeCheckBoxes([]); + setSelectedCashierCheckboxes([]); + setSelectedServiceTypeCheckboxes([]); + setIsOpen(false); + }; + + const applyCashierFilter = (appliedCashierCheckboxes: Array) => { + setSelectedCashierCheckboxes(appliedCashierCheckboxes); + }; + + const applyPaymentTypeFilter = (appliedPaymentTypeCheckboxes: Array) => { + setSelectedPaymentTypeCheckBoxes(appliedPaymentTypeCheckboxes); + }; + + const applyServiceTypeFilter = (appliedServiceTypes: Array) => { + setSelectedServiceTypeCheckboxes(appliedServiceTypes); + }; + + return ( + setIsOpen(false)}> + + +
+ + + +
+ + +
+
+ ); +}; diff --git a/packages/esm-billing-app/src/billable-services/payment-history/payment-history-table.component.tsx b/packages/esm-billing-app/src/billable-services/payment-history/payment-history-table.component.tsx new file mode 100644 index 0000000..bc51dd7 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/payment-history/payment-history-table.component.tsx @@ -0,0 +1,97 @@ +import { DataTableSkeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@carbon/react'; +import { EmptyState, ErrorState } from '@openmrs/esm-patient-common-lib'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useBills } from '../../billing.resource'; +import { MappedBill } from '../../types'; +// import { headers } from './payment-history-viewer.component'; +import styles from './payment-history.scss'; + +export const PaymentHistoryTable = ({ + tableData, + paidBillsResponse, + renderedRows, +}: { + tableData: any; + paidBillsResponse: ReturnType; + renderedRows: MappedBill[]; +}) => { + const { t } = useTranslation(); + const { bills, error, isLoading } = paidBillsResponse; + const { rows, getHeaderProps, getRowProps, getTableProps } = tableData; + + const headers = [ + { header: t('billDate', 'Date'), key: 'dateCreated' }, + { header: t('patientName', 'Patient Name'), key: 'patientName' }, + { header: t('totalAmount', 'Total Amount'), key: 'totalAmount' }, + { header: t('billingService', 'Service'), key: 'billingService' }, + { header: t('referenceCodes', ' Reference Codes'), key: 'referenceCodes' }, + { header: t('status', 'Status'), key: 'status' }, + ]; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+ +
+ ); + } + + if (bills.length === 0 || renderedRows.length === 0) { + return ( +
+ +
+ ); + } + + return ( + + + + {headers.map((header) => ( + + {header.header} + + ))} + + + + {rows.map((row) => ( + + {row.cells.map((cell) => ( + {cell.value} + ))} + + ))} + +
+ ); +}; diff --git a/packages/esm-billing-app/src/billable-services/payment-history/payment-history-viewer.component.tsx b/packages/esm-billing-app/src/billable-services/payment-history/payment-history-viewer.component.tsx new file mode 100644 index 0000000..0c9ed24 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/payment-history/payment-history-viewer.component.tsx @@ -0,0 +1,159 @@ +import { + Button, + DataTable, + Pagination, + TableContainer, + TableToolbar, + TableToolbarContent, + TableToolbarSearch, +} from '@carbon/react'; +import { isDesktop, showModal, useLayoutType, usePagination } from '@openmrs/esm-framework'; +import { usePaginationInfo } from '@openmrs/esm-patient-common-lib'; +import dayjs from 'dayjs'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import { useBills } from '../../billing.resource'; +import { useRenderedRows } from '../../hooks/use-rendered-rows'; +import { usePaymentPoints } from '../../payment-points/payment-points.resource'; +import { useClockInStatus } from '../../payment-points/use-clock-in-status'; +import { PaymentStatus, Timesheet } from '../../types'; +import { AppliedFilterTags } from './applied-filter-tages.component'; +import { Filter } from './filter.component'; +import { PaymentHistoryTable } from './payment-history-table.component'; +import styles from './payment-history.scss'; +import { PaymentTotals } from './payment-totals'; +import { TableToolBarDateRangePicker } from './table-toolbar-date-range'; +import { TimesheetsFilter } from './timesheets-filter.component'; + +export const PaymentHistoryViewer = () => { + const [dateRange, setDateRange] = useState>([dayjs().startOf('day').toDate(), new Date()]); + const { paymentPointUUID } = useParams(); + const isOnPaymentPointPage = Boolean(paymentPointUUID); + + const { isClockedInCurrentPaymentPoint } = useClockInStatus(paymentPointUUID); + const { paymentPoints } = usePaymentPoints(); + + const paidBillsResponse = useBills('', isOnPaymentPointPage ? '' : PaymentStatus.PAID); + const { bills } = paidBillsResponse; + + const [pageSize, setPageSize] = useState(10); + const [appliedFilters, setAppliedFilters] = useState>([]); + const [appliedTimesheet, setAppliedTimesheet] = useState(); + + const renderedRows = useRenderedRows(bills, appliedFilters, appliedTimesheet); + + const { paginated, goTo, results, currentPage } = usePagination(renderedRows, pageSize); + const { pageSizes } = usePaginationInfo(pageSize, renderedRows.length, currentPage, results?.length); + + const { t } = useTranslation(); + const headers = [ + { header: t('billDate', 'Date'), key: 'dateCreated' }, + { header: t('patientName', 'Patient Name'), key: 'patientName' }, + { header: t('totalAmount', 'Total Amount'), key: 'totalAmount' }, + { header: t('billingService', 'Service'), key: 'billingService' }, + { header: t('referenceCodes', ' Reference Codes'), key: 'referenceCodes' }, + { header: t('status', 'Status'), key: 'status' }, + ]; + const responsiveSize = isDesktop(useLayoutType()) ? 'sm' : 'lg'; + + const handleFilterByDateRange = (dates: Array) => { + setDateRange(dates); + }; + + const openClockInModal = () => { + const dispose = showModal('clock-in-modal', { + closeModal: () => dispose(), + paymentPoint: paymentPoints.find((paymentPoint) => paymentPoint.uuid === paymentPointUUID), + }); + }; + + const openClockOutModal = () => { + const dispose = showModal('clock-out-modal', { + closeModal: () => dispose(), + paymentPoint: paymentPoints.find((paymentPoint) => paymentPoint.uuid === paymentPointUUID), + }); + }; + + const resetFilters = () => { + setAppliedFilters([]); + setAppliedTimesheet(undefined); + }; + + const applyFilters = (filters: string[]) => { + setAppliedFilters(filters); + goTo(1); + }; + + const applyTimeSheetFilter = (sheet: Timesheet) => { + setAppliedTimesheet(sheet); + }; + + return ( +
+ + + {(tableData) => ( + +
+ + + ) => tableData.onInputChange(evt)} + /> + + + + + {isOnPaymentPointPage && !isClockedInCurrentPaymentPoint && ( + + )} + {isOnPaymentPointPage && isClockedInCurrentPaymentPoint && ( + + )} + + +
+ + {paginated && !paidBillsResponse.isLoading && !paidBillsResponse.error && ( + { + if (newPage !== currentPage) { + goTo(newPage); + } + setPageSize(pageSize); + }} + /> + )} +
+ )} +
+
+ ); +}; diff --git a/packages/esm-billing-app/src/billable-services/payment-history/payment-history.component.tsx b/packages/esm-billing-app/src/billable-services/payment-history/payment-history.component.tsx new file mode 100644 index 0000000..4b082cd --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/payment-history/payment-history.component.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import BillingHeader from '../../billing-header/billing-header.component'; +import { PaymentHistoryViewer } from './payment-history-viewer.component'; + +export const PaymentHistory = () => { + const { t } = useTranslation(); + return ( +
+ + +
+ ); +}; diff --git a/packages/esm-billing-app/src/billable-services/payment-history/payment-history.scss b/packages/esm-billing-app/src/billable-services/payment-history/payment-history.scss new file mode 100644 index 0000000..85325d8 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/payment-history/payment-history.scss @@ -0,0 +1,86 @@ +@use '@carbon/layout'; +@use '@carbon/colors'; +@import '~@openmrs/esm-styleguide/src/vars'; + +.table { + padding: layout.$spacing-05; +} + +.errorStateSkeleton, +.dataTableSkeleton, +.emptyStateWrapper { + padding-top: layout.$spacing-05; +} + +.checkBoxWrapper { + padding: layout.$spacing-05; +} + +.dateRangePicker { + flex-grow: unset; +} + +.tableToolBar { + position: relative; + + > section { + position: relative; + top: 0; + + > div { + height: auto; + align-items: center; + } + } +} + +.skeletonIcon { + width: layout.$spacing-09; + height: layout.$spacing-09; + margin-right: layout.$spacing-05; +} + +.loadingPaymentTotals, +.paymentTotals { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); + gap: layout.$spacing-05; + margin-bottom: layout.$spacing-05; +} + +.paymentTotal { + color: colors.$blue-60; + margin-top: layout.$spacing-05; +} + +.tile { + border: 1px solid $ui-03; +} + +.tagWrapper { + display: flex; + align-items: center; + border: 1px solid $ui-03; + gap: layout.$spacing-03; + padding: layout.$spacing-03; + flex-wrap: wrap; +} + +.loadingPaymentTotal { + width: 50% !important; +} + +.clockIn { + margin-left: layout.$spacing-05; + align-self: flex-end; +} + +.noServiceTypes, +.noCashiersInRange { + font-size: small; +} + +.timesheetsFilter { + flex-grow: unset; + margin-right: layout.$spacing-01; +} diff --git a/packages/esm-billing-app/src/billable-services/payment-history/payment-totals.tsx b/packages/esm-billing-app/src/billable-services/payment-history/payment-totals.tsx new file mode 100644 index 0000000..533ef13 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/payment-history/payment-totals.tsx @@ -0,0 +1,62 @@ +import { SkeletonText, Tile } from '@carbon/react'; +import React from 'react'; +import { usePaymentModes } from '../../billing.resource'; +import { convertToCurrency } from '../../helpers'; +import { MappedBill } from '../../types'; +import styles from './payment-history.scss'; +import { usePaymentTotals } from './use-payment-totals'; +import { useConfig } from '@openmrs/esm-framework'; + +export const PaymentTotals = ({ + renderedRows, + appliedFilters, +}: { + renderedRows: Array | null; + appliedFilters: Array; +}) => { + const { isLoading, paymentModes } = usePaymentModes(); + const paymentTotals = usePaymentTotals(renderedRows); + + const {defaultCurrency} = useConfig() + + const selectedPaymentTypeCheckBoxes = appliedFilters.filter((f) => paymentModes.find((m) => m.name === f)); + + if (isLoading) { + return ( +
+ {Array.from({ length: 4 }).map(() => ( + + + + + ))} +
+ ); + } + + if (selectedPaymentTypeCheckBoxes.length === 0) { + return ( +
+ {paymentTotals.map((total) => ( + + {total.type} +

{convertToCurrency(total.total, defaultCurrency)}

+
+ ))} +
+ ); + } + + return ( +
+ {paymentTotals + .filter((total) => selectedPaymentTypeCheckBoxes.includes(total.type)) + .map((total) => ( + + {total.type} +

{convertToCurrency(total.total, defaultCurrency)}

+
+ ))} +
+ ); +}; diff --git a/packages/esm-billing-app/src/billable-services/payment-history/payment-type-filter.component.tsx b/packages/esm-billing-app/src/billable-services/payment-history/payment-type-filter.component.tsx new file mode 100644 index 0000000..46fb7ce --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/payment-history/payment-type-filter.component.tsx @@ -0,0 +1,48 @@ +import { Checkbox, SkeletonIcon, usePrefix } from '@carbon/react'; +import React, { ChangeEvent } from 'react'; +import { usePaymentModes } from '../../billing.resource'; +import styles from './payment-history.scss'; + +export const PaymentTypeFilter = ({ + selectedPaymentTypeCheckBoxes, + applyPaymentTypeFilter, +}: { + selectedPaymentTypeCheckBoxes: Array; + applyPaymentTypeFilter: (filters: Array) => void; +}) => { + const { isLoading, paymentModes } = usePaymentModes(false); + const prefix = usePrefix(); + + const handleCheckboxChange = (e: ChangeEvent) => { + const checkboxId = e.target.id; + const isChecked = e.target.checked; + + const checkboxValue: HTMLSpanElement | null = document.querySelector(`label[for="${checkboxId}"]`); + + if (isChecked && checkboxValue) { + applyPaymentTypeFilter([...selectedPaymentTypeCheckBoxes, checkboxValue.innerText]); + } else { + applyPaymentTypeFilter(selectedPaymentTypeCheckBoxes.filter((item) => item !== checkboxValue?.innerText)); + } + }; + + if (isLoading) { + return ; + } + + return ( +
+
+ Payment Type + {paymentModes.map((method) => ( + + ))} +
+
+ ); +}; diff --git a/packages/esm-billing-app/src/billable-services/payment-history/service-type-filter.component.tsx b/packages/esm-billing-app/src/billable-services/payment-history/service-type-filter.component.tsx new file mode 100644 index 0000000..874b9ab --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/payment-history/service-type-filter.component.tsx @@ -0,0 +1,53 @@ +import { Checkbox, SkeletonIcon, usePrefix } from '@carbon/react'; +import React, { ChangeEvent } from 'react'; +import { MappedBill } from '../../types'; +import styles from './payment-history.scss'; +import { useBillsServiceTypes } from './use-bills-service-types'; + +export const ServiceTypeFilter = ({ + selectedServiceTypeCheckboxes, + bills, + applyServiceTypeFilter, +}: { + selectedServiceTypeCheckboxes: Array; + bills: Array; + applyServiceTypeFilter: (filters: Array) => void; +}) => { + const { billsServiceTypes, isLoading } = useBillsServiceTypes(bills); + const prefix = usePrefix(); + + const handleCheckboxChange = (e: ChangeEvent) => { + const checkboxId = e.target.id; + const isChecked = e.target.checked; + + const checkboxValue: HTMLSpanElement | null = document.querySelector(`label[for="${checkboxId}"]`); + const serviceTypeUUID = billsServiceTypes.find((s) => s.display === checkboxValue.innerText).uuid; + + if (isChecked && checkboxValue) { + applyServiceTypeFilter([...selectedServiceTypeCheckboxes, serviceTypeUUID]); + } else { + applyServiceTypeFilter(selectedServiceTypeCheckboxes.filter((item) => item !== serviceTypeUUID)); + } + }; + + if (isLoading) { + return ; + } + + return ( +
+
+ Service type + {billsServiceTypes.length === 0 &&

No Service Types In Bills Range

} + {billsServiceTypes.map((type) => ( + + ))} +
+
+ ); +}; diff --git a/packages/esm-billing-app/src/billable-services/payment-history/table-toolbar-date-range.tsx b/packages/esm-billing-app/src/billable-services/payment-history/table-toolbar-date-range.tsx new file mode 100644 index 0000000..e6e7402 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/payment-history/table-toolbar-date-range.tsx @@ -0,0 +1,38 @@ +import { DatePicker, DatePickerInput } from '@carbon/react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import styles from './payment-history.scss'; + +export const TableToolBarDateRangePicker = ({ + onChange, + currentValues, +}: { + onChange: (dates: Array) => void; + currentValues: Array; +}) => { + const currentDate = new Date(); + const { t } = useTranslation(); + return ( + + + + + ); +}; diff --git a/packages/esm-billing-app/src/billable-services/payment-history/timesheets-filter.component.tsx b/packages/esm-billing-app/src/billable-services/payment-history/timesheets-filter.component.tsx new file mode 100644 index 0000000..9e548f9 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/payment-history/timesheets-filter.component.tsx @@ -0,0 +1,93 @@ +import { Form, Select, SelectItem } from '@carbon/react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import React, { useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; +import { useTimeSheets } from '../../payment-points/payment-points.resource'; +import { MappedBill, Timesheet } from '../../types'; +import styles from './payment-history.scss'; + +const schema = z.object({ + timesheet: z.string(), +}); + +type FormData = z.infer; + +export const TimesheetsFilter = ({ + appliedFilters, + bills, + applyTimeSheetFilter, + dateRange, +}: { + appliedFilters: Array; + bills: MappedBill[]; + applyTimeSheetFilter: (sheet: Timesheet | undefined) => void; + dateRange: Array; +}) => { + const { timesheets } = useTimeSheets(); + const billsCashierUUIDs = bills + .map((bill) => bill.cashier) + .filter((cashier) => appliedFilters.includes(cashier.display)) + .map((c) => c.uuid); + + const uniqueBillsCashiersUUIDS = Array.from(new Set(billsCashierUUIDs)); + + const selectedCashiersTimesheets = timesheets + .sort((a, b) => new Date(b.clockIn).getTime() - new Date(a.clockIn).getTime()) + .filter((sheet) => uniqueBillsCashiersUUIDS.includes(sheet.cashier.uuid)) + .filter((sheet) => { + const sheetClockInTime = new Date(sheet.clockIn); + const isOpenTimeSheet = !sheet.clockOut; + if (isOpenTimeSheet) { + return sheetClockInTime >= dateRange.at(0); + } + const sheetClockOutTime = new Date(sheet.clockOut); + return sheetClockInTime >= dateRange.at(0) && sheetClockOutTime <= dateRange.at(1); + }); + + const { t } = useTranslation(); + const form = useForm({ + resolver: zodResolver(schema), + mode: 'onChange', + defaultValues: { + timesheet: undefined, + }, + }); + + const { register, watch } = form; + const selectedSheetUUID = watch('timesheet'); + + useEffect(() => { + if (!selectedSheetUUID) { + applyTimeSheetFilter(undefined); + return; + } + if (selectedSheetUUID) { + applyTimeSheetFilter(timesheets.find((sheet) => sheet.uuid === selectedSheetUUID)); + } + }, [selectedSheetUUID]); + + if (selectedCashiersTimesheets.length === 0) { + return null; + } + + return ( +
+ +
+ ); +}; diff --git a/packages/esm-billing-app/src/billable-services/payment-history/use-bills-service-types.tsx b/packages/esm-billing-app/src/billable-services/payment-history/use-bills-service-types.tsx new file mode 100644 index 0000000..65aeea4 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/payment-history/use-bills-service-types.tsx @@ -0,0 +1,15 @@ +import { MappedBill } from '../../types'; +import { useServiceTypes } from '../billable-service.resource'; + +export const useBillsServiceTypes = (bills: MappedBill[]) => { + const { serviceTypes, isLoading } = useServiceTypes(); + const lineItemServiceTypeUUIDS = bills + .map((bill) => bill.lineItems.map((lineItem) => lineItem.serviceTypeUuid)) + .flat(); + const uniqueLineItemServiceTypeUUIDs = Array.from(new Set(lineItemServiceTypeUUIDS)); + + return { + isLoading: isLoading, + billsServiceTypes: serviceTypes.filter((sType) => uniqueLineItemServiceTypeUUIDs.includes(sType.uuid)), + }; +}; diff --git a/packages/esm-billing-app/src/billable-services/payment-history/use-payment-totals.tsx b/packages/esm-billing-app/src/billable-services/payment-history/use-payment-totals.tsx new file mode 100644 index 0000000..cec5429 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/payment-history/use-payment-totals.tsx @@ -0,0 +1,34 @@ +import { usePaymentModes } from '../../billing.resource'; +import { MappedBill } from '../../types'; + +export type PaymentTypeTotal = { type: string; total: number }; + +export const usePaymentTotals = (renderedRows: MappedBill[] | null) => { + const { paymentModes } = usePaymentModes(false); + + if (!paymentModes) { + return []; + } + + if (!renderedRows) { + return paymentModes.map((mode) => { + return { total: 0, type: mode.name }; + }); + } + + const paymentTotals: PaymentTypeTotal[] = []; + + for (let i = 0; i < paymentModes.length; i++) { + const paymentMode = paymentModes[i]; + const paymentModeTotal = renderedRows + .filter((bill) => { + const billPaymentModeUUIDs = new Set(bill.payments.map((p) => p.instanceType.uuid)); + return billPaymentModeUUIDs.has(paymentMode.uuid); + }) + .reduce((acc, curr) => curr.totalAmount + acc, 0); + + paymentTotals.push({ total: paymentModeTotal, type: paymentMode.name }); + } + + return paymentTotals; +}; diff --git a/packages/esm-billing-app/src/billing.resource.ts b/packages/esm-billing-app/src/billing.resource.ts index 36bd811..1a4b6bc 100644 --- a/packages/esm-billing-app/src/billing.resource.ts +++ b/packages/esm-billing-app/src/billing.resource.ts @@ -36,6 +36,7 @@ export const useBills = (patientUuid: string = '', billStatus: string = '') => { cashPointName: bill?.cashPoint?.name, cashPointLocation: bill?.cashPoint?.location?.display, dateCreated: bill?.dateCreated ? formatDate(parseDate(bill.dateCreated), { mode: 'wide' }) : '--', + dateCreatedUnformatted: bill?.dateCreated, lineItems: bill?.lineItems, billingService: bill?.lineItems.map((bill) => bill?.item || bill?.billableService || '--').join(' '), payments: bill?.payments, @@ -76,6 +77,7 @@ export const useBill = (billUuid: string) => { cashPointName: bill?.cashPoint?.name, cashPointLocation: bill?.cashPoint?.location?.display, dateCreated: bill?.dateCreated ? formatDate(parseDate(bill.dateCreated), { mode: 'wide' }) : '--', + dateCreatedUnformatted: bill?.dateCreated, lineItems: bill?.lineItems, billingService: bill?.lineItems.map((bill) => bill.item).join(' '), payments: bill.payments, diff --git a/packages/esm-billing-app/src/constants.ts b/packages/esm-billing-app/src/constants.ts index bcddb34..0bb24d8 100644 --- a/packages/esm-billing-app/src/constants.ts +++ b/packages/esm-billing-app/src/constants.ts @@ -6,3 +6,18 @@ export const omrsDateFormat = 'YYYY-MM-DDTHH:mm:ss.SSSZZ'; // Size in MegaBits export const MAX_ALLOWED_FILE_SIZE = 2097152; + +export const colorsArray = [ + 'red', + 'magenta', + 'purple', + 'blue', + 'cyan', + 'teal', + 'green', + 'gray', + 'cool-gray', + 'warm-gray', + 'high-contrast', + 'outline', + ]; \ No newline at end of file diff --git a/packages/esm-billing-app/src/hooks/use-rendered-rows.ts b/packages/esm-billing-app/src/hooks/use-rendered-rows.ts new file mode 100644 index 0000000..515f75f --- /dev/null +++ b/packages/esm-billing-app/src/hooks/use-rendered-rows.ts @@ -0,0 +1,89 @@ +import flatMapDeep from 'lodash-es/flatMapDeep'; +import { useLocation, useParams } from 'react-router-dom'; +import { useBillsServiceTypes } from '../billable-services/payment-history/use-bills-service-types'; +import { usePaymentModes } from '../billing.resource'; +import { MappedBill, Timesheet } from '../types'; + +const getAllValues = (obj: Object): Array => { + return flatMapDeep(obj, (value) => { + if (typeof value === 'object' && value !== null) { + return getAllValues(value); + } + return value; + }); +}; + +export const useRenderedRows = (bills: MappedBill[], filters: Array, timesheet?: Timesheet) => { + const { pathname } = useLocation(); + const { billsServiceTypes } = useBillsServiceTypes(bills); + const { paymentModes } = usePaymentModes(false); + const { paymentPointUUID } = useParams(); + const isOnPaymentPointPage = Boolean(paymentPointUUID); + + const cashiers = Array.from(new Map(bills.map((bill) => [bill.cashier.uuid, bill.cashier])).values()); + + const cashierFilters = cashiers.filter((c) => filters.includes(c.display)).map((c) => c.display); + const serviceTypeFilters = billsServiceTypes.filter((st) => filters.includes(st.uuid)).map((st) => st.uuid); + const paymentModeFilters = paymentModes?.filter((pm) => filters.includes(pm.name)).map((pm) => pm.name); + + const preFiltered = bills + .filter((b) => (isOnPaymentPointPage ? b.cashPointUuid === paymentPointUUID : true)) + .filter((bill) => { + if (!timesheet) { + return true; + } + + if (timesheet.cashier.uuid !== bill.cashier.uuid) { + return true; + } + + const billPaymentTime = bill.payments.sort((a, b) => b.dateCreated - a.dateCreated).at(0).dateCreated; + const billCreatedOnTime = new Date(bill.dateCreatedUnformatted); + + const isOnPaidBillsOnlyPage = pathname === '/payment-history'; + const pivot = isOnPaidBillsOnlyPage ? new Date(billPaymentTime) : billCreatedOnTime; + + const timesheetStartDate = new Date(timesheet.clockIn); + const timesheetEndDate = timesheet.clockOut ? new Date(timesheet.clockOut) : new Date(); + return timesheetStartDate <= pivot && timesheetEndDate >= pivot; + }); + + if (bills.length === 0 || !bills) { + return []; + } + + if (filters.length === 0) { + return preFiltered; + } + + const filteredBills = preFiltered + .filter((row) => { + if (cashierFilters.length === 0) { + return true; + } + const rowValues = getAllValues(row); + return cashierFilters.some((cashier) => + rowValues.some((value) => String(value).toLowerCase().includes(cashier.toLowerCase())), + ); + }) + .filter((row) => { + if (paymentModeFilters.length === 0) { + return true; + } + const rowValues = getAllValues(row); + return paymentModeFilters.some((paymentType) => + rowValues.some((value) => String(value).toLowerCase().includes(paymentType.toLowerCase())), + ); + }) + .filter((row) => { + if (serviceTypeFilters.length === 0) { + return true; + } + const rowValues = getAllValues(row); + return serviceTypeFilters.some((serviceType) => + rowValues.some((value) => String(value).toLowerCase().includes(serviceType.toLowerCase())), + ); + }); + + return filteredBills; +}; diff --git a/packages/esm-billing-app/src/invoice/invoice-table.test.tsx b/packages/esm-billing-app/src/invoice/invoice-table.test.tsx index 1ce48e7..8861d5e 100644 --- a/packages/esm-billing-app/src/invoice/invoice-table.test.tsx +++ b/packages/esm-billing-app/src/invoice/invoice-table.test.tsx @@ -89,6 +89,7 @@ describe('InvoiceTable', () => { status: 'PAID', identifier: 'receipt-identifier', dateCreated: new Date().toISOString(), + dateCreatedUnformatted: '2023-09-01T12:00:00Z.000+0300', billingService: 'billing-service-uuid', payments: [], totalAmount: 300, diff --git a/packages/esm-billing-app/src/invoice/payments/payment-history/payment-history.test.tsx b/packages/esm-billing-app/src/invoice/payments/payment-history/payment-history.test.tsx index 968c410..c216c0d 100644 --- a/packages/esm-billing-app/src/invoice/payments/payment-history/payment-history.test.tsx +++ b/packages/esm-billing-app/src/invoice/payments/payment-history/payment-history.test.tsx @@ -76,6 +76,7 @@ describe('PaymentHistory Component', () => { status: 'PAID', identifier: 'invoice-123', dateCreated: '2023-09-01T12:00:00Z', + dateCreatedUnformatted: '2023-09-01T12:00:00Z.000+0300', lineItems: [], billingService: 'Billing Service', }; @@ -104,6 +105,7 @@ describe('PaymentHistory Component', () => { status: 'PENDING', identifier: 'invoice-124', dateCreated: '2023-09-02T10:00:00Z', + dateCreatedUnformatted: '2023-09-01T12:00:00Z.000+0300', lineItems: [], billingService: 'Billing Service', }; diff --git a/packages/esm-billing-app/src/invoice/payments/payments.component.test.tsx b/packages/esm-billing-app/src/invoice/payments/payments.component.test.tsx index ae08ee3..2d3f1f9 100644 --- a/packages/esm-billing-app/src/invoice/payments/payments.component.test.tsx +++ b/packages/esm-billing-app/src/invoice/payments/payments.component.test.tsx @@ -90,6 +90,7 @@ describe('Payments', () => { status: 'PAID', identifier: 'invoice-123', dateCreated: '2023-09-01T12:00:00Z', + dateCreatedUnformatted: '2023-09-01T12:00:00Z.000+0300', lineItems: [], billingService: 'Billing Service', }; diff --git a/packages/esm-billing-app/src/root.component.tsx b/packages/esm-billing-app/src/root.component.tsx index 7706111..8c2758b 100644 --- a/packages/esm-billing-app/src/root.component.tsx +++ b/packages/esm-billing-app/src/root.component.tsx @@ -14,6 +14,9 @@ import AddServiceForm from './billable-services/billables/services/service-form. import CommodityForm from './billable-services/billables/commodity/commodity-form.workspace'; import { PaymentPoints } from './payment-points/payment-points.component'; +import { PaymentPoint } from './payment-points/payment-point/payment-point.component'; + +import { PaymentHistory } from './billable-services/payment-history/payment-history.component'; const RootComponent: React.FC = () => { const basePath = `${window.spaBase}/billing`; @@ -42,6 +45,9 @@ const RootComponent: React.FC = () => { handleNavigation('payment-points')}> {t('paymentPoints', 'Payment Points')} + handleNavigation('payment-history')}> + {t('paymentHistory', 'Payment History')} + @@ -50,7 +56,12 @@ const RootComponent: React.FC = () => { } /> } /> } /> + } /> + } /> + + } /> + } /> diff --git a/packages/esm-billing-app/src/types/index.ts b/packages/esm-billing-app/src/types/index.ts index ad8b885..19850f9 100644 --- a/packages/esm-billing-app/src/types/index.ts +++ b/packages/esm-billing-app/src/types/index.ts @@ -13,6 +13,7 @@ export interface MappedBill { status: string; identifier: string; dateCreated: string; + dateCreatedUnformatted: string; lineItems: Array; billingService: string; payments: Array; @@ -123,6 +124,16 @@ export interface Payment { resourceVersion: string; } +export enum PaymentStatus { + POSTED = 'POSTED', + PENDING = 'PENDING', + PAID = 'PAID', + CREDITED = 'CREDITED', + CANCELLED = 'CANCELLED', + ADJUSTED = 'ADJUSTED', + EXEMPTED = 'EXEMPTED', +} + export interface PatientInvoice { uuid: string; display: string; From 967352a11aca7496ff384bada2de27a00ef1bcd6 Mon Sep 17 00:00:00 2001 From: Munyua123 Date: Wed, 11 Dec 2024 13:41:51 +0300 Subject: [PATCH 2/4] (Bump) updated githead --- packages/esm-billing-app/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/esm-billing-app/package.json b/packages/esm-billing-app/package.json index ecf2251..f4e49fd 100644 --- a/packages/esm-billing-app/package.json +++ b/packages/esm-billing-app/package.json @@ -1,6 +1,6 @@ { "name": "@ehospital/esm-billing-app", - "version": "1.1.8", + "version": "1.1.9", "description": "Billing frontend module for use in O3", "browser": "dist/ehospital-esm-billing-app.js", "main": "src/index.ts", @@ -121,5 +121,5 @@ "*.{js,jsx,ts,tsx}": "eslint --cache --fix" }, "packageManager": "yarn@4.1.1", - "gitHead": "480dacb69afe841a1c69da2805b588678d93d34c" + "gitHead": "1fabb64201d6e18fd3fe47471abe6ff21377da95" } From 402dba80c9b2f45739667640459e159b312a6a6e Mon Sep 17 00:00:00 2001 From: Munyua123 Date: Thu, 12 Dec 2024 08:26:03 +0300 Subject: [PATCH 3/4] SJT-120 Add the clock in and clock out modal and fix the service type filter. --- .../service-type-filter.component.tsx | 11 +++++------ .../payment-history/use-bills-service-types.tsx | 15 --------------- .../src/hooks/use-rendered-rows.ts | 6 +++--- packages/esm-billing-app/src/index.ts | 4 ++++ packages/esm-billing-app/src/routes.json | 8 ++++++++ 5 files changed, 20 insertions(+), 24 deletions(-) delete mode 100644 packages/esm-billing-app/src/billable-services/payment-history/use-bills-service-types.tsx diff --git a/packages/esm-billing-app/src/billable-services/payment-history/service-type-filter.component.tsx b/packages/esm-billing-app/src/billable-services/payment-history/service-type-filter.component.tsx index 874b9ab..be2647f 100644 --- a/packages/esm-billing-app/src/billable-services/payment-history/service-type-filter.component.tsx +++ b/packages/esm-billing-app/src/billable-services/payment-history/service-type-filter.component.tsx @@ -2,18 +2,17 @@ import { Checkbox, SkeletonIcon, usePrefix } from '@carbon/react'; import React, { ChangeEvent } from 'react'; import { MappedBill } from '../../types'; import styles from './payment-history.scss'; -import { useBillsServiceTypes } from './use-bills-service-types'; +import { useServiceTypes } from '../billable-service.resource'; export const ServiceTypeFilter = ({ selectedServiceTypeCheckboxes, - bills, applyServiceTypeFilter, }: { selectedServiceTypeCheckboxes: Array; bills: Array; applyServiceTypeFilter: (filters: Array) => void; }) => { - const { billsServiceTypes, isLoading } = useBillsServiceTypes(bills); + const { serviceTypes, isLoading } = useServiceTypes(); const prefix = usePrefix(); const handleCheckboxChange = (e: ChangeEvent) => { @@ -21,7 +20,7 @@ export const ServiceTypeFilter = ({ const isChecked = e.target.checked; const checkboxValue: HTMLSpanElement | null = document.querySelector(`label[for="${checkboxId}"]`); - const serviceTypeUUID = billsServiceTypes.find((s) => s.display === checkboxValue.innerText).uuid; + const serviceTypeUUID = serviceTypes.find((s) => s.display === checkboxValue.innerText).uuid; if (isChecked && checkboxValue) { applyServiceTypeFilter([...selectedServiceTypeCheckboxes, serviceTypeUUID]); @@ -38,8 +37,8 @@ export const ServiceTypeFilter = ({
Service type - {billsServiceTypes.length === 0 &&

No Service Types In Bills Range

} - {billsServiceTypes.map((type) => ( + {serviceTypes.length === 0 &&

No Service Types In Bills Range

} + {serviceTypes.map((type) => ( { - const { serviceTypes, isLoading } = useServiceTypes(); - const lineItemServiceTypeUUIDS = bills - .map((bill) => bill.lineItems.map((lineItem) => lineItem.serviceTypeUuid)) - .flat(); - const uniqueLineItemServiceTypeUUIDs = Array.from(new Set(lineItemServiceTypeUUIDS)); - - return { - isLoading: isLoading, - billsServiceTypes: serviceTypes.filter((sType) => uniqueLineItemServiceTypeUUIDs.includes(sType.uuid)), - }; -}; diff --git a/packages/esm-billing-app/src/hooks/use-rendered-rows.ts b/packages/esm-billing-app/src/hooks/use-rendered-rows.ts index 515f75f..9fcd84b 100644 --- a/packages/esm-billing-app/src/hooks/use-rendered-rows.ts +++ b/packages/esm-billing-app/src/hooks/use-rendered-rows.ts @@ -1,8 +1,8 @@ import flatMapDeep from 'lodash-es/flatMapDeep'; import { useLocation, useParams } from 'react-router-dom'; -import { useBillsServiceTypes } from '../billable-services/payment-history/use-bills-service-types'; import { usePaymentModes } from '../billing.resource'; import { MappedBill, Timesheet } from '../types'; +import { useServiceTypes } from '../billable-services/billable-service.resource'; const getAllValues = (obj: Object): Array => { return flatMapDeep(obj, (value) => { @@ -15,7 +15,7 @@ const getAllValues = (obj: Object): Array => { export const useRenderedRows = (bills: MappedBill[], filters: Array, timesheet?: Timesheet) => { const { pathname } = useLocation(); - const { billsServiceTypes } = useBillsServiceTypes(bills); + const {serviceTypes} = useServiceTypes() const { paymentModes } = usePaymentModes(false); const { paymentPointUUID } = useParams(); const isOnPaymentPointPage = Boolean(paymentPointUUID); @@ -23,7 +23,7 @@ export const useRenderedRows = (bills: MappedBill[], filters: Array, tim const cashiers = Array.from(new Map(bills.map((bill) => [bill.cashier.uuid, bill.cashier])).values()); const cashierFilters = cashiers.filter((c) => filters.includes(c.display)).map((c) => c.display); - const serviceTypeFilters = billsServiceTypes.filter((st) => filters.includes(st.uuid)).map((st) => st.uuid); + const serviceTypeFilters = serviceTypes.filter((st) => filters.includes(st.uuid)).map((st) => st.uuid); const paymentModeFilters = paymentModes?.filter((pm) => filters.includes(pm.name)).map((pm) => pm.name); const preFiltered = bills diff --git a/packages/esm-billing-app/src/index.ts b/packages/esm-billing-app/src/index.ts index e13dda7..a4f6e42 100644 --- a/packages/esm-billing-app/src/index.ts +++ b/packages/esm-billing-app/src/index.ts @@ -21,6 +21,8 @@ import PriceInfoOrder from './billable-services/billable-item/test-order/price-i import { BulkImportBillableServices } from './billable-services/bulk-import-billable-service.modal'; import { CreatePaymentPoint } from './payment-points/create-payment-point.component'; +import { ClockIn } from './payment-points/payment-point/clock-in.component'; +import { ClockOut } from './payment-points/payment-point/clock-out.component'; const moduleName = '@ehospital/esm-billing-app'; @@ -80,6 +82,8 @@ export const procedureOrder = getSyncLifecycle(ProcedureOrder, options); export const priceInfoOrder = getSyncLifecycle(PriceInfoOrder, options); export const bulkImportBillableServicesModal = getSyncLifecycle(BulkImportBillableServices, options); +export const clockInModal = getSyncLifecycle(ClockIn, options) +export const clockOut = getSyncLifecycle(ClockOut, options) export const editBillLineItemDialog = getAsyncLifecycle(() => import('./bill-item-actions/edit-bill-item.component'), { featureName: 'edit bill line item', diff --git a/packages/esm-billing-app/src/routes.json b/packages/esm-billing-app/src/routes.json index 5e89b96..26936e3 100644 --- a/packages/esm-billing-app/src/routes.json +++ b/packages/esm-billing-app/src/routes.json @@ -131,6 +131,14 @@ { "name": "bulk-import-billable-services-modal", "component": "bulkImportBillableServicesModal" + }, + { + "name": "clock-in-modal", + "component": "clockInModal" + }, + { + "name": "clock-out-modal", + "component": "clockOutModal" } ] } From 81297c9431ccf0f29a7a6faea0ac6a22e9e621a4 Mon Sep 17 00:00:00 2001 From: Munyua123 Date: Thu, 12 Dec 2024 08:36:52 +0300 Subject: [PATCH 4/4] (Bump) updated Githead --- packages/esm-billing-app/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/esm-billing-app/package.json b/packages/esm-billing-app/package.json index f4e49fd..74823b9 100644 --- a/packages/esm-billing-app/package.json +++ b/packages/esm-billing-app/package.json @@ -1,6 +1,6 @@ { "name": "@ehospital/esm-billing-app", - "version": "1.1.9", + "version": "1.2.0", "description": "Billing frontend module for use in O3", "browser": "dist/ehospital-esm-billing-app.js", "main": "src/index.ts", @@ -121,5 +121,5 @@ "*.{js,jsx,ts,tsx}": "eslint --cache --fix" }, "packageManager": "yarn@4.1.1", - "gitHead": "1fabb64201d6e18fd3fe47471abe6ff21377da95" + "gitHead": "d2c82bbf857f52e0b9fde4ba748c7e5d0db80413" }