diff --git a/src/api/doormile/endpoints.js b/src/api/doormile/endpoints.js
index 325752b..f969284 100644
--- a/src/api/doormile/endpoints.js
+++ b/src/api/doormile/endpoints.js
@@ -375,6 +375,12 @@ export const blockMiler = async (id, data) => {
return response.data;
};
+/** Lift a block: the rider can sign in again and is Offline until they start duty. */
+export const unblockMiler = async (id) => {
+ const response = await doormileAxios.put(`/admin/milers/${id}/unblock`, {});
+ return response.data;
+};
+
export const assignMilerVehicle = async (id, data) => {
const response = await doormileAxios.put(`/admin/milers/${id}/assign-vehicle`, data);
return response.data;
diff --git a/src/lib/doormileHooks.js b/src/lib/doormileHooks.js
index 1c85349..bbfc585 100644
--- a/src/lib/doormileHooks.js
+++ b/src/lib/doormileHooks.js
@@ -417,7 +417,14 @@ export const useBlockMiler = () =>
useDoormileMutation({
mutationFn: ({ id, data }) => api.blockMiler(id, data),
invalidates: [KEYS.milers],
- successMessage: 'Rider updated',
+ successMessage: 'Rider blocked',
+ });
+
+export const useUnblockMiler = () =>
+ useDoormileMutation({
+ mutationFn: (id) => api.unblockMiler(id),
+ invalidates: [KEYS.milers],
+ successMessage: 'Rider unblocked',
});
export const useAssignMilerVehicle = () =>
diff --git a/src/lib/orderPricing.js b/src/lib/orderPricing.js
new file mode 100644
index 0000000..82a83fb
--- /dev/null
+++ b/src/lib/orderPricing.js
@@ -0,0 +1,92 @@
+/**
+ * The delivery charge shown and sent (as `finalprice`) when ops create orders —
+ * the single order form and bulk upload. The backend stores the figure as
+ * given, so this IS the price the client is billed.
+ *
+ * What it replaces, in both pages:
+ * - A rate card value of 0 was overwritten by a made-up default
+ * (`Number(x) || 40`, `|| 10`, `|| 2`): a card set to "₹15 per km, no base
+ * fee, no free km" was charged ₹40 plus ₹15 for each km after the first 2.
+ * - A client without a rate card was priced off ANOTHER client's card
+ * (`list[0]`), or a hard-coded ₹40 + ₹10/km after 2 km.
+ * - The first card found for the client was used even if Inactive, and the
+ * Pricing page's "priority" was ignored.
+ */
+
+/**
+ * Doormile's standard rate, for a client with no rate card: ₹15 per km, at
+ * least 1 km billed. Without the minimum a trip under 1 km (or a distance the
+ * router rounded to 0) priced at ₹0, and the backend treats a 0 `finalprice` as
+ * "not given" and re-prices the booking from another client's card.
+ */
+export const STANDARD_RATE = Object.freeze({
+ baseprice: 0,
+ basedistance: 0,
+ priceperkm: 15,
+ minimumkm: 1,
+ isStandard: true,
+});
+
+/** A rate value as a number; an empty or invalid value is 0, never a default. */
+const amount = (value) => {
+ const n = Number(value);
+ return Number.isFinite(n) && n >= 0 ? n : 0;
+};
+
+const isActive = (rule) => !rule?.status || String(rule.status).toLowerCase() === 'active';
+
+/** Inside the card's effectivefrom / effectiveto window (either may be unset). */
+const inEffect = (rule, now = Date.now()) => {
+ const from = rule?.effectivefrom ? Date.parse(rule.effectivefrom) : NaN;
+ const to = rule?.effectiveto ? Date.parse(rule.effectiveto) : NaN;
+ return (Number.isNaN(from) || from <= now) && (Number.isNaN(to) || to >= now);
+};
+
+/** The Pricing page's priority: lower wins; unset is 1, and 0 stays 0. */
+const priorityOf = (rule) => {
+ const n = Number(rule?.priority);
+ return rule?.priority == null || rule.priority === '' || !Number.isFinite(n) ? 1 : n;
+};
+
+/**
+ * The rate card for a client: its Active rules, the lowest priority number
+ * first (the Pricing page's "lower wins"), newest on a tie. A Doormile-wide
+ * card (no client) applies next; failing both, the standard rate. Never
+ * another client's card.
+ */
+export function pickRateCard(pricingList, tenantId, now = Date.now()) {
+ const rules = (Array.isArray(pricingList) ? pricingList : []).filter((r) => isActive(r) && inEffect(r, now));
+ const byPriority = (a, b) => priorityOf(a) - priorityOf(b) || amount(b.pricingid) - amount(a.pricingid);
+
+ if (tenantId != null && String(tenantId) !== '' && String(tenantId) !== '0') {
+ const own = rules.filter((r) => String(r.tenantid) === String(tenantId)).sort(byPriority);
+ if (own.length) return own[0];
+ }
+ const doormileWide = rules.filter((r) => r.isdefault || !r.tenantid || String(r.tenantid) === '0').sort(byPriority);
+ return doormileWide[0] || STANDARD_RATE;
+}
+
+/**
+ * The charge for a trip of `distanceKm` on `rate`: the base price covers the
+ * first `basedistance` km, every km after that is `priceperkm`. Rounded to
+ * paise.
+ */
+export function chargeForDistance(distanceKm, rate = STANDARD_RATE) {
+ const km = Math.max(amount(distanceKm), amount(rate?.minimumkm));
+ const base = amount(rate?.baseprice);
+ const freeKm = amount(rate?.basedistance);
+ const perKm = amount(rate?.priceperkm);
+ const charge = km <= freeKm ? base : base + (km - freeKm) * perKm;
+ return Math.round(charge * 100) / 100;
+}
+
+/** "₹15 / km" style labels for the rate card panel. */
+export function describeRate(rate) {
+ const base = amount(rate?.baseprice);
+ const freeKm = amount(rate?.basedistance);
+ return {
+ base: base > 0 ? `₹${base}${freeKm > 0 ? ` (first ${freeKm} km)` : ''}` : 'No base fee',
+ perKm: `₹${amount(rate?.priceperkm)} / km${amount(rate?.minimumkm) > 0 ? ` (min ${amount(rate.minimumkm)} km)` : ''}`,
+ source: rate?.isStandard ? 'Standard rate (no client rate card)' : 'Client rate card',
+ };
+}
diff --git a/src/pages/doormile/orders/CreateOrder.jsx b/src/pages/doormile/orders/CreateOrder.jsx
index 3554ac1..88a3b08 100644
--- a/src/pages/doormile/orders/CreateOrder.jsx
+++ b/src/pages/doormile/orders/CreateOrder.jsx
@@ -46,10 +46,10 @@ import {
// point 300 m from the hub read "0 km". Both legs now display `meters`.
import {
calculateDrivingRoute,
- calculateTotalCharge,
formatRouteDistance,
formatRouteDuration
} from '@/lib/distance';
+import { chargeForDistance, describeRate, pickRateCard } from '@/lib/orderPricing';
import { buildCityScope, geocodeAddress, saveRecentAddress } from '@/lib/geocodingService';
import { useHubs } from '@/lib/doormileHooks';
import {
@@ -514,17 +514,10 @@ export default function CreateOrder() {
[pickupMode, pickCust.latitude, pickCust.longitude, dropCust.latitude, dropCust.longitude, flowAnchors]
);
- // Tenant Pricing
- const clientRate = useMemo(() => {
- const list = Array.isArray(pricingList) ? pricingList : [];
- if (selectedTenantId) {
- const match = list.find((p) => String(p.tenantid) === String(selectedTenantId));
- if (match) return match;
- }
- // Fallback to default pricing rule or standard system rates
- const defaultRule = list.find((p) => p.isdefault || !p.tenantid || p.tenantid === 0);
- return defaultRule || list[0] || { baseprice: 40, priceperkm: 10, basedistance: 2 };
- }, [pricingList, selectedTenantId]);
+ // The client's own active rate card, or the standard ₹15 / km — never another
+ // client's card (see lib/orderPricing).
+ const clientRate = useMemo(() => pickRateCard(pricingList, selectedTenantId), [pricingList, selectedTenantId]);
+ const rateLabels = useMemo(() => describeRate(clientRate), [clientRate]);
// Route calculation with full polyline for mini-map preview
useEffect(() => {
@@ -608,19 +601,12 @@ export default function CreateOrder() {
};
}, [isCustomerPickup, selectedLocation?.latitude, selectedLocation?.longitude, pickCust.latitude, pickCust.longitude]);
- // Total Charge calculation
- const totalCharge = useMemo(() => {
- const base = Number(clientRate?.baseprice) || 40;
- const perKm = Number(clientRate?.priceperkm) || 10;
- const minKm = Number(clientRate?.basedistance) || 2;
-
- if (!route.resolved || !route.distance) {
- return base;
- }
-
- const calculated = calculateTotalCharge(route.distance, base, perKm, minKm);
- return Math.max(base, calculated);
- }, [route, clientRate]);
+ // Total charge: the base price covers the rate card's first km, then the
+ // per-km rate. A 0 on the card stays 0 (it used to become ₹40 base + 2 free km).
+ const totalCharge = useMemo(
+ () => chargeForDistance(route.resolved ? route.distance : 0, clientRate),
+ [route, clientRate]
+ );
// The selected hub location's city. Address suggestions come only from it:
// a Coimbatore location searches Coimbatore, a Bengaluru or Hyderabad one
@@ -801,6 +787,12 @@ export default function CreateOrder() {
OpenToast('Please choose the client this order is for, at the top of the form.', 'warning', 4000);
return;
}
+ // The charge is calculated from the route; submitting before it lands sent
+ // ₹0, which the backend re-prices from another client's rate card.
+ if (!route.resolved) {
+ OpenToast('Still working out the route and price — try again in a moment.', 'warning', 4000);
+ return;
+ }
if (!pickCust.firstname.trim()) {
OpenToast('Pickup contact name is missing. Enter the name of the person handing over the parcel.', 'warning', 4000);
return;
@@ -1784,17 +1776,17 @@ export default function CreateOrder() {
{/* Rate Card Info */}