Skip to content

Commit

Permalink
add reports context
Browse files Browse the repository at this point in the history
  • Loading branch information
kilbot committed Oct 27, 2024
1 parent 3613e73 commit 31c2260
Show file tree
Hide file tree
Showing 14 changed files with 217 additions and 139 deletions.
2 changes: 1 addition & 1 deletion src/hooks/use-local-date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const useLocalDate = () => {
* Passing the Locale instance to date-fns functions converts date strings to the local date format
* eg: Locale.es converts January 1 to 1 de enero
*/
const locale = Locales[shortCode] ? Locales[shortCode] : undefined;
const locale = shortCode in Locales ? Locales[shortCode as keyof typeof Locales] : undefined;

/**
* Wrapper for date-fns format function
Expand Down
30 changes: 21 additions & 9 deletions src/hooks/use-locale/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import * as React from 'react';

import { getLocales } from 'expo-localization';
import { useObservableEagerState } from 'observable-hooks';
import { of } from 'rxjs';
import { of, Observable } from 'rxjs';

import type { StoreDocument } from '@wcpos/database';

import locales from './locales.json';
import { useAppState } from '../../contexts/app-state';
Expand All @@ -26,6 +28,10 @@ interface Language {
nativeName: string;
}

interface LocalesType {
[key: string]: Language;
}

/**
* Convert system locales to our Transifex locales
*/
Expand All @@ -34,28 +40,34 @@ const {
languageCode, // language code without the region, eg: 'en'
languageTag, // language code with the region, eg: 'en-US'
} = systemLocales[0];
const systemLanguage: Language =
locales[languageTag.toLowerCase()] || locales[languageCode] || locales['en'];
const systemLanguage: Language = (locales as LocalesType)[languageTag.toLowerCase()] ||
(languageCode && (locales as LocalesType)[languageCode]) || {
locale: 'en',
code: 'en',
name: 'English',
nativeName: 'English',
};

/**
*
*/
export const useLocale = () => {
const { store } = useAppState();
const locale$ = store.locale$;

/**
* Store may or may not be available
* - get the locale object from the store setting, or the system locale, or fallback to 'en'
*/
const storeLocale = useObservableEagerState(store ? store.locale$ : of(null));
const storeLocale = useObservableEagerState<string | null | undefined>(locale$ || of(null));

const language = React.useMemo(() => {
let lang: Language = null;
let lang: Language = systemLanguage;
if (storeLocale) {
lang = Object.values(locales).find((l) => l.locale === storeLocale);
}
if (!lang) {
lang = systemLanguage;
const foundLang = Object.values(locales).find((l) => l.locale === storeLocale);
if (foundLang) {
lang = foundLang;
}
}
return lang;
}, [storeLocale]);
Expand Down
4 changes: 1 addition & 3 deletions src/screens/main/components/order/customer.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import * as React from 'react';

import find from 'lodash/find';
import { useObservableEagerState } from 'observable-hooks';

import { ButtonPill, ButtonText } from '@wcpos/components/src/button';
import { ButtonPill } from '@wcpos/components/src/button';
import { useDataTable } from '@wcpos/components/src/data-table';
import { FormatAddress } from '@wcpos/components/src/format';
import { VStack } from '@wcpos/components/src/vstack';
import { useQueryManager } from '@wcpos/query';

import useCustomerNameFormat from '../../hooks/use-customer-name-format';

Expand Down
3 changes: 0 additions & 3 deletions src/screens/main/orders/filter-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,6 @@ const FilterBar = ({ query }) => {
(inputs$) =>
inputs$.pipe(
switchMap(([id]) => {
if (toNumber(id) === 0) {
return of(guestCustomer);
}
if (!id) {
return of(null);
}
Expand Down
2 changes: 1 addition & 1 deletion src/screens/main/orders/orders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const Orders = () => {
selector: {
$and: [
{ meta_data: { $elemMatch: { key: '_pos_user', value: String(wpCredentials?.id) } } },
// { meta_data: { $elemMatch: { key: '_pos_store', value: String(store?.id) } } },
{ meta_data: { $elemMatch: { key: '_pos_store', value: String(store?.id) } } },
],
},
},
Expand Down
21 changes: 11 additions & 10 deletions src/screens/main/reports/chart/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,25 @@ import {
ResponsiveContainer,
} from 'recharts';

import type { OrderDocument } from '@wcpos/database';

import { Tooltip } from './tooltip';
import { aggregateData } from './utils';
import { useAppState } from '../../../../contexts/app-state';
import { useT } from '../../../../contexts/translations';

interface Props {
orders: OrderDocument[];
}
import { useNumberFormat } from '../../hooks/use-number-format';
import { useReports } from '../context';

/**
*
*/
export const Chart = ({ orders }: Props) => {
export const Chart = () => {
const t = useT();
const { store } = useAppState();
const currency = useObservableEagerState(store.currency$);
const { format: formatNumber } = useNumberFormat();
const { selectedOrders } = useReports();

const data = React.useMemo(() => aggregateData(orders), [orders]);
console.log('data', data);
const data = React.useMemo(() => aggregateData(selectedOrders), [selectedOrders]);
const maxOrderCount = Math.max(...data.map((item) => item.order_count));

return (
<ResponsiveContainer width="100%" height="100%">
Expand Down Expand Up @@ -72,6 +70,7 @@ export const Chart = ({ orders }: Props) => {
fontSize={12}
stroke="#243B53"
tick={{ fill: '#243B53' }}
tickFormatter={(value) => formatNumber(value)}
/>
<YAxis
yAxisId="orders"
Expand All @@ -88,8 +87,10 @@ export const Chart = ({ orders }: Props) => {
fontSize={12}
stroke="#243B53"
tick={{ fill: '#243B53' }}
tickFormatter={(value) => formatNumber(value)}
tickCount={maxOrderCount + 1}
/>
<RechartsTooltip content={<Tooltip />} />
<RechartsTooltip content={(props) => <Tooltip {...props} />} />
<Bar yAxisId="total" dataKey="total" stackId="a" fill="#127FBF" />
<Bar yAxisId="total" dataKey="total_tax" stackId="a" fill="#627D98" />
<Line yAxisId="orders" type="monotone" dataKey="order_count" stroke="#829AB1" />
Expand Down
63 changes: 49 additions & 14 deletions src/screens/main/reports/chart/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
format,
parseISO,
eachDayOfInterval,
eachHourOfInterval,
eachWeekOfInterval,
Expand All @@ -21,8 +20,28 @@ import type { OrderDocument } from '@wcpos/database';

import { convertUTCStringToLocalDate } from '../../../../hooks/use-local-date';

type Interval = 'months' | 'weeks' | 'days' | '6hours' | 'hours';
type Interval = 'months' | 'weeks' | 'days' | '2hours' | 'hours';

/**
* Get the start of the 2 hour interval for a given date
* @param date - The date to get the start of the interval for
* @returns The start of the 2 hour interval
*/
export const getStartOf2HourInterval = (date: Date): Date => {
const hours = date.getHours();
const intervalStartHour = Math.floor(hours / 2) * 2; // Nearest even hour
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), intervalStartHour, 0, 0, 0);
};

/**
* Determine the appropriate interval for the given date range
* @param minDate - The start date of the range
* @param maxDate - The end date of the range
* @returns An object containing the date format and interval
*
* @NOTE - the interval format is used for the key and the display, so be careful that they
* will produce a unique value for each date range.
*/
export const determineInterval = (
minDate: Date,
maxDate: Date
Expand All @@ -34,27 +53,32 @@ export const determineInterval = (
if (diffInDays > 30) {
return { format: 'yyyy-MM', interval: 'months' }; // Aggregate by month
} else if (diffInWeeks >= 1) {
return { format: "yyyy-'W'ww", interval: 'weeks' }; // Aggregate by week
return { format: 'yyyy-MM-dd', interval: 'weeks' }; // Aggregate by week
} else if (diffInDays > 1) {
return { format: 'yyyy-MM-dd', interval: 'days' }; // Aggregate by day
} else if (diffInHours > 6) {
return { format: 'yyyy-MM-dd HH', interval: '6hours' }; // Aggregate by 6 hours
} else if (diffInHours > 10) {
return { format: "HH':00'", interval: '2hours' }; // Aggregate by 2 hours
} else {
return { format: 'yyyy-MM-dd HH', interval: 'hours' }; // Aggregate by hour
return { format: "HH':00'", interval: 'hours' }; // Aggregate by hour
}
};

export const generateAllDates = (minDate: Date, maxDate: Date, interval: Interval) => {
export const generateDateIntervals = (minDate: Date, maxDate: Date, interval: Interval): Date[] => {
if (interval === 'months') {
return eachMonthOfInterval({ start: startOfMonth(minDate), end: maxDate });
} else if (interval === 'weeks') {
return eachWeekOfInterval({ start: startOfWeek(minDate), end: maxDate });
return eachWeekOfInterval(
{ start: startOfWeek(minDate, { weekStartsOn: 1 }), end: maxDate },
{ weekStartsOn: 1 }
);
} else if (interval === 'days') {
return eachDayOfInterval({ start: startOfDay(minDate), end: maxDate });
} else if (interval === '6hours') {
} else if (interval === '2hours') {
const dates = [];
for (let date = startOfHour(minDate); date <= maxDate; date = addHours(date, 6)) {
let date = getStartOf2HourInterval(minDate);
while (date <= maxDate) {
dates.push(date);
date = addHours(date, 2);
}
return dates;
} else {
Expand All @@ -63,7 +87,9 @@ export const generateAllDates = (minDate: Date, maxDate: Date, interval: Interva
};

/**
*
* Aggregate order data by date
* @param orders - The orders to aggregate
* @returns An array of aggregated order data
*/
export const aggregateData = (orders: OrderDocument[]) => {
const dataMap: {
Expand All @@ -90,15 +116,24 @@ export const aggregateData = (orders: OrderDocument[]) => {
const minDate = min(dates);
const maxDate = max(dates);
const { format: dateFormat, interval } = determineInterval(minDate, maxDate);
const allDates = generateAllDates(minDate, maxDate, interval);
const dateIntervals = generateDateIntervals(minDate, maxDate, interval);

orders.forEach((order) => {
const { date_created_gmt, total, total_tax } = order; // Extract properties

// Skip orders without a valid date?
if (!date_created_gmt) return;

const date = convertUTCStringToLocalDate(date_created_gmt);
let date = convertUTCStringToLocalDate(date_created_gmt);

// Special cases for intervals
if (interval === 'weeks') {
date = startOfWeek(date, { weekStartsOn: 1 });
} else if (interval === '2hours') {
date = getStartOf2HourInterval(date);
}

// This key should match a value in dateIntervals
const key = format(date, dateFormat);

if (!dataMap[key]) {
Expand All @@ -111,7 +146,7 @@ export const aggregateData = (orders: OrderDocument[]) => {
});

// Fill in missing dates
allDates.forEach((date) => {
dateIntervals.forEach((date) => {
const key = format(date, dateFormat);
if (!dataMap[key]) {
dataMap[key] = { date: key, total: 0, total_tax: 0, order_count: 0, dateObj: date };
Expand Down
73 changes: 73 additions & 0 deletions src/screens/main/reports/context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as React from 'react';

import { useObservableSuspense } from 'observable-hooks';

import type { OrderCollection, OrderDocument } from '@wcpos/database';
import type { Query } from '@wcpos/query';

import type { RowSelectionState } from '@tanstack/react-table';

interface ReportsContextType {
query: Query<OrderCollection>;
allOrders: OrderDocument[];
selectedOrders: OrderDocument[];
unselectedRowIds: RowSelectionState;
setUnselectedRowIds: React.Dispatch<React.SetStateAction<RowSelectionState>>;
}

/**
*
*/
const ReportsContext = React.createContext<ReportsContextType | undefined>(undefined);

/**
*
*/
export const useReports = () => {
const context = React.useContext(ReportsContext);
if (!context) {
throw new Error('useReports must be used within a ReportsContext');
}
return context;
};

/**
*
*/
export const ReportsProvider = ({ query, children }) => {
const result = useObservableSuspense(query.resource);
const [unselectedRowIds, setUnselectedRowIds] = React.useState<RowSelectionState>({});

/**
*
*/
const allOrders = React.useMemo(
() => result.hits.map((hit) => hit.document as OrderDocument),
[result.hits]
);

/**
* Remove unselectedRowIds from orders
*/
const selectedOrders = React.useMemo(() => {
if (Object.keys(unselectedRowIds).length === 0) {
return allOrders;
}

return allOrders.filter((order) => !unselectedRowIds[order.uuid]);
}, [allOrders, unselectedRowIds]);

return (
<ReportsContext.Provider
value={{
query,
allOrders,
selectedOrders,
unselectedRowIds,
setUnselectedRowIds,
}}
>
{children}
</ReportsContext.Provider>
);
};
4 changes: 3 additions & 1 deletion src/screens/main/reports/filter-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { HStack } from '@wcpos/components/src/hstack';
import { Suspense } from '@wcpos/components/src/suspense';
import { useQuery } from '@wcpos/query';

import { useReports } from './context';
import { useAppState } from '../../../contexts/app-state';
import { convertLocalDateToUTCString } from '../../../hooks/use-local-date';
import { CashierPill } from '../components/order/filter-bar/cashier-pill';
Expand All @@ -24,7 +25,8 @@ import { useGuestCustomer } from '../hooks/use-guest-customer';
/**
*
*/
export const FilterBar = ({ query }) => {
export const FilterBar = () => {
const { query } = useReports();
const guestCustomer = useGuestCustomer();
const customerID = useObservableEagerState(
query.params$.pipe(map(() => query.findSelector('customer_id')))
Expand Down
5 changes: 4 additions & 1 deletion src/screens/main/reports/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ErrorBoundary } from '@wcpos/components/src/error-boundary';
import { Suspense } from '@wcpos/components/src/suspense';
import { useQuery } from '@wcpos/query';

import { ReportsProvider } from './context';
import { Reports } from './reports';
import { useAppState } from '../../../contexts/app-state';
import { convertLocalDateToUTCString } from '../../../hooks/use-local-date';
Expand Down Expand Up @@ -56,7 +57,9 @@ const ReportsWithProviders = () => {
return (
<ErrorBoundary>
<Suspense>
<Reports query={query} />
<ReportsProvider query={query}>
<Reports />
</ReportsProvider>
</Suspense>
</ErrorBoundary>
);
Expand Down
Loading

0 comments on commit 31c2260

Please sign in to comment.