mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 05:18:11 +00:00
fix
This commit is contained in:
@@ -16,7 +16,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import type { TrainComposition } from "@/services/trainBuilder.service";
|
import type { TrainComposition } from "@/services/trainBuilder.service";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { EXPORT_TRAIN_OPTIONS, importRunFor } from "@/constants/trainRuns";
|
import { IMPORT_TRAIN_OPTIONS, exportRunFor } from "@/constants/trainRuns";
|
||||||
|
|
||||||
const parseError = (error: unknown, fallback: string) => {
|
const parseError = (error: unknown, fallback: string) => {
|
||||||
if (isAxiosError(error)) {
|
if (isAxiosError(error)) {
|
||||||
@@ -56,11 +56,11 @@ export default function BuildTrainModal({ opened, onClose, onBuilt }: BuildTrain
|
|||||||
setLocomotiveIds([]);
|
setLocomotiveIds([]);
|
||||||
}, [yardId]);
|
}, [yardId]);
|
||||||
|
|
||||||
// The import run is fixed by the export run, so it tracks it rather than
|
// The export run is fixed by the import run, so it tracks it rather than
|
||||||
// being entered by hand (and clears back to empty when the export is cleared).
|
// being entered by hand (and clears back to empty when the import is cleared).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setImportTrainNumber(importRunFor(exportTrainNumber));
|
setExportTrainNumber(exportRunFor(importTrainNumber));
|
||||||
}, [exportTrainNumber]);
|
}, [importTrainNumber]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!opened) {
|
if (!opened) {
|
||||||
@@ -85,7 +85,7 @@ export default function BuildTrainModal({ opened, onClose, onBuilt }: BuildTrain
|
|||||||
// only "nothing picked" is reachable here.
|
// only "nothing picked" is reachable here.
|
||||||
if (!exportTrainNumber || !importTrainNumber) {
|
if (!exportTrainNumber || !importTrainNumber) {
|
||||||
toast({
|
toast({
|
||||||
title: "Pick an export train number (e.g. 8001) — the import run follows it",
|
title: "Pick an import train number (e.g. 8002) — the export run follows it",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -138,24 +138,24 @@ export default function BuildTrainModal({ opened, onClose, onBuilt }: BuildTrain
|
|||||||
maxLength={100}
|
maxLength={100}
|
||||||
/>
|
/>
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<Select
|
{/* Fixed by the import run — derived, never typed. */}
|
||||||
|
<TextInput
|
||||||
label="Export train number"
|
label="Export train number"
|
||||||
description="Odd — Ethiopia → Djibouti runs"
|
description="Odd — Ethiopia → Djibouti runs"
|
||||||
placeholder="e.g. 8001"
|
placeholder="e.g. 8001"
|
||||||
data={EXPORT_TRAIN_OPTIONS}
|
value={exportTrainNumber}
|
||||||
value={exportTrainNumber || null}
|
readOnly
|
||||||
onChange={(value) => setExportTrainNumber(value ?? "")}
|
variant="filled"
|
||||||
searchable
|
|
||||||
clearable
|
|
||||||
/>
|
/>
|
||||||
{/* Fixed by the export run — derived, never typed. */}
|
<Select
|
||||||
<TextInput
|
|
||||||
label="Import train number"
|
label="Import train number"
|
||||||
description="Even — Djibouti → Ethiopia runs"
|
description="Even — Djibouti → Ethiopia runs"
|
||||||
placeholder="e.g. 8002"
|
placeholder="e.g. 8002"
|
||||||
value={importTrainNumber}
|
data={IMPORT_TRAIN_OPTIONS}
|
||||||
readOnly
|
value={importTrainNumber || null}
|
||||||
variant="filled"
|
onChange={(value) => setImportTrainNumber(value ?? "")}
|
||||||
|
searchable
|
||||||
|
clearable
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -24,12 +24,27 @@ export const TRAIN_RUN_PAIRS: Record<string, string> = {
|
|||||||
"9001": "9002",
|
"9001": "9002",
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Selectable export runs, in run order. */
|
/** Even IMPORT run -> its odd EXPORT run. Derived so the two cannot drift. */
|
||||||
|
const EXPORT_BY_IMPORT: Record<string, string> = Object.fromEntries(
|
||||||
|
Object.entries(TRAIN_RUN_PAIRS).map(([exportRun, importRun]) => [importRun, exportRun]),
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Selectable export runs (odd), in run order. */
|
||||||
export const EXPORT_TRAIN_OPTIONS = Object.keys(TRAIN_RUN_PAIRS).map((run) => ({
|
export const EXPORT_TRAIN_OPTIONS = Object.keys(TRAIN_RUN_PAIRS).map((run) => ({
|
||||||
label: run,
|
label: run,
|
||||||
value: run,
|
value: run,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
/** Selectable import runs (even), in run order. */
|
||||||
|
export const IMPORT_TRAIN_OPTIONS = Object.values(TRAIN_RUN_PAIRS).map((run) => ({
|
||||||
|
label: run,
|
||||||
|
value: run,
|
||||||
|
}));
|
||||||
|
|
||||||
/** The import run implied by an export run; empty string when unset/unknown. */
|
/** The import run implied by an export run; empty string when unset/unknown. */
|
||||||
export const importRunFor = (exportRun: unknown): string =>
|
export const importRunFor = (exportRun: unknown): string =>
|
||||||
TRAIN_RUN_PAIRS[String(exportRun ?? "")] ?? "";
|
TRAIN_RUN_PAIRS[String(exportRun ?? "")] ?? "";
|
||||||
|
|
||||||
|
/** The export run implied by an import run; empty string when unset/unknown. */
|
||||||
|
export const exportRunFor = (importRun: unknown): string =>
|
||||||
|
EXPORT_BY_IMPORT[String(importRun ?? "")] ?? "";
|
||||||
|
|||||||
@@ -2669,9 +2669,6 @@
|
|||||||
"uploadNew": "Upload New",
|
"uploadNew": "Upload New",
|
||||||
"updateExisting": "Update Existing",
|
"updateExisting": "Update Existing",
|
||||||
"updateComingSoon": "Update existing functionality coming soon",
|
"updateComingSoon": "Update existing functionality coming soon",
|
||||||
"uploadNew": "Upload New",
|
|
||||||
"updateExisting": "Update Existing",
|
|
||||||
"updateComingSoon": "Update existing functionality coming soon",
|
|
||||||
"count": "Count",
|
"count": "Count",
|
||||||
"posMissing": "Missing 'Positions' or 'Users' sheet.",
|
"posMissing": "Missing 'Positions' or 'Users' sheet.",
|
||||||
"invalidFile": "The uploaded file is empty or invalid.",
|
"invalidFile": "The uploaded file is empty or invalid.",
|
||||||
@@ -2697,7 +2694,6 @@
|
|||||||
"archiveDepartment": "Archive Department",
|
"archiveDepartment": "Archive Department",
|
||||||
"deleteDepartment": "Delete Department",
|
"deleteDepartment": "Delete Department",
|
||||||
"deleteConfirm": "Delete Department?",
|
"deleteConfirm": "Delete Department?",
|
||||||
"delete": "Delete",
|
|
||||||
"deleteFailed": "Failed to delete department",
|
"deleteFailed": "Failed to delete department",
|
||||||
"cannotDeleteWithEmployees": "Cannot delete department with assigned employees",
|
"cannotDeleteWithEmployees": "Cannot delete department with assigned employees",
|
||||||
"reassignEmployeesFirst": "Please reassign or remove all employees from this department first.",
|
"reassignEmployeesFirst": "Please reassign or remove all employees from this department first.",
|
||||||
@@ -3087,7 +3083,10 @@
|
|||||||
"failedToResend": "Failed To Resend Verification Code",
|
"failedToResend": "Failed To Resend Verification Code",
|
||||||
"failedToSendInvitation": "Failed To Send Invitation",
|
"failedToSendInvitation": "Failed To Send Invitation",
|
||||||
"confirmRemoveEmployee": "Are you sure you want to remove {{name}}? This action cannot be undone.",
|
"confirmRemoveEmployee": "Are you sure you want to remove {{name}}? This action cannot be undone.",
|
||||||
"removeFunctionalityNotImplemented": "Remove functionality will be implemented with proper API integration"
|
"removeFunctionalityNotImplemented": "Remove functionality will be implemented with proper API integration",
|
||||||
|
"sendingPasswordReset": "Sending password reset link...",
|
||||||
|
"passwordResetSent": "Password Reset Link Sent",
|
||||||
|
"passwordResetSentTo": "{{name}} can now set a new password. Any earlier code no longer works."
|
||||||
},
|
},
|
||||||
"Resend Invite": "Resend Invite",
|
"Resend Invite": "Resend Invite",
|
||||||
"deputy": {
|
"deputy": {
|
||||||
@@ -3305,7 +3304,6 @@
|
|||||||
"selectStyleCategory": "Select Style Category",
|
"selectStyleCategory": "Select Style Category",
|
||||||
"customized": "Customized",
|
"customized": "Customized",
|
||||||
"textAlign": "Text Align",
|
"textAlign": "Text Align",
|
||||||
"pdfPreview": "PDF Preview",
|
|
||||||
"autoRefreshEnabled": "Auto refresh is enabled",
|
"autoRefreshEnabled": "Auto refresh is enabled",
|
||||||
"manualRefreshEnabled": "Manual refresh",
|
"manualRefreshEnabled": "Manual refresh",
|
||||||
"openInNewTab": "Open in new tab",
|
"openInNewTab": "Open in new tab",
|
||||||
@@ -7053,11 +7051,9 @@
|
|||||||
"attach": "Attach",
|
"attach": "Attach",
|
||||||
"selectAttachment": "Select Attachment from DMS",
|
"selectAttachment": "Select Attachment from DMS",
|
||||||
"search": "Search…",
|
"search": "Search…",
|
||||||
"loading": "Loading…",
|
|
||||||
"loadingFiles": "Fetching DMS files...",
|
|
||||||
"searchFiles": "Search folders and files...",
|
|
||||||
"loading": "Loading files...",
|
"loading": "Loading files...",
|
||||||
"loadingFiles": "Fetching DMS files...",
|
"loadingFiles": "Fetching DMS files...",
|
||||||
|
"searchFiles": "Search folders and files...",
|
||||||
"retry": "Retry",
|
"retry": "Retry",
|
||||||
"noSearchResults": "No files match your search",
|
"noSearchResults": "No files match your search",
|
||||||
"emptyFolder": "Nothing to show here",
|
"emptyFolder": "Nothing to show here",
|
||||||
|
|||||||
@@ -39,6 +39,18 @@ interface DraftField extends CreateFileUploadFieldDto {
|
|||||||
let draftCounter = 0;
|
let draftCounter = 0;
|
||||||
const nextKey = () => `draft-${Date.now()}-${++draftCounter}`;
|
const nextKey = () => `draft-${Date.now()}-${++draftCounter}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extensions offered as checkboxes. Mirrors DOC_EXTENSIONS in the API's
|
||||||
|
* file-upload-settings seeder — the only formats the document flows accept.
|
||||||
|
*
|
||||||
|
* The API validates `allowedExtensions` as plain strings, so free text let
|
||||||
|
* typos ("pd") through silently and the field then rejected every real upload.
|
||||||
|
* A fixed list makes that unrepresentable.
|
||||||
|
*/
|
||||||
|
const FILE_EXTENSION_OPTIONS = ["pdf", "jpg", "jpeg", "png"] as const;
|
||||||
|
|
||||||
|
const KNOWN_EXTENSIONS = new Set<string>(FILE_EXTENSION_OPTIONS);
|
||||||
|
|
||||||
function makeEmptyDraft(idx: number): DraftField {
|
function makeEmptyDraft(idx: number): DraftField {
|
||||||
return {
|
return {
|
||||||
key: nextKey(),
|
key: nextKey(),
|
||||||
@@ -93,13 +105,19 @@ export default function ManageFileUploadFieldsDialog({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateExtensions = (i: number, raw: string) => {
|
const toggleExtension = (i: number, ext: string, checked: boolean) =>
|
||||||
const list = raw
|
setFields((prev) =>
|
||||||
.split(",")
|
prev.map((f, idx) => {
|
||||||
.map((s) => s.trim().toLowerCase().replace(/^\./, ""))
|
if (idx !== i) return f;
|
||||||
.filter(Boolean);
|
const current = f.allowedExtensions;
|
||||||
update(i, { allowedExtensions: list });
|
if (checked) {
|
||||||
};
|
return current.includes(ext)
|
||||||
|
? f
|
||||||
|
: { ...f, allowedExtensions: [...current, ext] };
|
||||||
|
}
|
||||||
|
return { ...f, allowedExtensions: current.filter((e) => e !== ext) };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const remove = (i: number) =>
|
const remove = (i: number) =>
|
||||||
setFields((prev) => prev.filter((_, idx) => idx !== i));
|
setFields((prev) => prev.filter((_, idx) => idx !== i));
|
||||||
@@ -214,7 +232,9 @@ export default function ManageFileUploadFieldsDialog({
|
|||||||
index={i}
|
index={i}
|
||||||
total={fields.length}
|
total={fields.length}
|
||||||
onChange={(patch) => update(i, patch)}
|
onChange={(patch) => update(i, patch)}
|
||||||
onChangeExtensions={(raw) => updateExtensions(i, raw)}
|
onToggleExtension={(ext, checked) =>
|
||||||
|
toggleExtension(i, ext, checked)
|
||||||
|
}
|
||||||
onMove={(dir) => move(i, dir)}
|
onMove={(dir) => move(i, dir)}
|
||||||
onRemove={() => remove(i)}
|
onRemove={() => remove(i)}
|
||||||
/>
|
/>
|
||||||
@@ -258,7 +278,7 @@ function FieldEditor({
|
|||||||
index,
|
index,
|
||||||
total,
|
total,
|
||||||
onChange,
|
onChange,
|
||||||
onChangeExtensions,
|
onToggleExtension,
|
||||||
onMove,
|
onMove,
|
||||||
onRemove,
|
onRemove,
|
||||||
}: {
|
}: {
|
||||||
@@ -266,13 +286,23 @@ function FieldEditor({
|
|||||||
index: number;
|
index: number;
|
||||||
total: number;
|
total: number;
|
||||||
onChange: (patch: Partial<DraftField>) => void;
|
onChange: (patch: Partial<DraftField>) => void;
|
||||||
onChangeExtensions: (raw: string) => void;
|
onToggleExtension: (ext: string, checked: boolean) => void;
|
||||||
onMove: (dir: -1 | 1) => void;
|
onMove: (dir: -1 | 1) => void;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
}) {
|
}) {
|
||||||
const minFiles = getMinFiles(field);
|
const minFiles = getMinFiles(field);
|
||||||
const effectiveMax = field.isMultiple ? field.maxFiles : 1;
|
const effectiveMax = field.isMultiple ? field.maxFiles : 1;
|
||||||
|
|
||||||
|
// A field saved before this list existed can hold anything the old free-text
|
||||||
|
// box accepted (e.g. the typo "pd"). Show those alongside the standard ones so
|
||||||
|
// they stay visible and removable instead of silently vanishing on save.
|
||||||
|
const extensionChoices = [
|
||||||
|
...FILE_EXTENSION_OPTIONS,
|
||||||
|
...field.allowedExtensions.filter(
|
||||||
|
(ext) => !KNOWN_EXTENSIONS.has(ext),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-2xl border border-slate-200 bg-white p-4">
|
<div className="rounded-2xl border border-slate-200 bg-white p-4">
|
||||||
<div className="mb-3 flex items-center justify-between">
|
<div className="mb-3 flex items-center justify-between">
|
||||||
@@ -348,16 +378,33 @@ function FieldEditor({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5 md:col-span-2">
|
<div className="space-y-1.5 md:col-span-2">
|
||||||
<Label className="text-xs">Allowed Extensions</Label>
|
<Label className="text-xs">Allowed Extensions *</Label>
|
||||||
<Input
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-2">
|
||||||
value={field.allowedExtensions.join(", ")}
|
{extensionChoices.map((ext) => (
|
||||||
onChange={(e) => onChangeExtensions(e.target.value)}
|
<label
|
||||||
placeholder="pdf, docx, jpg"
|
key={ext}
|
||||||
className="font-mono"
|
className="flex items-center gap-2 text-sm text-slate-700"
|
||||||
/>
|
>
|
||||||
<p className="text-xs text-slate-500">
|
<input
|
||||||
Comma-separated, no leading dot.
|
type="checkbox"
|
||||||
</p>
|
checked={field.allowedExtensions.includes(ext)}
|
||||||
|
onChange={(e) => onToggleExtension(ext, e.target.checked)}
|
||||||
|
className="h-4 w-4 rounded border-slate-300 text-[#10B981] focus:ring-[#10B981]/20"
|
||||||
|
/>
|
||||||
|
<span className="font-mono">{ext}</span>
|
||||||
|
{!KNOWN_EXTENSIONS.has(ext) ? (
|
||||||
|
<span className="text-xs text-amber-600">(unrecognised)</span>
|
||||||
|
) : null}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{field.allowedExtensions.length === 0 ? (
|
||||||
|
<p className="text-xs text-red-500">Pick at least one extension.</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-slate-500">
|
||||||
|
Uploads are rejected unless the file matches one of these.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Freight } from "@edr/types";
|
import { Freight } from "@edr/types";
|
||||||
import type { ColumnFormat, FormFieldDef } from "@/pages/ruleEngine/config/resources";
|
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 { vehiclesConfig, VEHICLE_TYPE_OPTIONS, FUEL_TYPE_OPTIONS, VEHICLE_STATUS_OPTIONS } from "./vehicles";
|
||||||
import { driversConfig, DRIVER_STATUS_OPTIONS } from "./drivers";
|
import { driversConfig, DRIVER_STATUS_OPTIONS } from "./drivers";
|
||||||
|
|
||||||
@@ -299,25 +299,25 @@ export const FLEET_RESOURCES: FleetResourceConfig[] = [
|
|||||||
],
|
],
|
||||||
formFields: [
|
formFields: [
|
||||||
// Run numbers are optional — a wagon sits in the fleet unassigned to any
|
// 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.
|
// that choice, so it is derived rather than typed.
|
||||||
{
|
{
|
||||||
name: "exportTrainNumber",
|
name: "exportTrainNumber",
|
||||||
label: "Export train number",
|
label: "Export train number",
|
||||||
type: "select",
|
type: "text",
|
||||||
description: "Odd — Ethiopia → Djibouti runs",
|
description: "Odd — Ethiopia → Djibouti runs",
|
||||||
placeholder: "e.g. 8001",
|
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,
|
clearable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "importTrainNumber",
|
name: "importTrainNumber",
|
||||||
label: "Import train number",
|
label: "Import train number",
|
||||||
type: "text",
|
type: "select",
|
||||||
description: "Even — Djibouti → Ethiopia runs",
|
description: "Even — Djibouti → Ethiopia runs",
|
||||||
placeholder: "e.g. 8002",
|
placeholder: "e.g. 8002",
|
||||||
derivedValue: (values) => importRunFor(values.exportTrainNumber),
|
options: IMPORT_TRAIN_OPTIONS,
|
||||||
// Follows the export run to NULL when that is cleared.
|
|
||||||
clearable: true,
|
clearable: true,
|
||||||
},
|
},
|
||||||
{ name: "wagonNumber", label: "Wagon number", type: "text", required: true },
|
{ name: "wagonNumber", label: "Wagon number", type: "text", required: true },
|
||||||
|
|||||||
@@ -281,9 +281,9 @@ export const TeamMembers = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ActionDropdown = ({ employee }: { employee: TeamMemberDto }) => {
|
const ActionDropdown = ({ employee }: { employee: TeamMemberDto }) => {
|
||||||
const status = getUserStatus(employee);
|
// getUserStatus only ever yields "accepted" | "pending" — a member with no
|
||||||
const isPending = status === "pending";
|
// password set is "pending", so anything else has one already.
|
||||||
const isNotInvited = status === "Not Invited";
|
const isPending = getUserStatus(employee) === "pending";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@@ -328,8 +328,11 @@ export const TeamMembers = ({
|
|||||||
{t("positions.viewPositions")}
|
{t("positions.viewPositions")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
{/* Resend/Invite Option */}
|
{/* Resend/Invite Option. Offered for accepted members too: the code
|
||||||
{onInviteEmployee && (isPending || isNotInvited) && (
|
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 && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -337,7 +340,7 @@ export const TeamMembers = ({
|
|||||||
}}
|
}}
|
||||||
className="cursor-pointer dark:text-gray-200 dark:hover:bg-gray-600">
|
className="cursor-pointer dark:text-gray-200 dark:hover:bg-gray-600">
|
||||||
<RefreshCw className="h-4 w-4 mr-2" />
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
{isPending ? t("Resend Invite") : t("Send Invite")}
|
{isPending ? t("Resend Invite") : t("Send Password Reset")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -213,12 +213,15 @@ const UserManagementTree = () => {
|
|||||||
// Handle inviting or resending invitation to employees
|
// Handle inviting or resending invitation to employees
|
||||||
const handleInviteEmployee = async (employee: TeamMemberDto) => {
|
const handleInviteEmployee = async (employee: TeamMemberDto) => {
|
||||||
try {
|
try {
|
||||||
// Show immediate feedback that action is being processed
|
// A member who already set a password is being sent a fresh set-password
|
||||||
if (employee.status === "pending") {
|
// code — that is a reset, not an invite, so say so. Keyed off
|
||||||
toast.loading("Resending verification code...");
|
// hasSetPassword (the same fact the menu label uses) rather than the
|
||||||
} else {
|
// looser `status` field, so the wording cannot disagree with the menu.
|
||||||
toast.loading("Sending invitation...");
|
const isReset = employee.user.hasSetPassword;
|
||||||
}
|
|
||||||
|
toast.loading(
|
||||||
|
isReset ? "Sending password reset link..." : "Resending verification code...",
|
||||||
|
);
|
||||||
await resendVerificationCode({
|
await resendVerificationCode({
|
||||||
email: employee.user.email,
|
email: employee.user.email,
|
||||||
phoneNumber: employee.user.phoneNumber,
|
phoneNumber: employee.user.phoneNumber,
|
||||||
@@ -229,14 +232,14 @@ const UserManagementTree = () => {
|
|||||||
? localizedName(employee.user.name)
|
? localizedName(employee.user.name)
|
||||||
: localizedName(employee.user.name) || "User";
|
: localizedName(employee.user.name) || "User";
|
||||||
|
|
||||||
if (employee.user.status === "pending") {
|
if (isReset) {
|
||||||
toast.success("Verification Code Resent", {
|
toast.success("Password Reset Link Sent", {
|
||||||
description: `A new verification code has been sent to ${userName}`,
|
description: `${userName} can now set a new password. Any earlier code they had no longer works.`,
|
||||||
duration: 4000,
|
duration: 4000,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
toast.success("Invitation Sent", {
|
toast.success("Verification Code Resent", {
|
||||||
description: `Invitation sent to ${userName}`,
|
description: `A new verification code has been sent to ${userName}`,
|
||||||
duration: 4000,
|
duration: 4000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,12 +81,17 @@ export const OrganizationEmployeeSearch: React.FC<
|
|||||||
const handleInviteEmployee = async (employee: TeamMemberDto) => {
|
const handleInviteEmployee = async (employee: TeamMemberDto) => {
|
||||||
const lang = i18n.language;
|
const lang = i18n.language;
|
||||||
try {
|
try {
|
||||||
// Show immediate feedback that action is being processed
|
// A member who already set a password is being sent a fresh set-password
|
||||||
if (employee.user.status === "pending") {
|
// code — that is a reset, not an invite. Keyed off hasSetPassword (the
|
||||||
toast.loading(t("search.resendingVerification"));
|
// same fact the menu label uses) rather than the looser `status` field,
|
||||||
} else {
|
// so the wording cannot disagree with the menu the operator clicked.
|
||||||
toast.loading(t("search.sendingInvitation"));
|
const isReset = employee.user.hasSetPassword;
|
||||||
}
|
|
||||||
|
toast.loading(
|
||||||
|
isReset
|
||||||
|
? t("search.sendingPasswordReset")
|
||||||
|
: t("search.resendingVerification"),
|
||||||
|
);
|
||||||
|
|
||||||
await resendVerificationCode({
|
await resendVerificationCode({
|
||||||
email: employee.user.email,
|
email: employee.user.email,
|
||||||
@@ -97,14 +102,14 @@ export const OrganizationEmployeeSearch: React.FC<
|
|||||||
const userName =
|
const userName =
|
||||||
lang === "en" ? employee.user.name.en : employee.user.name.am;
|
lang === "en" ? employee.user.name.en : employee.user.name.am;
|
||||||
|
|
||||||
if (employee.user.status === "pending") {
|
if (isReset) {
|
||||||
toast.success(t("search.verificationCodeResent"), {
|
toast.success(t("search.passwordResetSent"), {
|
||||||
description: t("search.verificationCodeSentTo", { name: userName }),
|
description: t("search.passwordResetSentTo", { name: userName }),
|
||||||
duration: 4000,
|
duration: 4000,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
toast.success(t("search.invitationSent"), {
|
toast.success(t("search.verificationCodeResent"), {
|
||||||
description: t("search.invitationSentTo", { name: userName }),
|
description: t("search.verificationCodeSentTo", { name: userName }),
|
||||||
duration: 4000,
|
duration: 4000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user