diff --git a/src/features/schedule/components/ScheduleStartPage.test.tsx b/src/features/schedule/components/ScheduleStartPage.test.tsx
index d3c552f9..0ec5d5cf 100644
--- a/src/features/schedule/components/ScheduleStartPage.test.tsx
+++ b/src/features/schedule/components/ScheduleStartPage.test.tsx
@@ -91,10 +91,29 @@ vi.mock("@/shared/dictionaries/index.js", () => ({
getCityCodeByAirportCode: () => undefined,
}));
+// Capture the latest onCity callback so individual tests can invoke it.
+let capturedOnCity: ((code: string) => void) | null = null;
+let capturedShouldApply: (() => boolean) | null = null;
+let geoMockEnabled = false;
+
+vi.mock("@/shared/hooks/useGeoCityDefault.js", () => ({
+ useGeoCityDefault: (opts: { shouldApply: () => boolean; onCity: (code: string) => void }) => {
+ capturedOnCity = opts.onCity;
+ capturedShouldApply = opts.shouldApply;
+ // If the test enabled geo, fire it synchronously so we don't need async.
+ if (geoMockEnabled && opts.shouldApply()) {
+ opts.onCity("MOW");
+ }
+ },
+}));
+
describe("ScheduleStartPage", () => {
beforeEach(() => {
vi.clearAllMocks();
sessionStore.clear();
+ geoMockEnabled = false;
+ capturedOnCity = null;
+ capturedShouldApply = null;
});
it("renders the start page", () => {
@@ -180,3 +199,53 @@ describe("4.1.8: Schedule hydrates from cross-section store on mount", () => {
expect(depInput.defaultValue).toBe("");
});
});
+
+// ---------------------------------------------------------------------------
+// TZ §4.1.1-R8/R10: first-entry geolocation auto-fill + time default
+// ---------------------------------------------------------------------------
+
+describe("4.1.1-R8/R10: Schedule first-entry geolocation + time default", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ sessionStore.clear();
+ resetCrossSectionStore();
+ geoMockEnabled = false;
+ capturedOnCity = null;
+ capturedShouldApply = null;
+ });
+
+ it("4.1.1-R8: populates departure with geolocated city on first entry", () => {
+ geoMockEnabled = true; // mock will call onCity("MOW") synchronously on shouldApply()
+ render();
+ const depInput = screen.getByTestId("schedule-departure-input") as HTMLInputElement;
+ expect(depInput.defaultValue).toBe("MOW");
+ });
+
+ it("4.1.1-R8: does not populate when geolocation denied (geoMockEnabled=false)", () => {
+ // geoMockEnabled stays false → useGeoCityDefault mock never fires onCity
+ render();
+ const depInput = screen.getByTestId("schedule-departure-input") as HTMLInputElement;
+ expect(depInput.defaultValue).toBe("");
+ });
+
+ it("4.1.1-R8: does not override departure when cross-section store has schedule filter", () => {
+ setScheduleFilter({
+ mode: "route", departure: "LED", arrival: "KGD",
+ dateFrom: "20260515", dateTo: "20260521",
+ timeFrom: "0000", timeTo: "2400",
+ onlyDirect: false, showReturn: false, searchExecuted: false,
+ });
+ geoMockEnabled = true;
+ render();
+ // Departure should be LED from the snapshot, NOT overridden by geo.
+ const depInput = screen.getByTestId("schedule-departure-input") as HTMLInputElement;
+ expect(depInput.defaultValue).toBe("LED");
+ });
+
+ it("4.1.1-R10: time default = 00:00-24:00 on all viewports", () => {
+ render();
+ const timeSelector = screen.getByTestId("time-selector");
+ expect(timeSelector.textContent).toContain("00:00");
+ expect(timeSelector.textContent).toContain("24:00");
+ });
+});
diff --git a/src/features/schedule/components/ScheduleStartPage.tsx b/src/features/schedule/components/ScheduleStartPage.tsx
index 8da32ea3..d453c47c 100644
--- a/src/features/schedule/components/ScheduleStartPage.tsx
+++ b/src/features/schedule/components/ScheduleStartPage.tsx
@@ -39,6 +39,7 @@ import {
getCityCodeByAirportCode,
} from "@/shared/dictionaries/index.js";
import type { IDictionaries } from "@/shared/dictionaries/index.js";
+import { useGeoCityDefault } from "@/shared/hooks/useGeoCityDefault.js";
import { buildScheduleUrl } from "../url.js";
import { scheduleWindowBounds } from "@/shared/dateWindow.js";
import "./ScheduleStartPage.scss";
@@ -136,6 +137,18 @@ export const ScheduleStartPage: FC = () => {
const [departureCode, setDepartureCode] = useState(prefill.departure ?? "");
const [arrivalCode, setArrivalCode] = useState(prefill.arrival ?? "");
+ // TZ §4.1.1-R8: on first session entry with geo consent, auto-fill
+ // departure city with the user's nearest city when:
+ // - no stored schedule filter is present (fresh session), AND
+ // - the current departure is empty (not populated by prefill or user).
+ useGeoCityDefault({
+ dictionaries,
+ shouldApply: () => !getScheduleFilter() && !departureCode,
+ onCity: (cityCode) => {
+ setDepartureCode((prev) => (prev ? prev : cityCode));
+ },
+ });
+
// Start blank to match Angular's `ДД.ММ.ГГГГ - ДД.ММ.ГГГГ` placeholder
// (the "current week" pre-fill was a React-only convenience that
// pulled the date input out of parity). Submit handler defaults to