@@ -348,16 +378,33 @@ function FieldEditor({
-
Allowed Extensions
-
onChangeExtensions(e.target.value)}
- placeholder="pdf, docx, jpg"
- className="font-mono"
- />
-
- Comma-separated, no leading dot.
-
+
Allowed Extensions *
+
+ {extensionChoices.map((ext) => (
+
+ onToggleExtension(ext, e.target.checked)}
+ className="h-4 w-4 rounded border-slate-300 text-[#10B981] focus:ring-[#10B981]/20"
+ />
+ {ext}
+ {!KNOWN_EXTENSIONS.has(ext) ? (
+ (unrecognised)
+ ) : null}
+
+ ))}
+
+ {field.allowedExtensions.length === 0 ? (
+
Pick at least one extension.
+ ) : (
+
+ Uploads are rejected unless the file matches one of these.
+
+ )}
diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/config/resources.ts b/apps/edr-freight-web/backoffice/src/pages/fleet/config/resources.ts
index 793537dd9..3981cb47d 100644
--- a/apps/edr-freight-web/backoffice/src/pages/fleet/config/resources.ts
+++ b/apps/edr-freight-web/backoffice/src/pages/fleet/config/resources.ts
@@ -1,6 +1,6 @@
import { Freight } from "@edr/types";
import type { ColumnFormat, FormFieldDef } from "@/pages/ruleEngine/config/resources";
-import { EXPORT_TRAIN_OPTIONS, importRunFor } from "@/constants/trainRuns";
+import { IMPORT_TRAIN_OPTIONS, exportRunFor } from "@/constants/trainRuns";
import { vehiclesConfig, VEHICLE_TYPE_OPTIONS, FUEL_TYPE_OPTIONS, VEHICLE_STATUS_OPTIONS } from "./vehicles";
import { driversConfig, DRIVER_STATUS_OPTIONS } from "./drivers";
@@ -299,25 +299,25 @@ export const FLEET_RESOURCES: FleetResourceConfig[] = [
],
formFields: [
// Run numbers are optional — a wagon sits in the fleet unassigned to any
- // run until an operator picks an export run. The import run is fixed by
+ // run until an operator picks an import run. The export run is fixed by
// that choice, so it is derived rather than typed.
{
name: "exportTrainNumber",
label: "Export train number",
- type: "select",
+ type: "text",
description: "Odd — Ethiopia → Djibouti runs",
placeholder: "e.g. 8001",
- options: EXPORT_TRAIN_OPTIONS,
+ derivedValue: (values) => exportRunFor(values.importTrainNumber),
+ // Follows the import run to NULL when that is cleared.
clearable: true,
},
{
name: "importTrainNumber",
label: "Import train number",
- type: "text",
+ type: "select",
description: "Even — Djibouti → Ethiopia runs",
placeholder: "e.g. 8002",
- derivedValue: (values) => importRunFor(values.exportTrainNumber),
- // Follows the export run to NULL when that is cleared.
+ options: IMPORT_TRAIN_OPTIONS,
clearable: true,
},
{ name: "wagonNumber", label: "Wagon number", type: "text", required: true },
diff --git a/apps/edr-freight-web/backoffice/src/user-management/userManagement/TeamMembers.tsx b/apps/edr-freight-web/backoffice/src/user-management/userManagement/TeamMembers.tsx
index 7ee195d22..3cca16fe1 100644
--- a/apps/edr-freight-web/backoffice/src/user-management/userManagement/TeamMembers.tsx
+++ b/apps/edr-freight-web/backoffice/src/user-management/userManagement/TeamMembers.tsx
@@ -281,9 +281,9 @@ export const TeamMembers = ({
};
const ActionDropdown = ({ employee }: { employee: TeamMemberDto }) => {
- const status = getUserStatus(employee);
- const isPending = status === "pending";
- const isNotInvited = status === "Not Invited";
+ // getUserStatus only ever yields "accepted" | "pending" — a member with no
+ // password set is "pending", so anything else has one already.
+ const isPending = getUserStatus(employee) === "pending";
return (
@@ -328,8 +328,11 @@ export const TeamMembers = ({
{t("positions.viewPositions")}
- {/* Resend/Invite Option */}
- {onInviteEmployee && (isPending || isNotInvited) && (
+ {/* Resend/Invite Option. Offered for accepted members too: the code
+ this sends lets them set a new password, so for someone who
+ already has one it is a reset — labelled as such rather than as an
+ invite, which would misdescribe what the operator is doing. */}
+ {onInviteEmployee && (
{
e.stopPropagation();
@@ -337,7 +340,7 @@ export const TeamMembers = ({
}}
className="cursor-pointer dark:text-gray-200 dark:hover:bg-gray-600">
- {isPending ? t("Resend Invite") : t("Send Invite")}
+ {isPending ? t("Resend Invite") : t("Send Password Reset")}
)}
diff --git a/apps/edr-freight-web/backoffice/src/user-management/userManagement/UserManagementTree.tsx b/apps/edr-freight-web/backoffice/src/user-management/userManagement/UserManagementTree.tsx
index 70ee2903f..10cf8651a 100644
--- a/apps/edr-freight-web/backoffice/src/user-management/userManagement/UserManagementTree.tsx
+++ b/apps/edr-freight-web/backoffice/src/user-management/userManagement/UserManagementTree.tsx
@@ -213,12 +213,15 @@ const UserManagementTree = () => {
// Handle inviting or resending invitation to employees
const handleInviteEmployee = async (employee: TeamMemberDto) => {
try {
- // Show immediate feedback that action is being processed
- if (employee.status === "pending") {
- toast.loading("Resending verification code...");
- } else {
- toast.loading("Sending invitation...");
- }
+ // A member who already set a password is being sent a fresh set-password
+ // code — that is a reset, not an invite, so say so. Keyed off
+ // hasSetPassword (the same fact the menu label uses) rather than the
+ // looser `status` field, so the wording cannot disagree with the menu.
+ const isReset = employee.user.hasSetPassword;
+
+ toast.loading(
+ isReset ? "Sending password reset link..." : "Resending verification code...",
+ );
await resendVerificationCode({
email: employee.user.email,
phoneNumber: employee.user.phoneNumber,
@@ -229,14 +232,14 @@ const UserManagementTree = () => {
? localizedName(employee.user.name)
: localizedName(employee.user.name) || "User";
- if (employee.user.status === "pending") {
- toast.success("Verification Code Resent", {
- description: `A new verification code has been sent to ${userName}`,
+ if (isReset) {
+ toast.success("Password Reset Link Sent", {
+ description: `${userName} can now set a new password. Any earlier code they had no longer works.`,
duration: 4000,
});
} else {
- toast.success("Invitation Sent", {
- description: `Invitation sent to ${userName}`,
+ toast.success("Verification Code Resent", {
+ description: `A new verification code has been sent to ${userName}`,
duration: 4000,
});
}
diff --git a/apps/edr-freight-web/backoffice/src/user-management/userManagement/components/OrganizationEmployeeSearch.tsx b/apps/edr-freight-web/backoffice/src/user-management/userManagement/components/OrganizationEmployeeSearch.tsx
index 09018ef44..119e9b288 100644
--- a/apps/edr-freight-web/backoffice/src/user-management/userManagement/components/OrganizationEmployeeSearch.tsx
+++ b/apps/edr-freight-web/backoffice/src/user-management/userManagement/components/OrganizationEmployeeSearch.tsx
@@ -81,12 +81,17 @@ export const OrganizationEmployeeSearch: React.FC<
const handleInviteEmployee = async (employee: TeamMemberDto) => {
const lang = i18n.language;
try {
- // Show immediate feedback that action is being processed
- if (employee.user.status === "pending") {
- toast.loading(t("search.resendingVerification"));
- } else {
- toast.loading(t("search.sendingInvitation"));
- }
+ // A member who already set a password is being sent a fresh set-password
+ // code — that is a reset, not an invite. Keyed off hasSetPassword (the
+ // same fact the menu label uses) rather than the looser `status` field,
+ // so the wording cannot disagree with the menu the operator clicked.
+ const isReset = employee.user.hasSetPassword;
+
+ toast.loading(
+ isReset
+ ? t("search.sendingPasswordReset")
+ : t("search.resendingVerification"),
+ );
await resendVerificationCode({
email: employee.user.email,
@@ -97,14 +102,14 @@ export const OrganizationEmployeeSearch: React.FC<
const userName =
lang === "en" ? employee.user.name.en : employee.user.name.am;
- if (employee.user.status === "pending") {
- toast.success(t("search.verificationCodeResent"), {
- description: t("search.verificationCodeSentTo", { name: userName }),
+ if (isReset) {
+ toast.success(t("search.passwordResetSent"), {
+ description: t("search.passwordResetSentTo", { name: userName }),
duration: 4000,
});
} else {
- toast.success(t("search.invitationSent"), {
- description: t("search.invitationSentTo", { name: userName }),
+ toast.success(t("search.verificationCodeResent"), {
+ description: t("search.verificationCodeSentTo", { name: userName }),
duration: 4000,
});
}