import React, { FC } from 'react' import { Button, CheckboxControl, Modal } from '@wordpress/components' import { createInterpolateElement } from '@wordpress/element' import { sprintf } from '@wordpress/i18n' import { useI18n } from '@wordpress/react-i18n' import { Badge, Notice, Text } from '@wordpress/ui' import apiFetch from '@wordpress/api-fetch' import CustomsProductCard, { EnteredForProduct } from './CustomsProductCard' import HsCodeSelector from './HsCodeSelector' import { goodsDescriptionOwner, ProductNeedingCustomsInfo, } from '../customsRules' import { customsInformationToSave, EnteredCustomsInformation, } from '../customsInformationToSave' import { useGoodsDescriptionSuggestions } from '../useGoodsDescriptionSuggestions' import { useAnnounce } from './useAnnounce' import { GoodsDescriptionSuggestion } from '../model/models' export declare interface CustomsModalProps { products: Array nvit: boolean onClose: () => void onSaved: () => void } declare interface SaveError { code?: string sku?: string saved?: Array } const savedAnything = (result: PromiseSettledResult) => result.status === 'fulfilled' || ((result.reason as SaveError)?.saved?.length ?? 0) > 0 const goodsDescriptionOwners = (products: Array) => [ ...new Set( products .filter(product => product.missingGoodsDescription) .map(product => goodsDescriptionOwner(product.item)) ), ] const suggestedDescriptions = ( suggestions: Record ): Record => Object.fromEntries( Object.entries(suggestions).flatMap( ([productId, { status, description }]) => status === 'SUGGESTED' && description ? [[productId, description]] : [] ) ) const post = (path: string, body: Array) => apiFetch({ path: `/posten-bring-checkout${path}`, method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) const CustomsModal: FC = ({ products, nvit, onClose, onSaved, }) => { const { __ } = useI18n() const [entered, setEntered] = React.useState({ hsCodes: {}, skus: {}, goodsDescriptions: {}, }) const [sameHsCodeForAll, setSameHsCodeForAll] = React.useState(false) const [hsCodeForAll, setHsCodeForAll] = React.useState('') const [saving, setSaving] = React.useState(false) const [saveError, setSaveError] = React.useState() const { loading, suggestions } = useGoodsDescriptionSuggestions( goodsDescriptionOwners(products) ) React.useEffect( () => setEntered(current => ({ ...current, goodsDescriptions: { ...suggestedDescriptions(suggestions), ...current.goodsDescriptions, }, })), [suggestions] ) const productsMissingHsCode = products.filter(it => it.missingHsCode) const toSave = customsInformationToSave(products, { ...entered, hsCodeForAll: sameHsCodeForAll ? hsCodeForAll : undefined, }) const nothingToSave = Object.values(toSave).every(it => it.length === 0) const enteredFor = ({ item, }: ProductNeedingCustomsInfo): EnteredForProduct => ({ hsCode: entered.hsCodes[item.product_id], sku: entered.skus[item.product_id], goodsDescription: entered.goodsDescriptions[goodsDescriptionOwner(item)], }) const enter = ( { item }: ProductNeedingCustomsInfo, { hsCode, sku, goodsDescription }: EnteredForProduct ) => setEntered(current => ({ hsCodes: hsCode === undefined ? current.hsCodes : { ...current.hsCodes, [item.product_id]: hsCode }, skus: sku === undefined ? current.skus : { ...current.skus, [item.product_id]: sku }, goodsDescriptions: goodsDescription === undefined ? current.goodsDescriptions : { ...current.goodsDescriptions, [goodsDescriptionOwner(item)]: goodsDescription, }, })) const saveErrorMessage = (error: SaveError) => error?.code === 'duplicateSku' ? sprintf( __( 'The SKU %s is already used by another product. Enter a different SKU.', 'posten-bring-checkout' ), error.sku ?? '' ) : __( 'Could not save customs information. Please try again.', 'posten-bring-checkout' ) const save = async () => { setSaving(true) setSaveError(undefined) const results = await Promise.allSettled( ( [ ['/hscodes', toSave.hsCodes], ['/skus', toSave.skus], ['/goods-descriptions', toSave.goodsDescriptions], ] as Array<[string, Array]> ) .filter(([, body]) => body.length > 0) .map(([path, body]) => post(path, body)) ) const failure = results.find( (result): result is PromiseRejectedResult => result.status === 'rejected' ) setSaving(false) if (results.some(savedAnything)) { onSaved() } if (failure) { setSaveError(saveErrorMessage(failure.reason)) } else { onClose() } } // Translated up front: the minifier would merge two calls in a ternary into __(nvit ? a : b), which make-pot cannot read const titles = { nvit: __( 'NVIT - Items that need customs information', 'posten-bring-checkout' ), other: __('Items that need customs information', 'posten-bring-checkout'), } const nvitInfo = __( 'For the fastest and most cost-effective shipping, shipments to Northern Norway are transported by rail through Sweden. Due to new EU regulations, these shipments now require customs information. Read more about new customs regulations for Norwegian goods in transit (NVIT)', 'posten-bring-checkout' ) useAnnounce(nvit && nvitInfo.replace(/<\/?link>/g, '')) return ( {nvit && ( {createInterpolateElement(nvitInfo, { link: ( ), })} )} {productsMissingHsCode.length > 1 && (
)} {sameHsCodeForAll && (
{__('All items', 'posten-bring-checkout')} setHsCodeForAll(hsCode ?? '')} />
)}
{products.map(product => ( enter(product, changes)} /> ))}
{saveError && (

{saveError}

)}
{productsMissingHsCode.length > 0 && ( {__( 'HS code data is provided by Tolletaten (Norwegian customs)', 'posten-bring-checkout' )} )}
) } export default CustomsModal