Skip to content

Instantly share code, notes, and snippets.

@jay-babu
Created November 6, 2025 18:30
Show Gist options
  • Select an option

  • Save jay-babu/1974d4776ac2c44f7165279af0d3b414 to your computer and use it in GitHub Desktop.

Select an option

Save jay-babu/1974d4776ac2c44f7165279af0d3b414 to your computer and use it in GitHub Desktop.
diff --git a/src/components/common/PurchaseOrderFinalizationDialog.tsx b/src/components/common/PurchaseOrderFinalizationDialog.tsx
index 7d12de80d..38429496b 100644
--- a/src/components/common/PurchaseOrderFinalizationDialog.tsx
+++ b/src/components/common/PurchaseOrderFinalizationDialog.tsx
@@ -11,6 +11,7 @@ import {
import { Label } from "components/ui/label";
import { ReactNode, useEffect, useMemo } from "react";
import { useForm } from "react-hook-form";
+import { Loader2 } from "lucide-react";
export interface FinalizationOption {
id: string;
@@ -30,6 +31,8 @@ export interface PurchaseOrderFinalizationDialogProps {
description?: string;
isLoading?: boolean;
onActionComplete?: (actionId: string) => void;
+ replyToEmail?: string;
+ onReplyToEmailChange?: (v: string) => void;
}
interface FormData {
@@ -46,6 +49,8 @@ export const PurchaseOrderFinalizationDialog: React.FC<
description = "Select the actions you'd like to perform after finalizing this purchase order:",
isLoading = false,
onActionComplete,
+ replyToEmail,
+ onReplyToEmailChange,
}) => {
// Create default values for the form
const defaultValues = useMemo(() => {
@@ -106,49 +111,78 @@ export const PurchaseOrderFinalizationDialog: React.FC<
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
- <form onSubmit={form.handleSubmit(onSubmit)}>
- <div className="space-y-4 py-4">
- {options.map(
- (option) =>
- option.visible !== false && (
- <div key={option.id} className="flex items-start gap-3">
- <Checkbox
- id={option.id}
- checked={
- form.watch(`selectedOptions.${option.id}`) || false
- }
- onCheckedChange={(checked) => {
- form.setValue(
- `selectedOptions.${option.id}`,
- checked as boolean,
- );
- }}
- disabled={
- option.enabled === false || isLoading || isProcessing
- }
- />
- <div className="grid gap-1.5">
- <Label
- htmlFor={option.id}
- className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
- >
- {option.label}
- </Label>
- <p className="text-sm text-muted-foreground">
- {option.description}
- </p>
- </div>
- </div>
- ),
- )}
+ {isLoading ? (
+ <div className="py-10 flex items-center justify-center">
+ <Loader2 className="h-6 w-6 animate-spin" />
</div>
+ ) : (
+ <form onSubmit={form.handleSubmit(onSubmit)}>
+ <div className="space-y-4 py-4">
+ {onReplyToEmailChange && (
+ <div className="grid gap-1.5">
+ <Label
+ htmlFor="reply-to"
+ className="text-sm font-medium leading-none"
+ >
+ Reply-To Email
+ </Label>
+ <input
+ id="reply-to"
+ type="email"
+ value={replyToEmail}
+ onChange={(e) => onReplyToEmailChange(e.target.value)}
+ placeholder="[email protected]"
+ className="border rounded-md px-2 py-1 text-sm"
+ />
+ <p className="text-xs text-muted-foreground">
+ This is the email receipents will send to by default when
+ replying. You can update the default address in Settings →
+ Store Preferences → Purchase Order Email.
+ </p>
+ </div>
+ )}
+ {options.map(
+ (option) =>
+ option.visible !== false && (
+ <div key={option.id} className="flex items-start gap-3">
+ <Checkbox
+ id={option.id}
+ checked={
+ form.watch(`selectedOptions.${option.id}`) || false
+ }
+ onCheckedChange={(checked) => {
+ form.setValue(
+ `selectedOptions.${option.id}`,
+ checked as boolean,
+ );
+ }}
+ disabled={
+ option.enabled === false || isLoading || isProcessing
+ }
+ />
+ <div className="grid gap-1.5">
+ <Label
+ htmlFor={option.id}
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+ >
+ {option.label}
+ </Label>
+ <p className="text-sm text-muted-foreground">
+ {option.description}
+ </p>
+ </div>
+ </div>
+ ),
+ )}
+ </div>
- <DialogFooter>
- <Button type="submit" disabled={isLoading || isProcessing}>
- {isProcessing ? "Processing..." : "Confirm"}
- </Button>
- </DialogFooter>
- </form>
+ <DialogFooter>
+ <Button type="submit" disabled={isLoading || isProcessing}>
+ {isProcessing ? "Processing..." : "Confirm"}
+ </Button>
+ </DialogFooter>
+ </form>
+ )}
</DialogContent>
</Dialog>
);
diff --git a/src/components/po/EmailPOButton.tsx b/src/components/po/EmailPOButton.tsx
index 5bbd79de1..603a64641 100644
--- a/src/components/po/EmailPOButton.tsx
+++ b/src/components/po/EmailPOButton.tsx
@@ -7,12 +7,15 @@ import { flushSync } from "react-dom";
import { FaEnvelope } from "react-icons/fa6";
import PrintPO, { PrintPOProps } from "./PrintPO";
-export type EmailPOButtonProps = PrintPOProps & {};
+export type EmailPOButtonProps = PrintPOProps & {
+ promptForReplyTo?: (cb: (email: string) => Promise<void>) => void;
+};
export const EmailPOButton: React.FC<EmailPOButtonProps> = ({ ...props }) => {
// conditionally render the content otherwise the page gets really laggy
const [show, setShow] = useState(false);
const [isLoading, setIsLoading] = useState(false);
+ const { promptForReplyTo } = props;
const {
isEmailPending,
printViewContentRef,
@@ -21,21 +24,24 @@ export const EmailPOButton: React.FC<EmailPOButtonProps> = ({ ...props }) => {
onDismissEmailResponse,
} = useEmailPurchaseOrder(props.purchaseOrder?.metadata.id ?? 0);
- const handleRenderAndEmail = useCallback(async () => {
- flushSync(() => {
- setIsLoading(true);
- setShow(true);
- });
- try {
- await handleEmailPurchaseOrder();
- } catch (error) {
- console.error(error);
- captureException(error);
- } finally {
- setIsLoading(false);
- setShow(false);
- }
- }, [handleEmailPurchaseOrder]);
+ const handleRenderAndEmail = useCallback(
+ async (opts?: { replyTo?: string }) => {
+ flushSync(() => {
+ setIsLoading(true);
+ setShow(true);
+ });
+ try {
+ await handleEmailPurchaseOrder(opts);
+ } catch (error) {
+ console.error(error);
+ captureException(error);
+ } finally {
+ setIsLoading(false);
+ setShow(false);
+ }
+ },
+ [handleEmailPurchaseOrder],
+ );
return (
<>
@@ -44,7 +50,11 @@ export const EmailPOButton: React.FC<EmailPOButtonProps> = ({ ...props }) => {
colorScheme="blue"
leftIcon={<FaEnvelope />}
isLoading={isLoading || isEmailPending}
- onClick={handleRenderAndEmail}
+ onClick={() =>
+ promptForReplyTo?.(async (email) => {
+ await handleRenderAndEmail({ replyTo: email });
+ })
+ }
>
Email
</Button>
diff --git a/src/components/po/PurchaseOrderItemCard.tsx b/src/components/po/PurchaseOrderItemCard.tsx
index ebdf17601..d6a2ce8f8 100644
--- a/src/components/po/PurchaseOrderItemCard.tsx
+++ b/src/components/po/PurchaseOrderItemCard.tsx
@@ -88,6 +88,7 @@ export type PurchaseOrderItemCardProps = CardProps & {
hideTransfers?: boolean;
transferStatus?: PredicateState;
metadata?: PurchaseOrderEntityDb["metadata"];
+ promptForReplyTo?: (cb: (email: string) => Promise<void>) => void;
};
const columnHelper = createColumnHelper<CandidatePurchaseOrderItem>();
@@ -108,6 +109,7 @@ export const PurchaseOrderItemCard: React.FC<PurchaseOrderItemCardProps> = ({
hideTransfers,
transferStatus,
metadata,
+ promptForReplyTo,
...props
}) => {
const [showNotes, setShowNotes] = useState(false);
@@ -732,6 +734,7 @@ export const PurchaseOrderItemCard: React.FC<PurchaseOrderItemCardProps> = ({
<ButtonGroup>
{poStatus === PurchaseOrderStatusEnum.PURCHASED && (
<EmailPOButton
+ promptForReplyTo={promptForReplyTo}
purchaseOrder={purchaseOrder}
distributor={row.original.cohortVendorId}
entityId={entityId}
diff --git a/src/components/po/ReplyToPrompt.tsx b/src/components/po/ReplyToPrompt.tsx
new file mode 100644
index 000000000..c1d613058
--- /dev/null
+++ b/src/components/po/ReplyToPrompt.tsx
@@ -0,0 +1,83 @@
+import {
+ Modal,
+ ModalOverlay,
+ ModalContent,
+ ModalHeader,
+ ModalBody,
+ ModalFooter,
+ Button,
+ FormControl,
+ FormLabel,
+ Input,
+ Spinner,
+ Center,
+} from "@chakra-ui/react";
+import { useEffect, useState } from "react";
+
+type ReplyToPromptProps = {
+ isOpen: boolean;
+ onSubmit: (email: string) => void;
+ onCancel: () => void;
+ defaultEmail: string;
+ isLoading?: boolean;
+};
+
+export const ReplyToPrompt: React.FC<ReplyToPromptProps> = ({
+ isOpen,
+ onSubmit,
+ onCancel,
+ defaultEmail,
+ isLoading = false,
+}) => {
+ const [email, setEmail] = useState(defaultEmail);
+
+ useEffect(() => {
+ if (isOpen) setEmail(defaultEmail);
+ }, [isOpen, defaultEmail]);
+
+ const handleSubmit = () => onSubmit(email.trim());
+
+ return (
+ <Modal
+ isOpen={isOpen}
+ onClose={onCancel}
+ onCloseComplete={() => setEmail(defaultEmail ?? "")}
+ isCentered
+ >
+ <ModalOverlay />
+ <ModalContent>
+ <ModalHeader>Reply-To Email</ModalHeader>
+ <ModalBody>
+ {isLoading ? (
+ <Center py={10}>
+ <Spinner size="lg" />
+ </Center>
+ ) : (
+ <>
+ <FormControl>
+ <FormLabel>Reply-To address</FormLabel>
+ <Input
+ type="email"
+ value={email}
+ onChange={(e) => setEmail(e.target.value)}
+ />
+ </FormControl>
+ <p className="text-xs text-gray-500 mt-2">
+ You can change the default reply-to email in Settings → Store
+ Preferences → Purchase Order Reply-To Email.
+ </p>
+ </>
+ )}
+ </ModalBody>
+ <ModalFooter gap={2}>
+ <Button variant="ghost" onClick={onCancel}>
+ Cancel
+ </Button>
+ <Button colorScheme="blue" onClick={handleSubmit}>
+ Send
+ </Button>
+ </ModalFooter>
+ </ModalContent>
+ </Modal>
+ );
+};
diff --git a/src/context/Sales/SalesContextProvider.tsx b/src/context/Sales/SalesContextProvider.tsx
index 89c6c39dc..8518a921d 100644
--- a/src/context/Sales/SalesContextProvider.tsx
+++ b/src/context/Sales/SalesContextProvider.tsx
@@ -389,7 +389,6 @@ export const SalesContextProvider: React.FC<SalesContextProviderProps> = ({
if (!register.isRegisterOpen) return;
if (!itemsUndefined) return;
if (!isPaymentMethodsFetched) return;
- if (transactionStoreLoading) return;
setItems(new Map());
const t = async () => {
try {
@@ -493,7 +492,6 @@ export const SalesContextProvider: React.FC<SalesContextProviderProps> = ({
toast,
transactionExternalId,
transactionId,
- transactionStoreLoading,
]);
// State
diff --git a/src/pages/PurchaseOrders/PurchaseOrderDetails.tsx b/src/pages/PurchaseOrders/PurchaseOrderDetails.tsx
index e30785d71..dfbc6e0f8 100644
--- a/src/pages/PurchaseOrders/PurchaseOrderDetails.tsx
+++ b/src/pages/PurchaseOrders/PurchaseOrderDetails.tsx
@@ -62,6 +62,7 @@ import { DefaultLayoutWithNavbar } from "../../components/layouts/DefaultLayoutW
import { PurchaseOrderCandidateItems } from "../../components/po/PurchaseOrderCandidateItems";
import { PurchaseOrderItemCard } from "../../components/po/PurchaseOrderItemCard";
import { PurchaseOrderStatusTag } from "../../components/po/PurchaseOrderStatusTag";
+import { useAuthorization } from "context/AuthorizationContext/useAuthorizationContext";
import {
CandidatePurchaseOrderItem,
CohortVendorId,
@@ -74,6 +75,7 @@ import {
useModifyPurchaseOrderItemBulk,
usePatchPurchaseOrder,
} from "../../generated";
+import { ReplyToPrompt } from "components/po/ReplyToPrompt";
import { useAllVendors } from "../../hooks/useAllVendors";
import { useHybridUserConfiguration } from "../../hooks/useHybridUserConfiguration";
@@ -91,12 +93,24 @@ const PurchaseOrderDetails: React.FC<PurchaseOrderDetailsProps> = ({
setQuery,
}) => {
const [currentEntity, setCurrentEntity] = useState(0);
+ const { authorizedUser } = useAuthorization();
const { id } = useParams();
const { edit: isEditing } = query;
const { value: SHOULD_SHOW_VENDOR_PRICING_CARD } = useEntityConfig(
EntityConfigKey.SHOW_VENDOR_PRICING_CARD,
true,
);
+ const replyToPrompt = useDisclosure();
+ const [replyToCallback, setReplyToCallback] = useState<
+ ((email: string) => Promise<void>) | null
+ >(null);
+ const promptForReplyTo = useCallback(
+ (cb: (email: string) => Promise<void>) => {
+ setReplyToCallback(() => cb);
+ replyToPrompt.onOpen();
+ },
+ [replyToPrompt],
+ );
const idAsNumber = id ? parseInt(id) : 0;
const queryClient = useQueryClient();
const toast = useToast();
@@ -340,6 +354,14 @@ const PurchaseOrderDetails: React.FC<PurchaseOrderDetailsProps> = ({
]);
const activeEntity = currentEntity || tabs[0]?.id;
+ const { value: replyToDefault, isFetching: isReplyToFetching } =
+ useEntityConfig<string>(
+ EntityConfigKey.PURCHASE_ORDER_REPLY_TO_EMAIL,
+ authorizedUser?.user?.email ?? "",
+ activeEntity,
+ );
+
+ const [replyToEmail, setReplyToEmail] = useState(replyToDefault ?? "");
const currentItemsByVendor = useMemo(
// @ts-expect-error
@@ -507,6 +529,7 @@ const PurchaseOrderDetails: React.FC<PurchaseOrderDetailsProps> = ({
});
// Then show the dialog for additional actions
+ setReplyToEmail(replyToDefault ?? "");
finalizationDialog.onOpen();
} catch (error) {
toast({ title: "Failed to finalize PO", status: "error" });
@@ -521,6 +544,7 @@ const PurchaseOrderDetails: React.FC<PurchaseOrderDetailsProps> = ({
finalizationDialog,
checkVendorValidity,
vendorReviewDisclosure,
+ replyToDefault,
]);
const handleCreateTransfers = useCallback(async () => {
@@ -607,6 +631,7 @@ const PurchaseOrderDetails: React.FC<PurchaseOrderDetailsProps> = ({
});
// Then show the dialog for additional actions
+ setReplyToEmail(replyToDefault ?? "");
finalizationDialog.onOpen();
},
[
@@ -617,6 +642,7 @@ const PurchaseOrderDetails: React.FC<PurchaseOrderDetailsProps> = ({
idAsNumber,
updatePurchaseOrder,
finalizationDialog,
+ replyToDefault,
],
);
@@ -758,7 +784,14 @@ const PurchaseOrderDetails: React.FC<PurchaseOrderDetailsProps> = ({
isUpdatePending ||
isFetchingPurchaseOrder
}
- onClick={handleEmailPurchaseOrder}
+ onClick={() => {
+ setReplyToCallback(() => async (email: string) => {
+ await handleEmailPurchaseOrder({
+ replyTo: email.trim(),
+ });
+ });
+ replyToPrompt.onOpen();
+ }}
>
Email POs
</Button>
@@ -805,6 +838,20 @@ const PurchaseOrderDetails: React.FC<PurchaseOrderDetailsProps> = ({
</Button>
</>
)}
+ <ReplyToPrompt
+ isOpen={replyToPrompt.isOpen}
+ onCancel={() => {
+ setReplyToCallback(null);
+ replyToPrompt.onClose();
+ }}
+ onSubmit={async (email) => {
+ replyToPrompt.onClose();
+ setReplyToCallback(null);
+ if (replyToCallback) await replyToCallback(email);
+ }}
+ defaultEmail={replyToDefault}
+ isLoading={isReplyToFetching}
+ />
<ConfirmationDialog
title="Caution: Complete PO Without Receiving"
description="This will mark all items across all locations as received. Is this what you'd like to do?"
@@ -897,6 +944,7 @@ const PurchaseOrderDetails: React.FC<PurchaseOrderDetailsProps> = ({
</div>
)}
<PurchaseOrderItemCard
+ promptForReplyTo={promptForReplyTo}
title={
allPossibleEntities.find((e) => activeEntity === e.id)
?.name
@@ -997,12 +1045,13 @@ const PurchaseOrderDetails: React.FC<PurchaseOrderDetailsProps> = ({
{
id: "send-emails",
label: "Send Emails",
- description:
- "Send purchase order emails to vendors with contact information",
+ description: `Send purchase order emails to vendors with contact information.`,
enabled: true,
action: async () => {
// Then send emails
- await handleEmailPurchaseOrder();
+ await handleEmailPurchaseOrder({
+ replyTo: replyToEmail.trim(),
+ });
toast({
title: "Emails sent",
status: "success",
@@ -1015,8 +1064,13 @@ const PurchaseOrderDetails: React.FC<PurchaseOrderDetailsProps> = ({
},
]}
isLoading={
- isTransferCreationPending || isEmailPending || isUpdatePending
+ isReplyToFetching ||
+ isTransferCreationPending ||
+ isEmailPending ||
+ isUpdatePending
}
+ replyToEmail={replyToEmail}
+ onReplyToEmailChange={setReplyToEmail}
/>
<ConfirmationDialog
title="Warning: Duplicate Transfers"
diff --git a/src/pages/PurchaseOrders/useEmailPurchaseOrder.tsx b/src/pages/PurchaseOrders/useEmailPurchaseOrder.tsx
index 47d8f5361..85b4c830b 100644
--- a/src/pages/PurchaseOrders/useEmailPurchaseOrder.tsx
+++ b/src/pages/PurchaseOrders/useEmailPurchaseOrder.tsx
@@ -36,46 +36,51 @@ export const useEmailPurchaseOrder = (purchaseOrderId: number) => {
});
const printViewContentRef = useRef<HTMLDivElement>(null);
- const handleEmailPurchaseOrder = useCallback(async () => {
- if (!printViewContentRef.current) {
- return;
- }
+ const handleEmailPurchaseOrder = useCallback(
+ async (opts?: { replyTo?: string }) => {
+ if (!printViewContentRef.current) {
+ return;
+ }
- setPending(true);
+ setPending(true);
- const purchaseOrderPrintViews: HTMLElement[] = Array.from(
- printViewContentRef.current.children[0]?.children ?? [],
- ).map((child) => child as HTMLElement);
+ const purchaseOrderPrintViews: HTMLElement[] = Array.from(
+ printViewContentRef.current.children[0]?.children ?? [],
+ ).map((child) => child as HTMLElement);
- const cleanupFromPrint = initializeDocumentForPrint(
- printViewContentRef.current,
- );
+ const cleanupFromPrint = initializeDocumentForPrint(
+ printViewContentRef.current,
+ );
- try {
- const formData = new FormData();
- await Promise.all(
- purchaseOrderPrintViews.map(
- async (purchaseOrderPrintView: HTMLElement) => {
- const entityId = purchaseOrderPrintView.dataset.entityId!!;
- const vendorId = purchaseOrderPrintView.dataset.vendorId!!;
+ try {
+ const formData = new FormData();
+ const replyToEmail = (opts?.replyTo ?? "").trim();
+ formData.append("replyToEmail", replyToEmail);
+ await Promise.all(
+ purchaseOrderPrintViews.map(
+ async (purchaseOrderPrintView: HTMLElement) => {
+ const entityId = purchaseOrderPrintView.dataset.entityId!!;
+ const vendorId = purchaseOrderPrintView.dataset.vendorId!!;
- const pdf = await generateMultiPagePDFFromHTMLElement(
- purchaseOrderPrintView,
- );
+ const pdf = await generateMultiPagePDFFromHTMLElement(
+ purchaseOrderPrintView,
+ );
- formData.append("vendorIds", vendorId);
- formData.append("entityIds", entityId);
- formData.append("files", pdf.output("blob"));
- },
- ),
- );
+ formData.append("vendorIds", vendorId);
+ formData.append("entityIds", entityId);
+ formData.append("files", pdf.output("blob"));
+ },
+ ),
+ );
- await emailPurchaseOrder(formData);
- } finally {
- cleanupFromPrint();
- setPending(false);
- }
- }, [emailPurchaseOrder]);
+ await emailPurchaseOrder(formData);
+ } finally {
+ cleanupFromPrint();
+ setPending(false);
+ }
+ },
+ [emailPurchaseOrder],
+ );
return {
printViewContentRef,
diff --git a/src/pages/Settings/EntityConfiguration/EntityConfiguationPanel.tsx b/src/pages/Settings/EntityConfiguration/EntityConfiguationPanel.tsx
index 7f034f359..ec944f423 100644
--- a/src/pages/Settings/EntityConfiguration/EntityConfiguationPanel.tsx
+++ b/src/pages/Settings/EntityConfiguration/EntityConfiguationPanel.tsx
@@ -53,7 +53,6 @@ type EntityConfigKeyWOFeeReport = Exclude<
| EntityConfigKey.CUSTOMER_FACING_DISPLAY_LOGO
| EntityConfigKey.TERMINAL_SCREEN_LOGO
| EntityConfigKey.ENABLE_SALE_HOTKEYS
- | EntityConfigKey.PURCHASE_ORDER_REPLY_TO_EMAIL
>;
const entityConfigMap: Record<
@@ -77,6 +76,11 @@ const entityConfigMap: Record<
label: "One Click Receipt Printing",
defaultValue: false,
},
+ [EntityConfigKey.PURCHASE_ORDER_REPLY_TO_EMAIL]: {
+ type: z.string().email("Please enter a valid email").optional(),
+ label: "Purchase Order Reply-To Email",
+ defaultValue: "",
+ },
[EntityConfigKey.LABEL_PRICE_AT_TOP]: {
type: z.boolean().optional(),
label: "Label Price at Top",
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment