diff --git a/src/components/DataRetentionSelector.test.tsx b/src/components/DataRetentionSelector.test.tsx
new file mode 100644
index 00000000..c1a087f6
--- /dev/null
+++ b/src/components/DataRetentionSelector.test.tsx
@@ -0,0 +1,88 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import DataRetentionSelector, {
+ TimeUnit,
+ DataRetentionValue,
+} from "./DataRetentionSelector";
+
+describe("DataRetentionSelector", () => {
+ const defaultValue: DataRetentionValue = {
+ value: 30,
+ unit: TimeUnit.DAYS,
+ };
+
+ test("renders with default value", () => {
+ const onChange = jest.fn();
+ render();
+
+ expect(screen.getByLabelText("Retention Time")).toHaveValue(30);
+ expect(screen.getByLabelText("Unit")).toHaveTextContent("Days");
+ });
+
+ test("calls onChange when value changes", () => {
+ const onChange = jest.fn();
+ render();
+
+ const valueInput = screen.getByLabelText("Retention Time");
+ userEvent.clear(valueInput);
+ userEvent.type(valueInput, "60");
+
+ expect(onChange).toHaveBeenCalledWith({
+ value: 60,
+ unit: TimeUnit.DAYS,
+ });
+ });
+
+ test("calls onChange when unit changes", () => {
+ const onChange = jest.fn();
+ render();
+
+ const unitSelect = screen.getByLabelText("Unit");
+ userEvent.click(unitSelect);
+ userEvent.click(screen.getByText("Weeks"));
+
+ expect(onChange).toHaveBeenCalledWith({
+ value: 30,
+ unit: TimeUnit.WEEKS,
+ });
+ });
+
+ test("displays all time unit options", () => {
+ const onChange = jest.fn();
+ render();
+
+ const unitSelect = screen.getByLabelText("Unit");
+ userEvent.click(unitSelect);
+
+ expect(screen.getAllByText("Days").length).toBeGreaterThan(0);
+ expect(screen.getByText("Weeks")).toBeInTheDocument();
+ expect(screen.getByText("Months")).toBeInTheDocument();
+ expect(screen.getByText("Years")).toBeInTheDocument();
+ });
+
+ test("can be disabled", () => {
+ const onChange = jest.fn();
+ render(
+
+ );
+
+ const valueInput = screen.getByLabelText("Retention Time");
+ const unitSelect = screen.getByLabelText("Unit");
+
+ expect(valueInput).toBeDisabled();
+ expect(unitSelect).toHaveAttribute("aria-disabled", "true");
+ });
+
+ test("minimum allowed value is 1", () => {
+ const onChange = jest.fn();
+ render();
+
+ const valueInput = screen.getByLabelText("Retention Time");
+ expect(valueInput).toHaveAttribute("min", "1");
+ });
+});
diff --git a/src/components/DataRetentionSelector.tsx b/src/components/DataRetentionSelector.tsx
new file mode 100644
index 00000000..faae6908
--- /dev/null
+++ b/src/components/DataRetentionSelector.tsx
@@ -0,0 +1,77 @@
+import React from "react";
+import { Stack, TextField, MenuItem } from "@mui/material";
+
+export enum TimeUnit {
+ DAYS = "days",
+ WEEKS = "weeks",
+ MONTHS = "months",
+ YEARS = "years",
+}
+
+export type DataRetentionValue = {
+ value: number;
+ unit: TimeUnit;
+};
+
+type DataRetentionSelectorProps = {
+ value: DataRetentionValue;
+ onChange: (value: DataRetentionValue) => void;
+ disabled?: boolean;
+};
+
+const DataRetentionSelector: React.FC = ({
+ value,
+ onChange,
+ disabled = false,
+}) => {
+ const handleValueChange = (event: React.ChangeEvent) => {
+ const newValue = parseInt(event.target.value, 10);
+ if (isNaN(newValue) && event.target.value !== "") {
+ return;
+ }
+ onChange({
+ ...value,
+ value: isNaN(newValue) ? 1 : Math.max(1, newValue),
+ });
+ };
+
+ const handleUnitChange = (event: React.ChangeEvent) => {
+ onChange({
+ ...value,
+ unit: event.target.value as TimeUnit,
+ });
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default DataRetentionSelector;
diff --git a/src/scenes/AdminControl/AdminControl.js b/src/scenes/AdminControl/AdminControl.js
index 8ed31555..6a156a75 100644
--- a/src/scenes/AdminControl/AdminControl.js
+++ b/src/scenes/AdminControl/AdminControl.js
@@ -5,6 +5,7 @@ import AdminAddVehicle from "./Components/AdminAddVehicle";
import AdminAddLocation from "./Components/AdminAddLocation";
import AdminAddDeliverableType from "./Components/AdminAddDeliverableType";
import AdminAddRiderResponsibility from "./Components/AdminAddRiderResponsibility";
+import AdminDataRetention from "./Components/AdminDataRetention";
import { Stack } from "@mui/material";
import { RiderResponsibilityChips } from "./Components/RiderResponsibilityChips";
import { DeliverableTypeChips } from "./Components/DeliverableTypeChips";
@@ -19,6 +20,7 @@ export function AdminControl() {
+
);
}
diff --git a/src/scenes/AdminControl/Components/AdminDataRetention.test.tsx b/src/scenes/AdminControl/Components/AdminDataRetention.test.tsx
new file mode 100644
index 00000000..09b11c7c
--- /dev/null
+++ b/src/scenes/AdminControl/Components/AdminDataRetention.test.tsx
@@ -0,0 +1,157 @@
+import React from "react";
+import AdminDataRetention from "./AdminDataRetention";
+import { render } from "../../../test-utils";
+import * as models from "../../../models";
+import { userRoles } from "../../../apiConsts";
+import { screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+
+const preloadedState = {
+ whoami: {
+ user: new models.User({ roles: [userRoles.user, userRoles.admin] }),
+ },
+ loadingReducer: {
+ GET_WHOAMI: false,
+ },
+ tenantId: "tenant-id",
+};
+
+describe("AdminDataRetention", () => {
+ beforeEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ test("renders the component with default values", async () => {
+ render(, { preloadedState });
+
+ expect(
+ screen.getByText("Set data retention time")
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ "Determine how long data should be retained before automatic deletion"
+ )
+ ).toBeInTheDocument();
+ expect(
+ screen.getByLabelText(
+ "Retain data indefinitely (no automatic deletion)"
+ )
+ ).toBeChecked();
+ expect(screen.queryByLabelText("Retention Time")).toBeNull();
+ });
+
+ test("allows toggling indefinite retention", () => {
+ render(, { preloadedState });
+
+ const checkbox = screen.getByLabelText(
+ "Retain data indefinitely (no automatic deletion)"
+ );
+ expect(checkbox).toBeChecked();
+
+ userEvent.click(checkbox);
+
+ expect(checkbox).not.toBeChecked();
+ expect(screen.getByLabelText("Retention Time")).toBeInTheDocument();
+ expect(screen.getByLabelText("Unit")).toBeInTheDocument();
+ });
+
+ test("allows changing retention time value", () => {
+ render(, { preloadedState });
+
+ // Uncheck indefinite retention first
+ const checkbox = screen.getByLabelText(
+ "Retain data indefinitely (no automatic deletion)"
+ );
+ userEvent.click(checkbox);
+
+ const valueInput = screen.getByLabelText("Retention Time");
+ userEvent.clear(valueInput);
+ userEvent.type(valueInput, "60");
+
+ expect(valueInput).toHaveValue(60);
+ });
+
+ test("allows changing time unit", () => {
+ render(, { preloadedState });
+
+ // Uncheck indefinite retention first
+ const checkbox = screen.getByLabelText(
+ "Retain data indefinitely (no automatic deletion)"
+ );
+ userEvent.click(checkbox);
+
+ const unitSelect = screen.getByLabelText("Unit");
+ userEvent.click(unitSelect);
+ userEvent.click(screen.getByText("Weeks"));
+
+ // Check that the unit field now displays "Weeks"
+ expect(unitSelect).toHaveTextContent("Weeks");
+ });
+
+ test("save button is enabled and clickable", () => {
+ render(, { preloadedState });
+
+ const saveButton = screen.getByRole("button", {
+ name: "Save retention settings",
+ });
+ expect(saveButton).not.toBeDisabled();
+
+ userEvent.click(saveButton);
+ // Button should be clickable without errors
+ });
+
+ test("shows warning dialog for short retention periods", async () => {
+ render(, { preloadedState });
+
+ // Uncheck indefinite retention
+ const checkbox = screen.getByLabelText(
+ "Retain data indefinitely (no automatic deletion)"
+ );
+ userEvent.click(checkbox);
+
+ // Set a short retention period (5 days)
+ const valueInput = screen.getByLabelText("Retention Time");
+ userEvent.clear(valueInput);
+ userEvent.type(valueInput, "5");
+
+ // Click save
+ const saveButton = screen.getByRole("button", {
+ name: "Save retention settings",
+ });
+ userEvent.click(saveButton);
+
+ // Warning dialog should appear
+ await waitFor(() => {
+ expect(
+ screen.getByText("Short Retention Period Warning")
+ ).toBeInTheDocument();
+ });
+ });
+
+ test("displays forbidden message if user is not admin", () => {
+ render(, {
+ preloadedState: {
+ ...preloadedState,
+ whoami: { user: new models.User({ roles: [userRoles.user] }) },
+ },
+ });
+
+ expect(screen.queryByText("Set data retention time")).toBeNull();
+ expect(
+ screen.getByText("You don't have permission to view this page.")
+ ).toBeInTheDocument();
+ });
+
+ test("displays skeleton while loading", () => {
+ render(, {
+ preloadedState: {
+ ...preloadedState,
+ loadingReducer: {
+ GET_WHOAMI: true,
+ },
+ },
+ });
+
+ expect(screen.queryByText("Set data retention time")).toBeNull();
+ });
+});
diff --git a/src/scenes/AdminControl/Components/AdminDataRetention.tsx b/src/scenes/AdminControl/Components/AdminDataRetention.tsx
new file mode 100644
index 00000000..a79cefd7
--- /dev/null
+++ b/src/scenes/AdminControl/Components/AdminDataRetention.tsx
@@ -0,0 +1,191 @@
+import {
+ Button,
+ Checkbox,
+ FormControlLabel,
+ Skeleton,
+ Stack,
+ Typography,
+} from "@mui/material";
+import { makeStyles } from "tss-react/mui";
+import React, { useState } from "react";
+import { PaddedPaper } from "../../../styles/common";
+import { useDispatch, useSelector } from "react-redux";
+import { getWhoami } from "../../../redux/Selectors";
+import Forbidden from "../../../ErrorComponents/Forbidden";
+import { createLoadingSelector } from "../../../redux/LoadingSelectors";
+import * as models from "../../../models/index";
+import DataRetentionSelector, {
+ DataRetentionValue,
+ TimeUnit,
+} from "../../../components/DataRetentionSelector";
+import { displayInfoNotification } from "../../../redux/notifications/NotificationsActions";
+import ConfirmationDialog from "../../../components/ConfirmationDialog";
+
+const initialDataRetentionState: DataRetentionValue = {
+ value: 30,
+ unit: TimeUnit.DAYS,
+};
+
+const useStyles = makeStyles()({
+ root: {
+ width: "100%",
+ maxWidth: 460,
+ },
+});
+
+function AdminDataRetention() {
+ const [state, setState] = useState(
+ initialDataRetentionState
+ );
+ const [noRetention, setNoRetention] = useState(true);
+ const [showWarningDialog, setShowWarningDialog] = useState(false);
+ const loadingSelector = createLoadingSelector(["GET_WHOAMI"]);
+ const whoamiFetching = useSelector(loadingSelector);
+ const [isPosting, setIsPosting] = useState(false);
+ const dispatch = useDispatch();
+ const { classes } = useStyles();
+ const whoami = useSelector(getWhoami);
+
+ // Calculate total days for comparison
+ // Note: This uses approximate values for simplicity:
+ // - Months are assumed to be 30 days
+ // - Years are assumed to be 365 days
+ // For precise calculations, backend should use actual calendar dates
+ const getTotalDays = (value: number, unit: TimeUnit): number => {
+ switch (unit) {
+ case TimeUnit.DAYS:
+ return value;
+ case TimeUnit.WEEKS:
+ return value * 7;
+ case TimeUnit.MONTHS:
+ return value * 30; // Approximate
+ case TimeUnit.YEARS:
+ return value * 365; // Approximate, not accounting for leap years
+ default:
+ return value;
+ }
+ };
+
+ function handleSaveConfirmed() {
+ setIsPosting(true);
+ // Simulate async operation for better UX
+ setTimeout(() => {
+ // TODO: Implement actual save logic with DataStore or API
+ if (noRetention) {
+ console.log("Saving data retention settings: indefinite");
+ dispatch(
+ displayInfoNotification(
+ "Data retention set to indefinite (no automatic deletion)"
+ )
+ );
+ } else {
+ console.log("Saving data retention settings:", state);
+ dispatch(
+ displayInfoNotification(
+ `Data retention set to ${state.value} ${state.unit}`
+ )
+ );
+ }
+ setIsPosting(false);
+ setShowWarningDialog(false);
+ }, 500);
+ }
+
+ function handleSave() {
+ // Check if retention is enabled and less than 7 days
+ if (!noRetention) {
+ const totalDays = getTotalDays(state.value, state.unit);
+ if (totalDays < 7) {
+ setShowWarningDialog(true);
+ return;
+ }
+ }
+ handleSaveConfirmed();
+ }
+
+ if (whoamiFetching) {
+ return (
+
+
+
+ );
+ } else if (!whoami.roles.includes(models.Role.ADMIN)) {
+ return ;
+ } else {
+ const totalDays = !noRetention
+ ? getTotalDays(state.value, state.unit)
+ : 0;
+ return (
+
+
+
+ Set data retention time
+
+
+ Determine how long data should be retained before
+ automatic deletion
+
+
+ setNoRetention(e.target.checked)
+ }
+ disabled={isPosting}
+ />
+ }
+ label="Retain data indefinitely (no automatic deletion)"
+ />
+ {!noRetention && (
+
+ )}
+
+
+ setShowWarningDialog(false)}
+ onConfirmation={handleSaveConfirmed}
+ dialogTitle="Short Retention Period Warning"
+ >
+
+ You are setting a very short data retention period of{" "}
+
+ {totalDays} day{totalDays !== 1 ? "s" : ""}
+
+ . All data older than this will be automatically deleted.
+
+
+ Are you sure you want to proceed with this setting?
+
+
+
+ );
+ }
+}
+
+export default AdminDataRetention;