import { MoneyAmount, Product } from "@medusajs/client-types" import { useAdminRegions, useAdminStore, useAdminUpdateVariant, } from "medusa-react" import { useEffect, useMemo, useRef, useState } from "react" import { getAllProductPricesCurrencies, getAllProductPricesRegions, getCurrencyPricesOnly, getRegionPricesOnly, } from "./utils" import mapKeys from "lodash/mapKeys" import pick from "lodash/pick" import pickBy from "lodash/pickBy" import useNotification from "../../../../hooks/use-notification" import { currencies as CURRENCY_MAP } from "../../../../utils/currencies" import Fade from "../../../atoms/fade-wrapper" import Button from "../../../fundamentals/button" import CrossIcon from "../../../fundamentals/icons/cross-icon" import Modal from "../../../molecules/modal" import DeletePrompt from "../../delete-prompt" import EditPricesActions from "./edit-prices-actions" import EditPricesTable from "./edit-prices-table" import SavePrompt from "./save-prompt" type EditPricesModalProps = { close: () => void product: Product } /** * Return map of regionIds <> currency_codes */ function useRegionsCurrencyMap() { const map = {} const { regions: storeRegions } = useAdminRegions({ limit: 1000, }) storeRegions?.forEach((r) => { map[r.id] = r.currency_code }) return useMemo(() => map, [storeRegions]) } /** * Edit prices modal container. */ function EditPricesModal(props: EditPricesModalProps) { const editedPrices = useRef({}) const { regions: storeRegions } = useAdminRegions({ limit: 1000, }) const { store } = useAdminStore() const regionCurrenciesMap = useRegionsCurrencyMap() const regions = getAllProductPricesRegions(props.product).sort() const currencies = getAllProductPricesCurrencies(props.product).sort() const notification = useNotification() const updateVariant = useAdminUpdateVariant(props.product.id) const [showCloseConfirmationPrompt, setShowCloseConfirmationPrompt] = useState(false) const [showSaveConfirmationPrompt, setShowSaveConfirmationPrompt] = useState(false) const initialCurrencies = !currencies.length && !regions.length ? store?.currencies.map((c) => c.code) : currencies const [selectedCurrencies, setSelectedCurrencies] = useState(initialCurrencies) const [selectedRegions, setSelectedRegions] = useState(regions) const toggleCurrency = (currencyCode: string) => { const set = new Set(selectedCurrencies) if (set.has(currencyCode)) { set.delete(currencyCode) } else { set.add(currencyCode) } setSelectedCurrencies(Array.from(set)) } const toggleRegion = (regionId: string) => { const set = new Set(selectedRegions) if (set.has(regionId)) { set.delete(regionId) } else { set.add(regionId) } setSelectedRegions(Array.from(set)) } const onPriceUpdate = (prices: Record) => { editedPrices.current = prices } const onSave = () => { detectHiddenEditedColumns() setShowSaveConfirmationPrompt(true) } const detectHiddenEditedColumns = () => { const initialState = {} // figure out which price cells were initially populated props.product.variants!.forEach((variant) => { currencies.forEach((c) => { const currencyMetadata = CURRENCY_MAP[c.toUpperCase()] const ma = getCurrencyPricesOnly(variant.prices!).find( (p) => p.currency_code === c ) if (ma) { initialState[`${variant.id}-${c}`] = ma.amount / Math.pow(10, currencyMetadata.decimal_digits) } }) regions.forEach((r) => { const ma = getRegionPricesOnly(variant.prices!).find( (p) => p.region_id === r ) if (ma) { const currencyMetadata = CURRENCY_MAP[ma.currency_code.toUpperCase()] initialState[`${variant.id}-${r}`] = ma.amount / Math.pow(10, currencyMetadata.decimal_digits) } }) }) const diff = { ...editedPrices.current } // all prices that differ from the initial populated value are changed prices Object.keys(initialState).forEach((k) => { if (initialState[k] === diff[k]) { delete diff[k] } }) const dirtyColumns = [ ...new Set(Object.keys(diff).map((k) => k.split("-")[1])), ] const hiddenDirtyColumns = dirtyColumns.filter( (c) => !selectedCurrencies.includes(c) && !selectedRegions.includes(c) ) return hiddenDirtyColumns.map((c) => { if (c.length === 3) { return c.toUpperCase() } else { return storeRegions?.find((r) => r.id === c)?.name || c } }) } const save = (saveOnlyVisible?: boolean) => { const pricesEditMap: Record = editedPrices.current const variants = props.product.variants! const promises = variants.map((variant) => { const variantPrices = variant.prices!.filter((p) => !p.price_list_id) // pick price edits that are related to the current variant const variantPricesEditMap = mapKeys( pickBy(pricesEditMap, (_, k) => k.includes(variant.id)), (_, k) => k.split("-")[1] ) const currencyPriceEdits = pickBy( variantPricesEditMap, (o, k) => !k.startsWith("reg") && (saveOnlyVisible ? selectedCurrencies.includes(k) : true) ) const regionPriceEdits = pickBy( variantPricesEditMap, (o, k) => k.startsWith("reg") && (saveOnlyVisible ? selectedRegions.includes(k) : true) ) const pricesPayload: Partial[] = [] variantPrices.forEach((price) => { if (price.region_id) { // region price if (price.region_id in regionPriceEdits) { // this MA is edited - UPDATE CASE if (typeof regionPriceEdits[price.region_id] === "number") { const p = { ...price } const num = regionPriceEdits[price.region_id]! * Math.pow( 10, CURRENCY_MAP[price.currency_code.toUpperCase()].decimal_digits ) p.amount = parseFloat(num.toFixed(0)) pricesPayload.push(p) } else { // amount is unset -> DELETED case just skip } } else { pricesPayload.push(price) // not edited just send it so it's not deleted } delete regionPriceEdits[price.region_id] } else { // currency price if (price.currency_code in currencyPriceEdits) { // this MA is edited - UPDATE CASE if (typeof currencyPriceEdits[price.currency_code] === "number") { const p = { ...price } const num = currencyPriceEdits[price.currency_code] * Math.pow( 10, CURRENCY_MAP[price.currency_code.toUpperCase()].decimal_digits ) p.amount = parseFloat(num.toFixed(0)) pricesPayload.push(p) } else { // amount is unset -> DELETED case just skip } } else { pricesPayload.push(price) // not edited just send it so it's not deleted } delete currencyPriceEdits[price.currency_code] // not deleted entries are new prices } }) Object.entries(currencyPriceEdits).forEach(([currency, amount]) => { if (typeof amount === "number") { const num = amount * Math.pow(10, CURRENCY_MAP[currency.toUpperCase()].decimal_digits) amount = parseFloat(num.toFixed(0)) pricesPayload.push({ currency_code: currency, amount }) } }) Object.entries(regionPriceEdits).forEach(([region, amount]) => { if (typeof amount === "number") { const currency = regionCurrenciesMap[region] const num = amount * Math.pow(10, CURRENCY_MAP[currency.toUpperCase()].decimal_digits) amount = parseFloat(num.toFixed(0)) pricesPayload.push({ region_id: region, amount }) } }) // @ts-ignore return updateVariant.mutateAsync({ variant_id: variant.id, prices: pricesPayload.map((p) => pick(p, ["id", "amount", "region_id", "currency_code"]) ), }) }) Promise.all(promises) .then(() => { notification( "Success", "Successfully updated variant prices", "success" ) props.close() }) .catch((e) => { notification("Error", "Failed to update variant prices", "error") }) } useEffect(() => { const onEsc = (e: KeyboardEvent) => { if (e.key === "Escape") { setShowCloseConfirmationPrompt(true) } } document.addEventListener("keydown", onEsc) return () => document.removeEventListener("keydown", onEsc) }, []) return (
esc
{showCloseConfirmationPrompt && ( setShowCloseConfirmationPrompt(false)} onDelete={async () => props.close()} successText={""} confirmText="Yes, close" heading="Close" text="Are you sure you want to close this editor without saving?" /> )} {showSaveConfirmationPrompt && ( save()} onSaveOnlyVisible={async () => save(true)} hiddenEditedColumns={detectHiddenEditedColumns()} handleClose={() => setShowSaveConfirmationPrompt(false)} /> )}
) } export default EditPricesModal