Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const CHILD_ROUTE_ORDER_COLUMNS = [
{ key: "method", label: "Method" },
{ key: "payment", label: "Payment" },
{ key: "attributes", label: "Attributes" },
{ key: "actions", label: "Actions" },
];

function textOrUndefined(value) {
Expand Down Expand Up @@ -64,9 +65,14 @@ export function isMaterializedChildRouteDetail({ routePlan, routeGroup } = {}) {
}

export function formatChildOrderStatus(value) {
const status = String(value ?? "").trim().toLowerCase();
if (status === "completed") return "Completed";
if (status === "in_progress" || status === "in-progress") return "In progress";
const status = String(value ?? "").trim().replace(/-/g, "_").toUpperCase();
if (status === "READY" || status === "PENDING" || status === "ASSIGNED") return "Ready";
if (status === "EN_ROUTE" || status === "ARRIVED") return "In progress";
if (status === "DELIVERED" || status === "COMPLETED") return "Completed";
if (status === "FAILED") return "Failed";
if (status === "SKIPPED") return "Skipped";
if (status === "CANCELLED") return "Cancelled";
if (status === "IN_PROGRESS") return "In progress";
return "Preparing";
}

Expand Down Expand Up @@ -306,6 +312,63 @@ function getCustomerName(stop) {
) ?? EMPTY_LABEL;
}

function getStopPhone(stop) {
return firstText(
stop?.phone,
stop?.recipientPhone,
stop?.customerPhone,
stop?.shippingAddress?.phone,
stop?.address?.phone,
stop?.order?.phone,
stop?.order?.customer?.phone,
stop?.shopifyOrderSnapshot?.phone,
stop?.shopifyOrderSnapshot?.shippingAddress?.phone,
);
}

function getStopAddressField(stop, field) {
return firstText(
stop?.[field],
stop?.address?.[field],
stop?.shippingAddress?.[field],
stop?.order?.shippingAddress?.[field],
stop?.shopifyOrderSnapshot?.shippingAddress?.[field],
stop?.rawPayload?.shippingAddress?.[field],
);
}

function getStopLatitude(stop) {
return numberOrUndefined(
stop?.latitude ??
stop?.coordinates?.latitude ??
stop?.address?.latitude ??
stop?.shippingAddress?.latitude ??
(Array.isArray(stop?.coordinates) ? stop.coordinates[1] : undefined)
);
}

function getStopLongitude(stop) {
return numberOrUndefined(
stop?.longitude ??
stop?.coordinates?.longitude ??
stop?.address?.longitude ??
stop?.shippingAddress?.longitude ??
(Array.isArray(stop?.coordinates) ? stop.coordinates[0] : undefined)
);
}

function getStopInstructions(stop) {
return firstText(
stop?.instructions,
stop?.deliveryInstructions,
stop?.driverInstructions,
stop?.note,
stop?.order?.note,
stop?.shopifyOrderSnapshot?.note,
stop?.rawPayload?.note,
);
}

