Skip to content

Commit

Permalink
- new feature in beta state - out of the stock variants
Browse files Browse the repository at this point in the history
- improvement - products in tables are now clickable
  • Loading branch information
radoslaw-sz committed Apr 24, 2024
1 parent 24807db commit 72ba637
Show file tree
Hide file tree
Showing 13 changed files with 336 additions and 51 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rsc-labs/medusa-store-analytics",
"version": "0.11.1",
"version": "0.12.0",
"description": "Get analytics data about your store",
"author": "RSC Labs (https://rsoftcon.com)",
"main": "dist/index.js",
Expand Down
4 changes: 3 additions & 1 deletion src/api/admin/products-analytics/[kind]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export const GET = async (
const orderStatuses: OrderStatus[] = orderStatusesFromQuery !== undefined ?
orderStatusesFromQuery.map(status => OrderStatus[status.toUpperCase()]).filter(orderStatus => orderStatus !== undefined): [];


let result: any;
const productsAnalyticsService: ProductsAnalyticsService = req.scope.resolve('productsAnalyticsService');

Expand Down Expand Up @@ -63,6 +62,9 @@ export const GET = async (
dateRangeToCompareTo ? new Date(Number(dateRangeToCompareTo)) : undefined,
);
break;
case 'out-of-the-stock-variants':
result = await productsAnalyticsService.getOutOfTheStockVariants();
break;
}
res.status(200).json({
analytics: result
Expand Down
88 changes: 75 additions & 13 deletions src/services/productsAnalytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@
* limitations under the License.
*/

import { LineItem, OrderStatus, Return, ReturnItem, TransactionBaseService } from "@medusajs/medusa"
import { LineItem, OrderStatus, ProductVariant, Return, ReturnItem, TransactionBaseService } from "@medusajs/medusa"
import { Order } from "@medusajs/medusa"
import { In } from "typeorm"

type VariantsCountPopularity = {
sum: string,
productId: string,
variantId: string,
productTitle: string,
variantTitle: string,
Expand All @@ -31,6 +32,22 @@ type VariantsCountPopularityResult = {
previous: VariantsCountPopularity[]
}

type OutOfTheStockVariantsCount = {
productId: string,
variantId: string,
productTitle: string,
variantTitle: string,
thumbnail: string,
}

type OutOfTheStockVariantsCountResult = {
dateRangeFrom?: number
dateRangeTo?: number,
dateRangeFromCompareTo?: number,
dateRangeToCompareTo?: number,
current: OutOfTheStockVariantsCount[],
}

export default class ProductsAnalyticsService extends TransactionBaseService {

private readonly TOP_LIMIT;
Expand Down Expand Up @@ -59,11 +76,11 @@ export default class ProductsAnalyticsService extends TransactionBaseService {
.andWhere(`order.status IN(:...orderStatusesAsStrings)`, { orderStatusesAsStrings });

const variantsSumInLinteItemsInOrders = await query
.groupBy('lineitem.variant_id, variant.id, lineitem.title, lineitem.thumbnail')
.orderBy('sum', 'DESC')
.setParameters({from, dateRangeFromCompareTo})
.limit(this.TOP_LIMIT)
.getRawMany()
.groupBy('lineitem.variant_id, variant.id, variant.product_id, lineitem.title, lineitem.thumbnail')
.orderBy('sum', 'DESC')
.setParameters({from, dateRangeFromCompareTo})
.limit(this.TOP_LIMIT)
.getRawMany()

return {
dateRangeFrom: from.getTime(),
Expand All @@ -72,6 +89,7 @@ export default class ProductsAnalyticsService extends TransactionBaseService {
dateRangeToCompareTo: undefined,
current: variantsSumInLinteItemsInOrders.map(result => (
{
productId: result.variant_product_id,
variantId: result.variant_id,
sum: result.sum,
variantTitle: result.variant_title,
Expand Down Expand Up @@ -118,11 +136,11 @@ export default class ProductsAnalyticsService extends TransactionBaseService {
.andWhere(`order.status IN(:...orderStatusesAsStrings)`, { orderStatusesAsStrings });

const variantsSumInLinteItemsInOrders = await query
.groupBy('lineitem.variant_id, variant.id, lineitem.title, lineitem.thumbnail')
.orderBy('sum', 'DESC')
.setParameters({startQueryFrom, dateRangeFromCompareTo})
.limit(this.TOP_LIMIT)
.getRawMany()
.groupBy('lineitem.variant_id, variant.id, variant.product_id, lineitem.title, lineitem.thumbnail')
.orderBy('sum', 'DESC')
.setParameters({startQueryFrom, dateRangeFromCompareTo})
.limit(this.TOP_LIMIT)
.getRawMany()

return {
dateRangeFrom: startQueryFrom.getTime(),
Expand All @@ -131,6 +149,7 @@ export default class ProductsAnalyticsService extends TransactionBaseService {
dateRangeToCompareTo: undefined,
current: variantsSumInLinteItemsInOrders.map(result => (
{
productId: result.variant_product_id,
variantId: result.variant_id,
sum: result.sum,
variantTitle: result.variant_title,
Expand Down Expand Up @@ -178,10 +197,11 @@ export default class ProductsAnalyticsService extends TransactionBaseService {
.select('lineItem.variant_id', 'variant_id')
.addSelect('lineItem.title', 'title')
.addSelect('variant.title', 'variant_title')
.addSelect('variant.product_id', 'product_id')
.addSelect('lineItem.thumbnail', 'thumbnail')
.addSelect('SUM(returnItem.quantity)', 'sum')
.where('return.created_at >= :from', { from })
.groupBy('lineItem.title, variant_title, lineItem.thumbnail, lineItem.variant_id')
.groupBy('lineItem.title, variant_title, variant.product_id, lineItem.thumbnail, lineItem.variant_id')

const variantsReturnedSum = await query
.orderBy('sum', 'DESC')
Expand All @@ -196,6 +216,7 @@ export default class ProductsAnalyticsService extends TransactionBaseService {
dateRangeToCompareTo: undefined,
current: variantsReturnedSum.map(result => (
{
productId: result.product_id,
variantId: result.variant_id,
sum: result.sum,
variantTitle: result.variant_title,
Expand Down Expand Up @@ -251,10 +272,11 @@ export default class ProductsAnalyticsService extends TransactionBaseService {
.select('lineItem.variant_id', 'variant_id')
.addSelect('lineItem.title', 'title')
.addSelect('variant.title', 'variant_title')
.addSelect('variant.product_id', 'product_id')
.addSelect('lineItem.thumbnail', 'thumbnail')
.addSelect('SUM(returnItem.quantity)', 'sum')
.where('return.created_at >= :startQueryFrom', { startQueryFrom })
.groupBy('lineItem.title, variant_title, lineItem.thumbnail, lineItem.variant_id')
.groupBy('lineItem.title, variant_title, variant.product_id, lineItem.thumbnail, lineItem.variant_id')

const variantsReturnedSum = await query
.orderBy('sum', 'DESC')
Expand All @@ -269,6 +291,7 @@ export default class ProductsAnalyticsService extends TransactionBaseService {
dateRangeToCompareTo: undefined,
current: variantsReturnedSum.map(result => (
{
productId: result.product_id,
variantId: result.variant_id,
sum: result.sum,
variantTitle: result.variant_title,
Expand Down Expand Up @@ -374,4 +397,43 @@ export default class ProductsAnalyticsService extends TransactionBaseService {
previous: undefined
}
}

async getOutOfTheStockVariants(limit?: number) : Promise<OutOfTheStockVariantsCountResult> {
const productStatusesAsStrings = ['published']
const query = this.activeManager_
.getRepository(ProductVariant)
.createQueryBuilder('productVariant')
.select("productVariant.id", "variant_id")
.addSelect("productVariant.updated_at", "updated_at")
.addSelect("productVariant.title", "variant_title")
.innerJoinAndSelect('productVariant.product', 'product')
.addSelect("product.thumbnail", "thumbnail")
.addSelect("product.title", "product_title")
.where(`product.status IN(:...productStatusesAsStrings)`, { productStatusesAsStrings })
.andWhere('productVariant.inventory_quantity = :expectedQuantity', { expectedQuantity: 0})
.andWhere('product.is_giftcard = :isGiftCard', { isGiftCard: false});

const outOfTheStockVariants = await query
.groupBy('productVariant.id, variant_title, product.id, product.thumbnail, product_title')
.orderBy('productVariant.updated_at', 'DESC')
.limit(limit !== undefined ? limit : this.TOP_LIMIT)
.getRawMany()

return {
dateRangeFrom: undefined,
dateRangeTo: undefined,
dateRangeFromCompareTo: undefined,
dateRangeToCompareTo: undefined,
current: outOfTheStockVariants.map(result => (
{
productId: result.product_id,
variantId: result.variant_id,
sum: result.sum,
variantTitle: result.variant_title,
productTitle: result.product_title,
thumbnail: result.thumbnail
}
)),
}
}
}
4 changes: 3 additions & 1 deletion src/ui-components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
export { ComparedDate, SwitchComparison, DropdownOrderStatus } from './common/overview-components';

export { OrdersOverviewCard } from './orders/orders-overview-card'
export { OrdersPaymentProviderCard } from './orders/orders-payment-provider-card'

export { SalesOverviewCard } from './sales/sales-overview-card'
export { SalesChannelPopularityCard } from './sales/sales-channel-popularity-card'
Expand All @@ -26,9 +27,10 @@ export { CumulativeCustomersCard } from './customers/cumulative-history/cumulati
export { VariantsTopByCountCard } from './products/variants-top-by-count';
export { ReturnedVariantsByCountCard } from './products/returned_variants/returned-variants-by-count';
export { ProductsSoldCountCard } from './products/products-sold-count';
export { OutOfTheStockVariantsCard } from './products/out_of_the_stock_variants/out-of-the-stock-variants-by-count';

export { DiscountsTopCard } from './marketing/discounts-top-by-count';

export { DateLasts, OrderStatus } from './utils/types'
export type { DateRange } from './utils/types'
export { convertDateLastsToComparedDateRange, convertDateLastsToDateRange } from './utils/helpers'
export { convertDateLastsToComparedDateRange, convertDateLastsToDateRange, amountToDisplay } from './utils/helpers'
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright 2024 RSC-Labs, https://rsoftcon.com/
*
* MIT License
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Heading, Alert, Tooltip, Badge } from "@medusajs/ui";
import { ArrowRightOnRectangle, InformationCircle } from "@medusajs/icons";
import { CircularProgress, Grid } from "@mui/material";
import { useAdminCustomQuery } from "medusa-react"
import { OutOfTheStockVariantsTable, OutOfTheStockVariantsTableRow } from "./out-of-the-stock-variants-table";
import { AdminOutOfTheStockVariantsStatisticsQuery, OutOfTheStockVariantsCountResponse, OutOfTheStockVariantsCountResult } from "./types";

function transformToVariantTopTable(result: OutOfTheStockVariantsCountResult): OutOfTheStockVariantsTableRow[] {
const currentMap = new Map<string, OutOfTheStockVariantsTableRow>();

result.current.forEach(currentItem => {
currentMap.set(currentItem.variantId, {
productId: currentItem.productId,
productTitle: currentItem.productTitle,
variantTitle: currentItem.variantTitle,
thumbnail: currentItem.thumbnail,
});
});

return Array.from(currentMap.values());
}

const OutOfTheStockVariants = () => {
const { data, isError, isLoading, error } = useAdminCustomQuery<
AdminOutOfTheStockVariantsStatisticsQuery,
OutOfTheStockVariantsCountResponse
>(
`/products-analytics/out-of-the-stock-variants`,
[],
{}
)

if (isLoading) {
return <CircularProgress size={12}/>
}

if (isError) {
const trueError = error as any;
const errorText = `Error when loading data. It shouldn't have happened - please raise an issue. For developer: ${trueError?.response?.data?.message}`
return <Alert variant="error">{errorText}</Alert>
}

if (data.analytics == undefined) {
return <Heading level="h3">Cannot get variants</Heading>
}

return <OutOfTheStockVariantsTable tableRows={transformToVariantTopTable(data.analytics)}/>
}

export const OutOfTheStockVariantsCard = () => {
return (
<Grid container paddingBottom={2} spacing={3}>
<Grid item>
<Grid container spacing={2} alignItems={'center'}>
<Grid item>
<ArrowRightOnRectangle/>
</Grid>
<Grid item>
<Heading level="h2">
Out of the stock variants
</Heading>
</Grid>
<Grid item>
<Tooltip content='It includes only published products and not gift cards'>
<InformationCircle />
</Tooltip>
</Grid>
<Grid item>
<Tooltip content='This feature might be changed or improved in the future'>
<Badge rounded="full" size="small" color="green">Beta</Badge>
</Tooltip>
</Grid>
</Grid>
</Grid>
<Grid item xs={12} md={12}>
<Heading level="h3">
Showing last 5 variants
</Heading>
</Grid>
<Grid item xs={12} md={12}>
<OutOfTheStockVariants/>
</Grid>
</Grid>
)
}
Loading

0 comments on commit 72ba637

Please sign in to comment.