From 72ba6374061807b40fc8dd9ad54f88453a1cd788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Szymkiewicz?= Date: Wed, 24 Apr 2024 19:19:36 +0200 Subject: [PATCH] - new feature in beta state - out of the stock variants - improvement - products in tables are now clickable --- package.json | 2 +- .../admin/products-analytics/[kind]/route.ts | 4 +- src/services/productsAnalytics.ts | 88 ++++++++++++++--- src/ui-components/index.ts | 4 +- .../out-of-the-stock-variants-by-count.tsx | 97 +++++++++++++++++++ .../out-of-the-stock-variants-table.tsx | 77 +++++++++++++++ .../out_of_the_stock_variants/types.ts | 21 ++++ .../products/products-sold-count.tsx | 2 +- .../returned-variants-by-count.tsx | 14 ++- .../returned-variants-table.tsx | 34 ++++--- .../products/variants-top-by-count.tsx | 4 +- .../products/variants-top-table.tsx | 34 ++++--- src/ui-components/tabs/products.tsx | 6 ++ 13 files changed, 336 insertions(+), 51 deletions(-) create mode 100644 src/ui-components/products/out_of_the_stock_variants/out-of-the-stock-variants-by-count.tsx create mode 100644 src/ui-components/products/out_of_the_stock_variants/out-of-the-stock-variants-table.tsx create mode 100644 src/ui-components/products/out_of_the_stock_variants/types.ts diff --git a/package.json b/package.json index a2db70a..d21efd0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/api/admin/products-analytics/[kind]/route.ts b/src/api/admin/products-analytics/[kind]/route.ts index 0b985b5..6a56317 100644 --- a/src/api/admin/products-analytics/[kind]/route.ts +++ b/src/api/admin/products-analytics/[kind]/route.ts @@ -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'); @@ -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 diff --git a/src/services/productsAnalytics.ts b/src/services/productsAnalytics.ts index 850b779..56ede2c 100644 --- a/src/services/productsAnalytics.ts +++ b/src/services/productsAnalytics.ts @@ -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, @@ -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; @@ -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(), @@ -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, @@ -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(), @@ -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, @@ -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') @@ -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, @@ -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') @@ -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, @@ -374,4 +397,43 @@ export default class ProductsAnalyticsService extends TransactionBaseService { previous: undefined } } + + async getOutOfTheStockVariants(limit?: number) : Promise { + 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 + } + )), + } + } } \ No newline at end of file diff --git a/src/ui-components/index.ts b/src/ui-components/index.ts index 8174d0e..6f0f916 100644 --- a/src/ui-components/index.ts +++ b/src/ui-components/index.ts @@ -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' @@ -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' \ No newline at end of file +export { convertDateLastsToComparedDateRange, convertDateLastsToDateRange, amountToDisplay } from './utils/helpers' \ No newline at end of file diff --git a/src/ui-components/products/out_of_the_stock_variants/out-of-the-stock-variants-by-count.tsx b/src/ui-components/products/out_of_the_stock_variants/out-of-the-stock-variants-by-count.tsx new file mode 100644 index 0000000..c7bdb2c --- /dev/null +++ b/src/ui-components/products/out_of_the_stock_variants/out-of-the-stock-variants-by-count.tsx @@ -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(); + + 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 + } + + 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 {errorText} + } + + if (data.analytics == undefined) { + return Cannot get variants + } + + return +} + +export const OutOfTheStockVariantsCard = () => { + return ( + + + + + + + + + Out of the stock variants + + + + + + + + + + Beta + + + + + + + Showing last 5 variants + + + + + + + ) +} \ No newline at end of file diff --git a/src/ui-components/products/out_of_the_stock_variants/out-of-the-stock-variants-table.tsx b/src/ui-components/products/out_of_the_stock_variants/out-of-the-stock-variants-table.tsx new file mode 100644 index 0000000..16ad4c5 --- /dev/null +++ b/src/ui-components/products/out_of_the_stock_variants/out-of-the-stock-variants-table.tsx @@ -0,0 +1,77 @@ +/* + * 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, Text } from "@medusajs/ui"; +import { Box, Divider, Grid } from "@mui/material"; +import { Link } from "react-router-dom" + +export type OutOfTheStockVariantsTableRow = { + productId: string, + productTitle: string, + variantTitle: string, + thumbnail: string, +} + +export const OutOfTheStockVariantsTable = ({tableRows} : {tableRows: OutOfTheStockVariantsTableRow[]}) => { + return ( + + + + + + + + + Variant + + + + + {tableRows.length > 0 ? tableRows.map(tableRow => ( + + + + + + {tableRow.thumbnail && + + } + + {tableRow.productTitle} - {tableRow.variantTitle} + + + + + + + )) : + + + + + None + + + + + } + + ) +} \ No newline at end of file diff --git a/src/ui-components/products/out_of_the_stock_variants/types.ts b/src/ui-components/products/out_of_the_stock_variants/types.ts new file mode 100644 index 0000000..ef3aaef --- /dev/null +++ b/src/ui-components/products/out_of_the_stock_variants/types.ts @@ -0,0 +1,21 @@ +export type AdminOutOfTheStockVariantsStatisticsQuery = {} + +export type OutOfTheStockVariantsCount = { + productId: string, + variantId: string, + productTitle: string, + variantTitle: string, + thumbnail: string, +} + +export type OutOfTheStockVariantsCountResult = { + dateRangeFrom?: number + dateRangeTo?: number, + dateRangeFromCompareTo?: number, + dateRangeToCompareTo?: number, + current: OutOfTheStockVariantsCount[], +} + +export type OutOfTheStockVariantsCountResponse = { + analytics: OutOfTheStockVariantsCountResult +} \ No newline at end of file diff --git a/src/ui-components/products/products-sold-count.tsx b/src/ui-components/products/products-sold-count.tsx index a6a2185..663be37 100644 --- a/src/ui-components/products/products-sold-count.tsx +++ b/src/ui-components/products/products-sold-count.tsx @@ -107,7 +107,7 @@ export const ProductsSoldCountCard = ({orderStatuses, dateRange, dateRangeCompar return ( - + diff --git a/src/ui-components/products/returned_variants/returned-variants-by-count.tsx b/src/ui-components/products/returned_variants/returned-variants-by-count.tsx index b468b09..0787e61 100644 --- a/src/ui-components/products/returned_variants/returned-variants-by-count.tsx +++ b/src/ui-components/products/returned_variants/returned-variants-by-count.tsx @@ -10,7 +10,7 @@ * limitations under the License. */ -import { Heading } from "@medusajs/ui"; +import { Heading, Alert } from "@medusajs/ui"; import { ShoppingBag } from "@medusajs/icons"; import { CircularProgress, Grid } from "@mui/material"; import { DateRange } from "../../utils/types"; @@ -26,6 +26,7 @@ type AdminProductsStatisticsQuery = { type VariantsCountPopularity = { sum: string, + productId: string, variantId: string, productTitle: string, variantTitle: string, @@ -51,6 +52,7 @@ function transformToVariantTopTable(result: VariantsCountPopularityResult): Vari result.current.forEach(currentItem => { const currentCount = currentMap.get(currentItem.variantId) ? currentMap.get(currentItem.variantId).sum : '0'; currentMap.set(currentItem.variantId, { + productId: currentItem.productId, productTitle: currentItem.productTitle, variantTitle: currentItem.variantTitle, thumbnail: currentItem.thumbnail, @@ -63,7 +65,7 @@ function transformToVariantTopTable(result: VariantsCountPopularityResult): Vari const ReturnedVariantsByCount = ({dateRange, dateRangeCompareTo} : { dateRange?: DateRange, dateRangeCompareTo?: DateRange}) => { - const { data, isLoading } = useAdminCustomQuery< + const { data, isError, isLoading, error } = useAdminCustomQuery< AdminProductsStatisticsQuery, VariantsCountPopularityResponse >( @@ -81,6 +83,12 @@ const ReturnedVariantsByCount = ({dateRange, dateRangeCompareTo} : { return } + 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 {errorText} + } + if (data.analytics == undefined) { return Cannot get orders or products } @@ -97,7 +105,7 @@ export const ReturnedVariantsByCountCard = ({dateRange, dateRangeCompareTo} : return ( - + diff --git a/src/ui-components/products/returned_variants/returned-variants-table.tsx b/src/ui-components/products/returned_variants/returned-variants-table.tsx index 20a4011..d98cace 100644 --- a/src/ui-components/products/returned_variants/returned-variants-table.tsx +++ b/src/ui-components/products/returned_variants/returned-variants-table.tsx @@ -12,9 +12,11 @@ import { Heading, Text } from "@medusajs/ui"; import { Box, Divider, Grid } from "@mui/material"; +import { Link } from "react-router-dom" export type VariantsTopTableRow = { sum: string, + productId: string, productTitle: string, variantTitle: string, thumbnail: string, @@ -44,22 +46,24 @@ export const ReturnedVariantsTable = ({tableRows} : {tableRows: VariantsTopTable - - {tableRow.thumbnail && - - } - - {tableRow.productTitle} - {tableRow.variantTitle} + + + {tableRow.thumbnail && + + } + + {tableRow.productTitle} - {tableRow.variantTitle} + - + diff --git a/src/ui-components/products/variants-top-by-count.tsx b/src/ui-components/products/variants-top-by-count.tsx index 768e49b..b025899 100644 --- a/src/ui-components/products/variants-top-by-count.tsx +++ b/src/ui-components/products/variants-top-by-count.tsx @@ -28,6 +28,7 @@ type AdminProductsStatisticsQuery = { type VariantsCountPopularity = { sum: string, + productId: string, variantId: string, productTitle: string, variantTitle: string, @@ -53,6 +54,7 @@ function transformToVariantTopTable(result: VariantsCountPopularityResult): Vari result.current.forEach(currentItem => { const currentCount = currentMap.get(currentItem.variantId) ? currentMap.get(currentItem.variantId).sum : '0'; currentMap.set(currentItem.variantId, { + productId: currentItem.productId, productTitle: currentItem.productTitle, variantTitle: currentItem.variantTitle, thumbnail: currentItem.thumbnail, @@ -106,7 +108,7 @@ export const VariantsTopByCountCard = ({orderStatuses, dateRange, dateRangeCompa return ( - + diff --git a/src/ui-components/products/variants-top-table.tsx b/src/ui-components/products/variants-top-table.tsx index 24fa08b..1e6ed54 100644 --- a/src/ui-components/products/variants-top-table.tsx +++ b/src/ui-components/products/variants-top-table.tsx @@ -12,9 +12,11 @@ import { Heading, Text } from "@medusajs/ui"; import { Box, Divider, Grid } from "@mui/material"; +import { Link } from "react-router-dom" export type VariantsTopTableRow = { sum: string, + productId: string, productTitle: string, variantTitle: string, thumbnail: string, @@ -44,22 +46,24 @@ export const VariantsTopTable = ({tableRows} : {tableRows: VariantsTopTableRow[] - - {tableRow.thumbnail && - - } - - {tableRow.productTitle} - {tableRow.variantTitle} + + + {tableRow.thumbnail && + + } + + {tableRow.productTitle} - {tableRow.variantTitle} + - + diff --git a/src/ui-components/tabs/products.tsx b/src/ui-components/tabs/products.tsx index 1ac391d..b4f430b 100644 --- a/src/ui-components/tabs/products.tsx +++ b/src/ui-components/tabs/products.tsx @@ -19,6 +19,7 @@ import { DateRange } from '..'; import { Grid } from "@mui/material"; +import { OutOfTheStockVariantsCard } from "../products/out_of_the_stock_variants/out-of-the-stock-variants-by-count"; const ProductsTab = ({orderStatuses, dateRange, dateRangeCompareTo, compareEnabled} : {orderStatuses: OrderStatus[], dateRange?: DateRange, dateRangeCompareTo?: DateRange, compareEnabled: boolean}) => { @@ -39,6 +40,11 @@ const ProductsTab = ({orderStatuses, dateRange, dateRangeCompareTo, compareEnabl + + + + + ) }