function normalizeAttributes(attributes) {
if (typeof attributes === "string") {
const value = attributes.trim();
Expand Down Expand Up @@ -373,6 +436,10 @@ export function buildChildRouteOrderRows(stops, { ianaTimezone, timezoneAbbrevia

return {
id: firstText(stop?.id, stop?.deliveryStopId, stop?.shopifyOrderGid, stop?.orderId) ?? `child-order-${index + 1}`,
orderId: firstText(stop?.orderId, stop?.sourceOrderId),
deliveryStopId: firstText(stop?.deliveryStopId),
shopifyOrderGid: firstText(stop?.shopifyOrderGid),
shopifyOrderLegacyId: firstText(stop?.shopifyOrderLegacyId, stop?.legacyResourceId, stop?.shopifyOrderSnapshot?.legacyResourceId),
stop: index + 1,
order: firstText(stop?.order, stop?.orderName, stop?.sourceOrderId, stop?.shopifyOrderGid) ?? EMPTY_LABEL,
status: formatChildOrderStatus(getOrderStatusSource(stop)),
Expand All @@ -390,6 +457,22 @@ export function buildChildRouteOrderRows(stops, { ianaTimezone, timezoneAbbrevia
attributes,
attributesSummary: formatAttributesSummary(attributes),
attributesDetail: attributes.length > 0 ? attributes.map((attribute) => attribute.label).join("\n") : EMPTY_LABEL,
editFields: {
recipientName: firstText(stop?.recipientName, stop?.recipient, stop?.customerName),
phone: getStopPhone(stop),
address1: getStopAddressField(stop, "address1"),
address2: getStopAddressField(stop, "address2"),
city: getStopAddressField(stop, "city"),
province: getStopAddressField(stop, "province"),
postalCode: getStopAddressField(stop, "postalCode"),
countryCode: getStopAddressField(stop, "countryCode"),
latitude: getStopLatitude(stop),
longitude: getStopLongitude(stop),
timeWindowStart: firstText(stop?.timeWindowStart),
timeWindowEnd: firstText(stop?.timeWindowEnd),
serviceMinutes: numberOrUndefined(stop?.serviceMinutes),
instructions: getStopInstructions(stop),
},
};
});
}
60 changes: 60 additions & 0 deletions apps/shopify-app/app/features/delivery/route-detail.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
deleteDeliveryRoutePlan,
fetchDeliveryRoutePlanDetail,
refreshDeliveryRoutePlanOrderData,
transitionDeliveryRoutePlanStop,
updateDeliveryRoutePlanStop,
} from "./route-plans.server";
import {
firstArray,
Expand Down Expand Up @@ -44,6 +46,7 @@ import {
} from "./route-order-refresh";

const ROUTE_REFRESH_CONCURRENCY = 5;
const ROUTE_STOP_TRANSITION_STATUSES = new Set(["READY", "IN_PROGRESS", "COMPLETED"]);

function roundPerfDuration(duration) {
return Number(duration.toFixed(2));
Expand Down Expand Up @@ -293,6 +296,30 @@ function getRedirectSearch(request, deletedKeys = []) {
return `${search ? `?${search}` : ""}${url.hash}`;
}

function readRouteStopTransitionStatus(value) {
const status = textOrUndefined(value)?.toUpperCase();
return status && ROUTE_STOP_TRANSITION_STATUSES.has(status) ? status : null;
}

function readRouteStopOverridePayload(formData) {
return {
recipientName: textOrUndefined(formData.get("recipientName")) ?? null,
phone: textOrUndefined(formData.get("phone")) ?? null,
address1: textOrUndefined(formData.get("address1")) ?? null,
address2: textOrUndefined(formData.get("address2")) ?? null,
city: textOrUndefined(formData.get("city")) ?? null,
province: textOrUndefined(formData.get("province")) ?? null,
postalCode: textOrUndefined(formData.get("postalCode")) ?? null,
countryCode: textOrUndefined(formData.get("countryCode")) ?? null,
latitude: numberOrUndefined(formData.get("latitude")) ?? null,
longitude: numberOrUndefined(formData.get("longitude")) ?? null,
timeWindowStart: textOrUndefined(formData.get("timeWindowStart")) ?? null,
timeWindowEnd: textOrUndefined(formData.get("timeWindowEnd")) ?? null,
serviceMinutes: numberOrUndefined(formData.get("serviceMinutes")) ?? null,
instructions: textOrUndefined(formData.get("instructions")) ?? null,
};
}

export const routeDetailLoader = async ({ params, request }) => {
const routeId = cleanRoutePathParam(params.routeId);
const routeGroupIdHint = getRouteGroupIdHint(request);
Expand Down Expand Up @@ -510,6 +537,39 @@ export const routeDetailAction = async ({ params, request }) => {
});
}

if (intent === "transitionRouteStop") {
const deliveryStopId = textOrUndefined(formData.get("deliveryStopId"));
const status = readRouteStopTransitionStatus(formData.get("status"));
if (!status) {
return {
routePlan: null,
stop: null,
errors: [{ message: "지원하지 않는 stop 상태입니다." }],
};
}
return transitionDeliveryRoutePlanStop(
request,
routeId,
deliveryStopId,
{
status,
idempotencyKey: textOrUndefined(formData.get("idempotencyKey")),
},
{ sessionToken: shopifySessionToken },
);
}

if (intent === "updateRouteStop") {
const deliveryStopId = textOrUndefined(formData.get("deliveryStopId"));
return updateDeliveryRoutePlanStop(
request,
routeId,
deliveryStopId,
readRouteStopOverridePayload(formData),
{ sessionToken: shopifySessionToken },
);
}

if (intent === "previewRouteOptimization") {
const draft = readRouteDraftPayload(formData.get("draft"));
const result = await previewDeliveryRouteGroupOptimization(
Expand Down
102 changes: 102 additions & 0 deletions apps/shopify-app/app/features/delivery/route-plans.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const DELIVERY_API_ENDPOINT_NOT_FOUND_ERROR_CODE =
export const DELIVERY_API_DRIVER_ENDPOINT_NOT_FOUND_ERROR_CODE =
"DELIVERY_API_DRIVER_ENDPOINT_NOT_FOUND";
export const DELIVERY_ROUTE_PLAN_ID_MISSING_ERROR_CODE = "DELIVERY_ROUTE_PLAN_ID_MISSING";
export const DELIVERY_ROUTE_STOP_ID_MISSING_ERROR_CODE = "DELIVERY_ROUTE_STOP_ID_MISSING";
export { buildRouteScopeFromOrders } from "./route-scope.js";

const deliveryApiGetCache = new Map();
Expand Down Expand Up @@ -246,6 +247,107 @@ export async function updateDeliveryRoutePlanStops(request, routePlanId, payload
};
}

export async function transitionDeliveryRoutePlanStop(request, routePlanId, deliveryStopId, payload, options = {}) {
const normalizedRoutePlanId = textOrNull(routePlanId);
const normalizedDeliveryStopId = textOrNull(deliveryStopId);

if (!normalizedRoutePlanId) {
return {
routePlan: null,
stop: null,
errors: [
{
code: DELIVERY_ROUTE_PLAN_ID_MISSING_ERROR_CODE,
message: "수정할 route plan ID가 없어 route stop 상태를 저장하지 못했습니다.",
},
],
};
}
if (!normalizedDeliveryStopId) {
return {
routePlan: null,
stop: null,
errors: [
{
code: DELIVERY_ROUTE_STOP_ID_MISSING_ERROR_CODE,
message: "수정할 delivery stop ID가 없어 route stop 상태를 저장하지 못했습니다.",
},
],
};
}

const safeRoutePlanId = encodeURIComponent(normalizedRoutePlanId);
const safeDeliveryStopId = encodeURIComponent(normalizedDeliveryStopId);
const result = await deliveryApiRequest(
request,
`/admin/route-plans/${safeRoutePlanId}/stops/${safeDeliveryStopId}/transition`,
{
body: JSON.stringify({
status: textOrNull(payload?.status),
idempotencyKey: textOrNull(payload?.idempotencyKey),
}),
fetch: options.fetch,
method: "POST",
sessionToken: options.sessionToken,
},
);

return {
routePlan: result.data?.routePlan ?? null,
stop: result.data?.stop ?? result.data?.deliveryStop ?? null,
errors: result.errors,
};
}

export async function updateDeliveryRoutePlanStop(request, routePlanId, deliveryStopId, payload, options = {}) {
const normalizedRoutePlanId = textOrNull(routePlanId);
const normalizedDeliveryStopId = textOrNull(deliveryStopId);

if (!normalizedRoutePlanId) {
return {
routePlan: null,
stop: null,
errors: [
{
code: DELIVERY_ROUTE_PLAN_ID_MISSING_ERROR_CODE,
message: "수정할 route plan ID가 없어 route stop을 저장하지 못했습니다.",
},
],
};
}
if (!normalizedDeliveryStopId) {
return {
routePlan: null,
stop: null,
errors: [
{
code: DELIVERY_ROUTE_STOP_ID_MISSING_ERROR_CODE,
message: "수정할 delivery stop ID가 없어 route stop을 저장하지 못했습니다.",
},
],
};
}

const safeRoutePlanId = encodeURIComponent(normalizedRoutePlanId);
const safeDeliveryStopId = encodeURIComponent(normalizedDeliveryStopId);
const result = await deliveryApiRequest(
request,
`/admin/route-plans/${safeRoutePlanId}/stops/${safeDeliveryStopId}/override`,
{
body: JSON.stringify(payload ?? {}),
fetch: options.fetch,
method: "PATCH",
sessionToken: options.sessionToken,
},
);

return {
routePlan: result.data?.routePlan ?? null,
stop: result.data?.stop ?? result.data?.deliveryStop ?? null,
errors: result.errors,
};
}

export async function updateDeliveryRoutePlanDepartureTime(request, routePlanId, payload, options = {}) {
const normalizedRoutePlanId = textOrNull(routePlanId);

Expand Down
Loading