diff --git a/CHANGELOG.md b/CHANGELOG.md
index f72ed99d..bdc43ad2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ and this project adheres to
### Added
- ✨(backend) allow converting a file while it is being analyzed
+- ✨(frontend) add file type, contact and modification date topbar filters
### Fixed
diff --git a/src/frontend/apps/drive/src/features/drivers/Driver.ts b/src/frontend/apps/drive/src/features/drivers/Driver.ts
index 7b23e1c3..049acb57 100644
--- a/src/frontend/apps/drive/src/features/drivers/Driver.ts
+++ b/src/frontend/apps/drive/src/features/drivers/Driver.ts
@@ -42,6 +42,8 @@ export type ItemFilters = {
is_favorite?: boolean;
category?: string;
contact?: string;
+ updated_at_after?: string;
+ updated_at_before?: string;
};
export type PaginatedChildrenResult = {
diff --git a/src/frontend/apps/drive/src/features/explorer/components/app-view/ExplorerFilters.scss b/src/frontend/apps/drive/src/features/explorer/components/app-view/ExplorerFilters.scss
index 6af6bb20..12049adb 100644
--- a/src/frontend/apps/drive/src/features/explorer/components/app-view/ExplorerFilters.scss
+++ b/src/frontend/apps/drive/src/features/explorer/components/app-view/ExplorerFilters.scss
@@ -2,6 +2,9 @@
display: flex;
align-items: center;
gap: 0.5rem;
+
+ // Match the custom date range picker height with the filter buttons (32px).
+ --c--components--forms-datepicker--height: 32px;
}
.explorer__filters__item {
diff --git a/src/frontend/apps/drive/src/features/explorer/components/app-view/ExplorerFilters.tsx b/src/frontend/apps/drive/src/features/explorer/components/app-view/ExplorerFilters.tsx
index 30d47506..4eb79f6f 100644
--- a/src/frontend/apps/drive/src/features/explorer/components/app-view/ExplorerFilters.tsx
+++ b/src/frontend/apps/drive/src/features/explorer/components/app-view/ExplorerFilters.tsx
@@ -6,6 +6,7 @@ import {
SearchFilter,
SearchUserItem,
} from "@gouvfr-lasuite/ui-kit";
+import { DateRangePicker } from "@gouvfr-lasuite/cunningham-react";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import folderIcon from "@/assets/folder/folder.svg";
@@ -16,6 +17,7 @@ import { ItemType, UserLight } from "@/features/drivers/types";
import { ItemFilters, ItemFiltersScope } from "@/features/drivers/Driver";
import { useItems } from "../../hooks/useQueries";
import { useContacts, useUsers } from "@/features/users/hooks/useUserQueries";
+import { DatePreset, DateRange, presetRange } from "../../utils/dateFilters";
import { TFunction } from "i18next";
import { ItemIcon } from "../icons/ItemIcon";
import { getItemTitle } from "../../utils/utils";
@@ -58,6 +60,14 @@ export const ExplorerFilters = () => {
onFiltersChange?.(handleFilterChange(filters, name, value));
};
+ // The modification date filter drives two query params at once.
+ const onModifiedChange = (range: DateRange | null) => {
+ const newFilters = { ...filters };
+ delete newFilters.updated_at_after;
+ delete newFilters.updated_at_before;
+ onFiltersChange?.({ ...newFilters, ...(range ?? {}) });
+ };
+
return (
{
onChange={(value) => onChange("contact", value ?? ALL)}
/>
)}
+
);
};
@@ -220,6 +231,94 @@ export const ExplorerFilterCategory = (props: {
);
};
+const MODIFIED_PRESETS: DatePreset[] = [
+ "today",
+ "last_7_days",
+ "last_30_days",
+ "this_year",
+];
+const MODIFIED_CUSTOM = "custom";
+
+export const ExplorerFilterModified = (props: {
+ onChange: (range: DateRange | null) => void;
+}) => {
+ const { t } = useTranslation();
+ const [preset, setPreset] = useState(null);
+ const [isCustom, setIsCustom] = useState(false);
+
+ const options: FilterOption[] = useMemo(
+ () => [
+ { ...getResetOption(t), showSeparator: true },
+ ...MODIFIED_PRESETS.map((value) => ({
+ label: t(`explorer.filters.modified.options.${value}`),
+ value,
+ render: () => (
+
+ {t(`explorer.filters.modified.options.${value}`)}
+
+ ),
+ })),
+ {
+ label: t("explorer.filters.modified.options.custom"),
+ value: MODIFIED_CUSTOM,
+ render: () => (
+
+ {t("explorer.filters.modified.options.custom")}
+
+ ),
+ },
+ ],
+ [t],
+ );
+
+ const onSelectionChange = (key: Key | null) => {
+ if (key === ALL) {
+ setPreset(null);
+ setIsCustom(false);
+ props.onChange(null);
+ return;
+ }
+ if (key === MODIFIED_CUSTOM) {
+ setPreset(key);
+ setIsCustom(true);
+ return;
+ }
+ setPreset(key);
+ setIsCustom(false);
+ props.onChange(presetRange(key as DatePreset));
+ };
+
+ return (
+ <>
+
+ {isCustom && (
+
+ // The picker emits ISO datetimes; the backend wants date-only bounds.
+ props.onChange(
+ range
+ ? {
+ updated_at_after: range[0].slice(0, 10),
+ updated_at_before: range[1].slice(0, 10),
+ }
+ : null,
+ )
+ }
+ />
+ )}
+ >
+ );
+};
+
export const ExplorerFilterType = (props: {
value: ItemType | null;
onChange: (value: Key | null) => void;
diff --git a/src/frontend/apps/drive/src/features/explorer/utils/__tests__/dateFilters.test.ts b/src/frontend/apps/drive/src/features/explorer/utils/__tests__/dateFilters.test.ts
new file mode 100644
index 00000000..8d4b8692
--- /dev/null
+++ b/src/frontend/apps/drive/src/features/explorer/utils/__tests__/dateFilters.test.ts
@@ -0,0 +1,40 @@
+import { presetRange, toISODate } from "../dateFilters";
+
+describe("presetRange", () => {
+ // 2026-05-29 in local time (month is 0-indexed).
+ const today = new Date(2026, 4, 29);
+
+ it("returns the current day for the today preset", () => {
+ expect(presetRange("today", today)).toEqual({
+ updated_at_after: "2026-05-29",
+ updated_at_before: "2026-05-29",
+ });
+ });
+
+ it("spans the last 7 days, today included", () => {
+ expect(presetRange("last_7_days", today)).toEqual({
+ updated_at_after: "2026-05-23",
+ updated_at_before: "2026-05-29",
+ });
+ });
+
+ it("spans the last 30 days, today included", () => {
+ expect(presetRange("last_30_days", today)).toEqual({
+ updated_at_after: "2026-04-30",
+ updated_at_before: "2026-05-29",
+ });
+ });
+
+ it("spans from the first day of the year", () => {
+ expect(presetRange("this_year", today)).toEqual({
+ updated_at_after: "2026-01-01",
+ updated_at_before: "2026-05-29",
+ });
+ });
+});
+
+describe("toISODate", () => {
+ it("formats a local date as YYYY-MM-DD", () => {
+ expect(toISODate(new Date(2026, 0, 5))).toBe("2026-01-05");
+ });
+});
diff --git a/src/frontend/apps/drive/src/features/explorer/utils/dateFilters.ts b/src/frontend/apps/drive/src/features/explorer/utils/dateFilters.ts
new file mode 100644
index 00000000..2739a0c2
--- /dev/null
+++ b/src/frontend/apps/drive/src/features/explorer/utils/dateFilters.ts
@@ -0,0 +1,49 @@
+import { ItemFilters } from "@/features/drivers/Driver";
+
+export type DatePreset = "today" | "last_7_days" | "last_30_days" | "this_year";
+
+export type DateRange = Pick<
+ ItemFilters,
+ "updated_at_after" | "updated_at_before"
+>;
+
+const pad = (value: number) => String(value).padStart(2, "0");
+
+/**
+ * Format a date as a local YYYY-MM-DD string, matching the date-only bounds the
+ * backend filter expects (it compares on the date part, whole-day inclusive).
+ */
+export const toISODate = (date: Date): string =>
+ `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
+
+const addDays = (date: Date, days: number): Date => {
+ const result = new Date(date);
+ result.setDate(result.getDate() + days);
+ return result;
+};
+
+/**
+ * Compute the modification date range for a preset, today included as the upper
+ * bound. `today` is injectable for deterministic tests.
+ */
+export const presetRange = (preset: DatePreset, today: Date = new Date()): DateRange => {
+ let after: Date;
+ switch (preset) {
+ case "today":
+ after = today;
+ break;
+ case "last_7_days":
+ after = addDays(today, -6);
+ break;
+ case "last_30_days":
+ after = addDays(today, -29);
+ break;
+ case "this_year":
+ after = new Date(today.getFullYear(), 0, 1);
+ break;
+ }
+ return {
+ updated_at_after: toISODate(after),
+ updated_at_before: toISODate(today),
+ };
+};
diff --git a/src/frontend/apps/drive/src/features/i18n/translations.json b/src/frontend/apps/drive/src/features/i18n/translations.json
index 5c085f90..c11e1b9a 100644
--- a/src/frontend/apps/drive/src/features/i18n/translations.json
+++ b/src/frontend/apps/drive/src/features/i18n/translations.json
@@ -282,6 +282,18 @@
"empty": "No contact found",
"reset": "Reset"
},
+ "modified": {
+ "label": "Modified",
+ "options": {
+ "today": "Today",
+ "last_7_days": "Last 7 days",
+ "last_30_days": "Last 30 days",
+ "this_year": "This year",
+ "custom": "Custom range"
+ },
+ "start": "Start",
+ "end": "End"
+ },
"workspace": {
"label": "Workspace"
},
@@ -907,6 +919,18 @@
"empty": "Aucun contact trouvé",
"reset": "Réinitialiser"
},
+ "modified": {
+ "label": "Modifié",
+ "options": {
+ "today": "Aujourd'hui",
+ "last_7_days": "7 derniers jours",
+ "last_30_days": "30 derniers jours",
+ "this_year": "Cette année",
+ "custom": "Période personnalisée"
+ },
+ "start": "Début",
+ "end": "Fin"
+ },
"workspace": {
"label": "Espace"
},