diff --git a/.example.env b/.example.env
index a42ab852082..2b23c68d46a 100644
--- a/.example.env
+++ b/.example.env
@@ -198,5 +198,9 @@ REACT_DECIMAL_ROUNDING_METHOD=ROUND_HALF_UP
# Maximum number of forms that can be favorited in the forms dialog (default: 5)
REACT_MAX_FORM_DIALOG_FAVORITES=5
+# Maximum number of datapoints allowed in a single upsert request (default: 100)
+# Must not exceed the backend limit
+REACT_MAX_DATAPOINTS_PER_UPSERT=100
+
# Default tab for medication selector. Valid values: product, valueset
REACT_MEDICATION_VALUE_SET_SELECT_DEFAULT_TAB=product
diff --git a/care.config.ts b/care.config.ts
index 58e1fd2a927..f5c211578c7 100644
--- a/care.config.ts
+++ b/care.config.ts
@@ -421,6 +421,14 @@ const careConfig = {
maxFormDialogFavorites: env.REACT_MAX_FORM_DIALOG_FAVORITES
? parseInt(env.REACT_MAX_FORM_DIALOG_FAVORITES, 10)
: 5,
+
+ /**
+ * Maximum number of datapoints allowed in a single upsert request.
+ * This should be set with whatever backend sets.
+ */
+ maxDatapointsPerUpsert: env.REACT_MAX_DATAPOINTS_PER_UPSERT
+ ? parseInt(env.REACT_MAX_DATAPOINTS_PER_UPSERT, 10)
+ : 100,
} as const;
export default careConfig;
diff --git a/public/locale/en.json b/public/locale/en.json
index 55ccda61d9e..6430e17ff12 100644
--- a/public/locale/en.json
+++ b/public/locale/en.json
@@ -3107,6 +3107,7 @@
"issuer_type": "Issuer Type",
"item": "Item",
"item_condition": "Item Condition?",
+ "item_limit_reached": "Item limit reached",
"item_location": "Item Location",
"item_marked_as_abandoned": "Item marked as abandoned successfully",
"item_marked_as_entered_in_error": "Item marked as entered in error successfully",
@@ -3227,7 +3228,7 @@
"live": "Live",
"live_monitoring": "Live Monitoring",
"live_patients_total_beds": "Live Patients / Total beds",
- "load_from_order": "Load from order",
+ "load_from_order_with_items": "Load from order ({{count}} items)",
"load_more": "Load More",
"loading": "Loading...",
"loading_appointment_details": "Loading appointment details...",
@@ -3457,6 +3458,7 @@
"max": "Max",
"max_applicable_discounts": "Maximum Applicable Discounts",
"max_applicable_discounts_description": "The maximum number of discount components that can be applied to a single invoice. Set to 0 for no discount.",
+ "max_datapoints_per_upsert_limit": "You cannot add more than {{count}} items in a single delivery.",
"max_dosage_24_hrs": "Max. dosage in 24 hrs.",
"max_dosage_in_24hrs_gte_base_dosage_error": "Max. dosage in 24 hours must be greater than or equal to base dosage",
"max_favorites_reached": "You've reached the limit. Only {{count}} forms can be favourited.",
diff --git a/scripts/validate-env.ts b/scripts/validate-env.ts
index f0ad002ce00..8b0cf06b231 100644
--- a/scripts/validate-env.ts
+++ b/scripts/validate-env.ts
@@ -178,6 +178,7 @@ const envSchema = z
message: `Must be one of: ${VALID_ROUNDING_METHODS.join(", ")}`,
})
.optional(),
+ REACT_MAX_DATAPOINTS_PER_UPSERT: numberAsString.optional(),
REACT_MAX_FORM_DIALOG_FAVORITES: positiveNumberAsString.optional(),
})
.superRefine(async (data, ctx) => {
diff --git a/src/CAREUI/display/Callout.tsx b/src/CAREUI/display/Callout.tsx
index c14a471be7d..2fc727cd04b 100644
--- a/src/CAREUI/display/Callout.tsx
+++ b/src/CAREUI/display/Callout.tsx
@@ -1,11 +1,11 @@
-import React from "react";
+import React, { ReactNode } from "react";
import { cn } from "@/lib/utils";
interface CalloutProps {
variant?: "primary" | "secondary" | "warning" | "alert" | "danger";
className?: string;
- badge: string;
+ badge: ReactNode;
children: React.ReactNode;
}
@@ -27,20 +27,7 @@ export default function Callout({
props.className,
)}
>
-
- {props.badge}
-
+ {props.badge}
{props.children}
diff --git a/src/pages/Facility/services/inventory/externalSupply/deliveryOrder/AddSupplyDeliveryForm.tsx b/src/pages/Facility/services/inventory/externalSupply/deliveryOrder/AddSupplyDeliveryForm.tsx
index 9ae9ececd46..34824173509 100644
--- a/src/pages/Facility/services/inventory/externalSupply/deliveryOrder/AddSupplyDeliveryForm.tsx
+++ b/src/pages/Facility/services/inventory/externalSupply/deliveryOrder/AddSupplyDeliveryForm.tsx
@@ -1,6 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { PlusCircle, Trash2 } from "lucide-react";
+import { PlusCircle, Trash2, TriangleAlert } from "lucide-react";
import { useQueryParams } from "raviger";
import { useCallback, useMemo, useState } from "react";
import { useFieldArray, useForm } from "react-hook-form";
@@ -43,6 +43,7 @@ import {
TableRow,
} from "@/components/ui/table";
+import Callout from "@/CAREUI/display/Callout";
import {
getExtensionFieldsWithName,
processExtensions,
@@ -129,6 +130,8 @@ interface Props {
origin?: string;
destination: string;
onSuccess: () => void;
+ supplyDeliveriesCount: number;
+ isFetchingSupplyDeliveries: boolean;
}
export function AddSupplyDeliveryForm({
@@ -137,6 +140,8 @@ export function AddSupplyDeliveryForm({
origin,
destination,
onSuccess,
+ supplyDeliveriesCount,
+ isFetchingSupplyDeliveries,
}: Props) {
const { t } = useTranslation();
const queryClient = useQueryClient();
@@ -237,6 +242,12 @@ export function AddSupplyDeliveryForm({
name: "items",
});
+ const hasReachedUpsertLimit =
+ supplyDeliveriesCount >= careConfig.maxDatapointsPerUpsert;
+
+ const disableAddItem =
+ isProcessing || hasReachedUpsertLimit || isFetchingSupplyDeliveries;
+
const loadFromSupplyRequests = () => {
setIsSelectDialogOpen(true);
handleSelectAll(true);
@@ -269,6 +280,7 @@ export function AddSupplyDeliveryForm({
noOptionsMessage={t("no_orders_found")}
className="px-10"
popoverContentClassName="w-auto"
+ disabled={disableAddItem}
/>
);
@@ -897,29 +909,47 @@ export function AddSupplyDeliveryForm({
-
-
=
+ careConfig.maxDatapointsPerUpsert ? (
+
+ }
>
-
- {t("add_another")}
-
- {supplyRequests?.results?.length &&
- supplyRequests?.results?.length > 0 && (
+
+ {t("max_datapoints_per_upsert_limit", {
+ count: careConfig.maxDatapointsPerUpsert,
+ })}
+
+
+ ) : (
+
+
+
+ {t("add_another")}
+
+ {!!supplyRequests?.results?.length && (
- {t("load_from_order")} ({supplyRequests?.count}{" "}
- {t("items")}
- )
+ {t("load_from_order_with_items", {
+ count: supplyRequests.count,
+ })}
)}
-
+
+ )}
{t("add_items_to_delivery_description")}
{qParams.supplyOrder ? (
- supplyRequests?.results?.length &&
- supplyRequests?.results?.length > 0 && (
+ !!supplyRequests?.results?.length && (
<>
- {t("load_from_order")} ({supplyRequests?.count}{" "}
- {t("items")}
- )
+ {t("load_from_order_with_items", {
+ count: supplyRequests.count,
+ })}
- {t("or")} -
@@ -971,6 +1001,7 @@ export function AddSupplyDeliveryForm({
type="button"
variant="outline_primary"
onClick={() => handleAddAnotherItem()}
+ disabled={disableAddItem}
>
{t("add_item")}
@@ -1007,7 +1038,7 @@ export function AddSupplyDeliveryForm({
{t("select_all")}
-
+
{supplyRequests.results.map((request) => (
delivery.supply_request && delivery.supply_request.id,
@@ -465,12 +471,27 @@ export function DeliveryOrderShow({
(delivery) => delivery.status === SupplyDeliveryStatus.completed,
) ?? false;
+ const allSupplyDeliveriesCompletedOrAbandoned =
+ !!supplyDeliveries?.results?.length &&
+ supplyDeliveries.results.every(
+ (delivery) =>
+ delivery.status === SupplyDeliveryStatus.completed ||
+ delivery.status === SupplyDeliveryStatus.abandoned,
+ );
+
const deliveryOrderStatusActions = getDeliveryOrderStatusActions(
deliveryOrder.status,
internal,
anyCompletedSupplyDeliveries,
);
+ const hasReachedUpsertLimit =
+ supplyDeliveries &&
+ supplyDeliveries.results.length >= careConfig.maxDatapointsPerUpsert;
+
+ const isWithinUpsertLimit =
+ selectedDeliveries.length <= careConfig.maxDatapointsPerUpsert;
+
return (
{isUpdating ? t("updating") : t("mark_as_completed")}
@@ -799,7 +821,8 @@ export function DeliveryOrderShow({
disabled={
isUpdating ||
isUpsertingDeliveries ||
- selectedDeliveries.length === 0
+ selectedDeliveries.length === 0 ||
+ !isWithinUpsertLimit
}
>
{isUpsertingDeliveries
@@ -819,7 +842,8 @@ export function DeliveryOrderShow({
disabled={
isUpdating ||
isUpsertingDeliveries ||
- selectedDeliveries.length === 0
+ selectedDeliveries.length === 0 ||
+ !isWithinUpsertLimit
}
>
{t("mark_as_abandoned")}
@@ -829,7 +853,8 @@ export function DeliveryOrderShow({
disabled={
isUpdating ||
isUpsertingDeliveries ||
- selectedDeliveries.length === 0
+ selectedDeliveries.length === 0 ||
+ !isWithinUpsertLimit
}
>
{t("mark_as_damaged")}
@@ -840,6 +865,23 @@ export function DeliveryOrderShow({
)}
+ {!isWithinUpsertLimit && (
+
+
+ }
+ >
+
+ {t("max_datapoints_per_upsert_limit", {
+ count: careConfig.maxDatapointsPerUpsert,
+ })}
+
+
+
+ )}
{isLoadingSupplyDeliveries ? (
@@ -911,14 +953,34 @@ export function DeliveryOrderShow({
<>>
)}
+ {hasReachedUpsertLimit && canAddSupplyDeliveries && (
+
+ }
+ >
+
+ {t("max_datapoints_per_upsert_limit", {
+ count: careConfig.maxDatapointsPerUpsert,
+ })}
+
+
+ )}
+
{/* Add New Supply Delivery Form - Always show when in draft mode */}
- {canAddSupplyDeliveries && (
+ {canAddSupplyDeliveries && !hasReachedUpsertLimit && (
)}
diff --git a/src/pages/Facility/services/inventory/externalSupply/requestOrder/AddItemsForm.tsx b/src/pages/Facility/services/inventory/externalSupply/requestOrder/AddItemsForm.tsx
index 74f96e4e765..0418ec13bed 100644
--- a/src/pages/Facility/services/inventory/externalSupply/requestOrder/AddItemsForm.tsx
+++ b/src/pages/Facility/services/inventory/externalSupply/requestOrder/AddItemsForm.tsx
@@ -5,6 +5,10 @@ import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import * as z from "zod";
+import careConfig from "@careConfig";
+
+import Callout from "@/CAREUI/display/Callout";
+
import {
Table,
TableBody,
@@ -27,6 +31,7 @@ import { ProductKnowledgeSelect } from "@/pages/Facility/services/inventory/Prod
import { EmptyState } from "@/components/ui/empty-state";
import { Separator } from "@/components/ui/separator";
+import { cn } from "@/lib/utils";
import { ProductKnowledgeBase } from "@/types/inventory/productKnowledge/productKnowledge";
import { RequestOrderStatus } from "@/types/inventory/requestOrder/requestOrder";
import { SupplyRequestStatus } from "@/types/inventory/supplyRequest/supplyRequest";
@@ -34,7 +39,7 @@ import supplyRequestApi from "@/types/inventory/supplyRequest/supplyRequestApi";
import { zodDecimal } from "@/Utils/decimal";
import { ShortcutBadge } from "@/Utils/keyboardShortcutComponents";
import mutate from "@/Utils/request/mutate";
-import { Box, Check, Trash2 } from "lucide-react";
+import { Box, Check, Trash2, TriangleAlert } from "lucide-react";
const supplyRequestFormSchema = z.object({
requests: z.array(
@@ -56,6 +61,8 @@ interface AddItemsFormProps {
updateOrderStatus: (status: RequestOrderStatus) => void;
disableApproveButton: boolean;
showEmptyState: boolean;
+
+ supplyRequestsCount: number;
}
export function AddItemsForm({
@@ -64,6 +71,7 @@ export function AddItemsForm({
updateOrderStatus,
disableApproveButton,
showEmptyState,
+ supplyRequestsCount,
}: AddItemsFormProps) {
const { t } = useTranslation();
@@ -79,6 +87,9 @@ export function AddItemsForm({
name: "requests",
});
+ const hasReachedUpsertLimit =
+ supplyRequestsCount + fields.length >= careConfig.maxDatapointsPerUpsert;
+
const { mutate: createSupplyRequests, isPending: isCreating } = useMutation({
mutationFn: async (
requests: Array<{ item: { id: string; name: string }; quantity: string }>,
@@ -208,12 +219,28 @@ export function AddItemsForm({
)}
-
+ {hasReachedUpsertLimit ? (
+
+ }
+ >
+
+ {t("max_datapoints_per_upsert_limit", {
+ count: careConfig.maxDatapointsPerUpsert,
+ })}
+
+
+ ) : (
+
+ )}
{fields.length > 0 ? (
<>
@@ -239,7 +266,9 @@ export function AddItemsForm({
>
) : (
-
-{t("or")}-
+
+ -{t("or")}-
+
diff --git a/src/pages/Facility/services/inventory/externalSupply/requestOrder/RequestOrderShow.tsx b/src/pages/Facility/services/inventory/externalSupply/requestOrder/RequestOrderShow.tsx
index c53145de7dd..8cf326fea3c 100644
--- a/src/pages/Facility/services/inventory/externalSupply/requestOrder/RequestOrderShow.tsx
+++ b/src/pages/Facility/services/inventory/externalSupply/requestOrder/RequestOrderShow.tsx
@@ -755,6 +755,9 @@ export function RequestOrderShow({
showEmptyState={
supplyRequests?.results.length === 0
}
+ supplyRequestsCount={
+ supplyRequests?.results.length || 0
+ }
/>
)}
diff --git a/src/pages/Facility/services/pharmacy/components/AddMedicationReturnItemForm.tsx b/src/pages/Facility/services/pharmacy/components/AddMedicationReturnItemForm.tsx
index dd953eb287f..c7b9c60794b 100644
--- a/src/pages/Facility/services/pharmacy/components/AddMedicationReturnItemForm.tsx
+++ b/src/pages/Facility/services/pharmacy/components/AddMedicationReturnItemForm.tsx
@@ -467,8 +467,9 @@ export function AddMedicationReturnItemForm({
variant="secondary"
onClick={loadFromMedicationDispenses}
>
- {t("load_from_order")} ({medicationDispenses.length}{" "}
- {t("items")})
+ {t("load_from_order_with_items", {
+ count: medicationDispenses.length,
+ })}
)}
@@ -506,8 +507,9 @@ export function AddMedicationReturnItemForm({
variant="outline_primary"
onClick={loadFromMedicationDispenses}
>
- {t("load_from_order")} ({medicationDispenses.length}{" "}
- {t("items")})
+ {t("load_from_order_with_items", {
+ count: medicationDispenses.length,
+ })}
- {t("or")} -
diff --git a/src/pages/Scheduling/components/CreateScheduleTemplateSheet.tsx b/src/pages/Scheduling/components/CreateScheduleTemplateSheet.tsx
index 3f89a0dea17..906d31c3285 100644
--- a/src/pages/Scheduling/components/CreateScheduleTemplateSheet.tsx
+++ b/src/pages/Scheduling/components/CreateScheduleTemplateSheet.tsx
@@ -54,6 +54,7 @@ import {
ScheduleAvailabilityCreateRequest,
} from "@/types/scheduling/schedule";
import scheduleApis from "@/types/scheduling/scheduleApi";
+import { Info } from "lucide-react";
interface Props {
facilityId: string;
@@ -275,7 +276,10 @@ export default function CreateScheduleTemplateSheet({
if (!slotsPerSession || !tokenDuration) return null;
return (
-
+ }
+ >
{template.is_public && !field.value && (
-
+
+ }
+ >
{t("template_visibility_change_warning")}
@@ -636,7 +641,10 @@ const NewAvailabilityCard = ({
if (!slotsPerSession || !tokenDuration) return null;
return (
-
+ }
+ >
{
await page
.getByRole("row", { name: "Requested Qty." })
.getByRole("checkbox")
- .click();
- await page.getByRole("button", { name: "Mark as Completed" }).click();
+ .check();
+ const receiveButton = page.getByRole("button", {
+ name: "Receive & Update Stock",
+ });
+ await expect(receiveButton).toBeEnabled();
+ await receiveButton.click();
+ await page.getByRole("button", { name: "Confirm" }).click();
+ const markAsCompletedButton = page.getByRole("button", {
+ name: "Mark as Completed",
+ });
+ await expect(markAsCompletedButton).toBeEnabled();
+ await markAsCompletedButton.click();
await page.goto(bioChembasePath + "/inventory/internal/receive");
await page.getByRole("tab", { name: "Incoming Deliveries" }).click();