import React, { useCallback, useEffect, useMemo, useState } from "react"; import { TableBody, TableHead, TableRow } from "@mui/material"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; import Card from "@mui/material/Card"; import CardActions from "@mui/material/CardActions"; import Stack from "@mui/material/Stack"; import { useTheme } from "@mui/material/styles"; import Table from "../../../components/Table"; import { TableCell } from "../../../components/Table/TableCell"; import TableHeading from "../../../components/Table/TableHeading"; import TableCardHeader from "../../../components/TableCardHeader"; import { useApi } from "../../../contexts/ApiContext"; import { useDialog } from "../../../contexts/DialogContext"; import { useI18n } from "../../../contexts/I18nContext"; import { useUser } from "../../../contexts/UserContext"; import { hasPermission } from "../../../util/has_permission"; import { ArticlePriceListSerializer, ArticlePriceSerializer, ArticlePriceUpdateSerializer, } from "../types/contrib"; import { ArticlePriceRow } from "./ArticlePriceRow"; export interface ArticlePricingCardProps { code: string; prices: ArticlePriceSerializer[]; setPrices: (prices: ArticlePriceSerializer[]) => void; } export const ArticlePricingCard: React.FC = ({ code, prices, setPrices, }) => { const theme = useTheme(); const openDialog = useDialog(); const api = useApi(); const { t } = useI18n(); const { user } = useUser(); const [priceLists, setPriceLists] = useState([]); const [isEditing, setIsEditing] = useState(false); const [isDisabled, setIsDisabled] = useState(false); const [showActive, _setShowActive] = useState(true); const [showInactive, _setShowInactive] = useState(false); const [changedPrices, setChangedPrices] = useState>({}); const [deletedPrices, setDeletedPrices] = useState([]); const [createdPrices, setCreatedPrices] = useState([]); const hasChanges = useMemo( () => Object.entries(changedPrices).length > 0 || deletedPrices.length > 0 || createdPrices.length > 0, [changedPrices, deletedPrices, createdPrices], ); const shownPrices = useMemo(() => { if (!isEditing) { return prices.filter( (price) => (showActive && price.price_list.is_active) || (showInactive && !price.price_list.is_active), ); } return prices .map(({ amount, ...price }) => { if (deletedPrices.includes(price.price_list.id)) { return null; } return { ...price, amount: changedPrices[price.price_list.id] ?? amount, }; }) .filter((price): price is ArticlePriceSerializer => price !== null) .filter( (price) => (showActive && price.price_list.is_active) || (showInactive && !price.price_list.is_active), ); }, [prices, isEditing, showActive, showInactive, changedPrices, deletedPrices]); const unusedPriceLists = useMemo(() => { const usedPriceLists = [ ...shownPrices.map((price) => price.price_list.id), ...createdPrices.map((price) => price.price_list_id), ]; return [ ...priceLists.filter((pl) => !usedPriceLists.includes(pl.id)), // Include deleted price lists in the list of unused price lists. // These are not acquired from the OPTIONS call but are still guaranteed // to be valid. ...prices .map((p) => p.price_list) .filter((pl) => deletedPrices.includes(pl.id) && !usedPriceLists.includes(pl.id)), ]; }, [priceLists, shownPrices, createdPrices, deletedPrices]); const clear = useCallback(() => { setChangedPrices({}); setDeletedPrices([]); setCreatedPrices([]); setIsEditing(false); }, []); const allPriceLists = useMemo( () => [...priceLists, ...prices.map((p) => p.price_list)], [priceLists, prices], ); const sortedRows = useMemo(() => { const existingRows = shownPrices.map((price) => ({ kind: "existing" as const, price })); const createdRows = !isEditing ? [] : createdPrices .map((price, i) => { const priceList = allPriceLists.find((pl) => pl.id === price.price_list_id); if (!priceList) { return null; } return { kind: "created" as const, price: { price_list: priceList, amount: price.amount }, index: i, priceLists: [priceList, ...unusedPriceLists], }; }) .filter( ( row, ): row is { kind: "created"; price: ArticlePriceSerializer; index: number; priceLists: ArticlePriceListSerializer[]; } => row !== null, ); return [...existingRows, ...createdRows].sort((a, b) => { const plA = a.price.price_list; const plB = b.price.price_list; return ( plA.channel.localeCompare(plB.channel) || plA.site_code.localeCompare(plB.site_code) || plA.name.localeCompare(plB.name) ); }); }, [shownPrices, isEditing, createdPrices, allPriceLists, unusedPriceLists]); const save = useCallback(async () => { const action = api.operations["pricing.contrib:pricing-update"]; if (!action) { throw new Error('Invalid action "pricing.contrib:pricing-update".'); } setIsDisabled(true); const response = await action.call({ params: { code }, body: { updates: [ ...Object.entries(changedPrices).map(([price_list, amount]) => ({ price_list_id: parseInt(price_list), amount, })), ...createdPrices, ], deleted: deletedPrices.filter( (pl) => !createdPrices.map((p) => p.price_list_id).includes(pl), ), }, }); setIsDisabled(false); if (response.ok) { const updatedPriceLists = await response.json(); setPrices(updatedPriceLists); clear(); } else { console.error("[ARTICLE_PRICING_CARD]", response); } }, [api, prices, changedPrices, deletedPrices, createdPrices]); useEffect(() => { if (isEditing) { api.operations["pricing.contrib:pricing-options"] .call({ params: { code } }) .then(async (response) => { if (response.ok) { setPriceLists(await response.json()); } }); } }, [isEditing]); return ( { if (hasChanges) { if ( await openDialog( t("Unsaved changes"), t("You have unsaved changes. Are you sure you want to discard your changes?"), { ok: t("Discard changes"), cancel: t("Cancel"), }, ) ) { clear(); } } else { setIsEditing(isEditing); } }} /> {/* * TODO: Redesign and show filters when needed * * * * * * * * * * * */} {t("Price List")} {t("Channel")} {t("Site")} {t("Amount")} {t("Active")} .MuiTableCell-root": { borderBottom: "none" } }} > {shownPrices.length === 0 && !(isEditing && createdPrices.length > 0) && ( {t("No prices")} )} {sortedRows.map((row, i) => { if (row.kind === "existing") { return ( { setChangedPrices((prev) => ({ ...prev, [row.price.price_list.id]: amount, })); }} onDelete={() => { setDeletedPrices((prev) => [...prev, row.price.price_list.id]); }} /> ); } return ( { setCreatedPrices((prev) => prev.map((p, j) => (row.index === j ? { ...p, amount } : p)), ); }} onChangePriceList={(price_list) => { setCreatedPrices((prev) => prev.map((p, j) => (row.index === j ? { ...p, price_list_id: price_list } : p)), ); }} onDelete={() => { setCreatedPrices((prev) => prev.filter((_, j) => j !== row.index)); }} /> ); })}
{isEditing && ( )}
); };