Skip to content

Create task screen — due date field #122

Closed

Description

@MessiasLima

Description

Add an optional due date action to the Create Task screen so users can choose when a task is due. Not every task has a due date, so the default state should be no due date.

This extends the core create task flow from #121. The UI should not behave like a required text field. It should be a discreet label/button-style action that says "Add due date" when no date is selected. Tapping it opens the date picker. After selection, the chosen date is displayed and persisted into Task.dueDate, represented as kotlin.time.Instant from #120.

The visual design of this action must be reviewed as part of the ticket to ensure it fits the Create Task screen layout before implementation is considered complete.

Scope

In scope: optional due date UI action, "Add due date" empty state, date picker interaction, selected date display, clearing/removing the selected due date if supported by the reviewed design, saving the selected value into Task.dueDate, string resources, design review, and ViewModel tests for null default and date changes.

Out of scope: priority, URL, location, recurring tasks, reminders, custom calendar component, and date-based list grouping.

Acceptance criteria

  • GIVEN the user opens the Create Task screen
    WHEN the form is displayed
    THEN the due date is unset by default
  • GIVEN no due date is selected
    WHEN the Create Task screen is displayed
    THEN the UI shows an "Add due date" label/button-style action, not a required text field
  • GIVEN the user taps "Add due date"
    WHEN the action is triggered
    THEN a date picker is shown
  • GIVEN the date picker is shown
    WHEN the user selects a date
    THEN the selected date is shown in the due date action area
  • GIVEN the user selects a due date and saves the task
    WHEN the task is persisted
    THEN dueDate stores the selected day as a kotlin.time.Instant
  • GIVEN the user does not select a due date
    WHEN they save the task
    THEN dueDate is persisted as null
  • GIVEN the due date UI has been implemented
    WHEN the ticket is reviewed
    THEN the design is explicitly reviewed for placement, visual hierarchy, tap target, empty state, and selected-date state

Activity

  1. added this to the next milestone on Jul 15, 2026
  2. MessiasLima commented on Jul 26, 2026

    @MessiasLima
    OwnerAuthor

    /oc

    Issue #122 Plan: Create Task Due Date

    Goal

    Add an optional due date to the Create Task flow using a reusable FoliaryDatePicker component.

    The due date must:

    • be unset by default
    • be optional when saving a task
    • display as a card-style action, not a text field
    • open a bottom sheet with a date picker when tapped
    • show the selected date after confirmation
    • show an inline X icon button when a date is selected
    • clear the date by calling onDateSelected(null) when the X button is tapped
    • persist into Task.dueDate as kotlin.time.Instant
    • include string resources and test coverage
    • include an explicit design review before the ticket is considered complete

    Existing State

    Current Create Task Flow

    • CreateTaskScreen currently renders the title field, description field, and save button.
    • There is no due date UI in the screen today.

    File:

    • foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt

    Current Create Task State And Events

    • CreateTaskViewData currently contains id, title, description, and saveButtonEnabled.
    • CreateTaskEvent currently contains TitleChanged, DescriptionChanged, SaveClicked, and BackClicked.
    • The save path always persists dueDate = null.

    File:

    • foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModel.kt

    Current Task Model

    Task already supports due dates, so no schema or repository shape change is needed.

    • Task.dueDate: Instant? already exists.
    • TaskRepository.save(task) already persists it.
    • DAO queries already understand nullable due dates.

    Files:

    • foliary/src/commonMain/kotlin/dev/appoutlet/foliary/data/task/database/entity/Task.kt
    • foliary/src/commonMain/kotlin/dev/appoutlet/foliary/data/task/TaskRepository.kt
    • foliary/src/commonMain/kotlin/dev/appoutlet/foliary/data/task/TaskRepositoryImpl.kt
    • foliary/src/commonMain/kotlin/dev/appoutlet/foliary/data/task/database/TaskDao.kt

    Existing UI Patterns

    • FoliaryCard already provides the visual base for the new component.
    • There is no existing date picker component in the project.
    • There is no existing date formatting utility for this use case.
    • There is already project support for ModalBottomSheet.

    Files:

    • foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/card/FoliaryCard.kt
    • foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/scene/BottomSheetScene.kt

    Chosen Implementation Approach

    Component

    Create a new reusable component named FoliaryDatePicker.

    Recommended file:

    • foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/datepicker/FoliaryDatePicker.kt

    This component will:

    • look like a FoliaryCard
    • show a label and a selected-date text area
    • open a bottom sheet with a Material 3 DatePicker
    • emit the selected value through a lambda
    • show a trailing X icon button when a date is selected
    • clear the date by calling onDateSelected(null)

    Why This Approach

    • Keeps the Create Task screen simple.
    • Creates a reusable component for future task editing flows.
    • Avoids introducing a custom calendar implementation.
    • Aligns with the requirement that the UI is card-like, not a text field.
    • Keeps the clear behavior close to the selected value, where users expect it.

    Component Contract

    Recommended public API:

    @Composable
    fun FoliaryDatePicker(
        label: String,
        placeholder: String,
        selectedDate: Instant?,
        onDateSelected: (Instant?) -> Unit,
        modifier: Modifier = Modifier,
    )

    Behavior Rules

    • If selectedDate == null, show the placeholder text and do not show the clear button.
    • If selectedDate != null, show the selected date text and a trailing X icon button.
    • Tapping the card opens a modal bottom sheet.
    • Selecting a date and confirming calls onDateSelected(selectedInstant) and closes the sheet.
    • Tapping the X icon calls onDateSelected(null).
    • Tapping the X icon must not open the bottom sheet.

    UI Details

    Card Layout

    The component should be visually structured as:

    • label on top
    • value row below
    • selected date or placeholder on the leading side
    • clear X icon button on the trailing side when there is a selected date

    Recommended style:

    • full width
    • same horizontal padding as the title and description fields
    • subtle visual hierarchy
    • entire card is the tap target
    • clear button remains visually secondary

    Interaction Detail

    The clear button must consume its own click event. Pressing X must clear the date only. It must not also trigger the card tap and open the bottom sheet.

    Bottom Sheet Details

    Sheet Content

    The bottom sheet will contain:

    • a Material 3 DatePicker
    • a dismiss button
    • a confirm button

    Recommended implementation details:

    • keep sheet visibility state local to FoliaryDatePicker
    • use DatePicker and rememberDatePickerState
    • initialize picker state from selectedDate
    • enable confirm only when a date is selected in the picker
    • close the sheet after confirm or cancel

    Sheet State Initialization

    When the card opens and there is already a selected date:

    • convert the stored Instant to the current system local date
    • use that local date to initialize the picker selection

    This avoids timezone drift and keeps the selected day stable.

    Persistence Semantics

    Chosen Rule

    Store the picked day as the end of the selected local day, converted to Instant.

    Recommended rationale:

    • the app currently determines overdue tasks using dueDate < now
    • if the selected day is stored as start-of-day, a task becomes overdue at the beginning of its due day
    • storing the end of the selected day better matches user expectation

    Conversion Behavior

    When confirming from the picker:

    1. read the selected picker day
    2. convert it to local date in the current system timezone
    3. convert that local date to the last instant of that local day
    4. persist that final Instant

    The conversion should use TimeZone.currentSystemDefault().

    Preselecting A Saved Date In The Picker

    When a saved due date already exists:

    1. convert the stored Instant to local date
    2. convert that local date to a picker-compatible day value
    3. feed it into the DatePicker initial state

    Selected Date Display Format

    There is no existing shared date formatting utility in the project and no new libraries may be added without explicit permission.

    Recommended initial formatting approach:

    • convert Instant to local date using TimeZone.currentSystemDefault()
    • render the local date using LocalDate.toString()

    This produces a stable YYYY-MM-DD display without new dependencies.

    If a more human-friendly localized format is desired later, it should be handled in a separate formatting improvement task.

    Create Task Screen Changes

    Update CreateTaskScreen to render FoliaryDatePicker.

    File:

    • foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt

    Placement

    Recommended placement:

    • below description
    • above the bottom save button

    Wiring

    Pass the current state and emit changes through the existing event channel.

    Expected integration shape:

    FoliaryDatePicker(
        modifier = Modifier.widthInCompact()
            .fillMaxWidth()
            .padding(horizontal = 16.dp)
            .testTag("CreateTaskScreen:DueDatePicker"),
        label = stringResource(Res.string.create_task_due_date_label),
        placeholder = stringResource(Res.string.create_task_due_date_placeholder),
        selectedDate = viewData.dueDate,
        onDateSelected = { onEvent(CreateTaskEvent.DueDateChanged(it)) },
    )

    Test Tags

    Recommended tags:

    • CreateTaskScreen:DueDatePicker
    • FoliaryDatePicker:Card
    • FoliaryDatePicker:Value
    • FoliaryDatePicker:ClearButton

    These will help UI tests stay stable.

    ViewModel Changes

    Update CreateTaskViewModel.kt.

    File:

    • foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModel.kt

    State Changes

    Add a new property to CreateTaskViewData:

    val dueDate: Instant? = null

    Event Changes

    Add a new event:

    data class DueDateChanged(val dueDate: Instant?) : CreateTaskEvent

    Reducer Changes

    Handle the new event by updating state.dueDate.

    Recommended handler:

    private fun onDueDateChange(dueDate: Instant?) = intent {
        reduce { state.copy(dueDate = dueDate) }
    }

    Save Changes

    Update the saved Task construction to use:

    dueDate = state.dueDate

    instead of:

    dueDate = null

    No Changes Expected

    These parts should not need behavior changes:

    • navigation
    • repository interface
    • DAO
    • database migration
    • save-button enablement logic

    String Resources

    Add the following strings to:

    • foliary/src/commonMain/composeResources/values/strings.xml

    Required strings:

    • create_task_due_date_label
    • create_task_due_date_placeholder
    • create_task_due_date_clear_a11y
    • date_picker_confirm
    • date_picker_cancel

    Recommended English copy:

    • create_task_due_date_label: Due date
    • create_task_due_date_placeholder: Add due date
    • create_task_due_date_clear_a11y: Clear due date
    • date_picker_confirm: Confirm
    • date_picker_cancel: Cancel

    No hardcoded UI strings should be introduced anywhere in the screen or component.

    Icon Choice

    Use the existing Lucide icon set.

    Recommended icon:

    • Lucide.X

    Reason:

    • already aligned with the project icon library
    • matches the requested visual behavior
    • consistent with current usage of Lucide icons elsewhere

    Accessibility

    The component should include:

    • content description on the clear button using create_task_due_date_clear_a11y
    • no duplicate content descriptions for decorative icons
    • clear tap target sizing for the X icon button
    • readable placeholder and selected-date text states

    Internal Helper Logic

    Keep helper logic private to the new component file unless reuse becomes necessary.

    Recommended private helpers:

    • convert Instant to local date
    • convert local date to picker-compatible millis
    • convert local date to end-of-day Instant
    • format selected Instant for display

    This keeps the new behavior self-contained and avoids premature abstraction.

    Test Plan

    1. ViewModel Tests

    Update:

    • foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModelTest.kt

    Add coverage for:

    • default state has dueDate == null
    • DueDateChanged(selectedDate) updates state
    • DueDateChanged(null) clears state
    • save persists selected due date
    • save persists null when untouched

    2. Screen Tests

    Update:

    • foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt

    Add coverage for:

    • due date label is shown
    • placeholder text is shown when no date is selected
    • selected date text is shown when viewData.dueDate is present
    • clear button is shown only when a date exists
    • tapping the clear button emits CreateTaskEvent.DueDateChanged(null)

    3. Component Tests

    Add:

    • foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/datepicker/FoliaryDatePickerTest.kt

    Recommended coverage:

    • placeholder is rendered when selectedDate == null
    • selected date is rendered when selectedDate != null
    • clear button is hidden when date is null
    • clear button is visible when date is selected
    • clear button calls onDateSelected(null)
    • tapping the card opens the sheet if Compose test APIs are reliable for the current setup

    4. Scope Of Verification

    Follow repo rules:

    • do not run full build
    • do not run all tests
    • run only focused JVM tests related to changed code

    Recommended commands after implementation:

    ./gradlew foliary:jvmTest --tests "dev.appoutlet.foliary.feature.createtask.CreateTaskViewModelTest"
    ./gradlew foliary:jvmTest --tests "dev.appoutlet.foliary.feature.createtask.CreateTaskScreenTest"
    ./gradlew foliary:jvmTest --tests "dev.appoutlet.foliary.core.ui.component.datepicker.FoliaryDatePickerTest"

    Design Review Checklist

    The issue requires explicit design review. The implementation is not complete until this review happens.

    Review checklist:

    • placement feels natural in the Create Task layout
    • the due date card does not compete with the title field
    • empty state clearly reads as optional
    • selected-date state is easy to scan
    • trailing clear button is visible but not visually heavy
    • tap target is large enough on mobile
    • spacing works on compact and desktop layouts
    • bottom sheet flow feels consistent with the app's Material 3 UI

    Non-Goals

    This ticket does not include:

    • recurring tasks
    • reminders
    • priority
    • URLs
    • location
    • custom calendar component
    • date grouping changes in task lists
    • localized date formatting infrastructure
    • edit-task flow support beyond the reusable component being future-safe

    Risks And Edge Cases

    Timezone Conversion

    Risk:

    • the picker works with day values, while persistence uses Instant

    Mitigation:

    • always convert through local date using TimeZone.currentSystemDefault()
    • never persist picker millis directly

    Overdue Semantics

    Risk:

    • tasks can become overdue too early if start-of-day is stored

    Mitigation:

    • store end-of-day Instant

    Click Handling

    Risk:

    • tapping the clear button could also trigger the card click

    Mitigation:

    • keep the clear button as its own IconButton
    • ensure it handles the interaction independently

    Date Formatting

    Risk:

    • no shared localized formatter exists

    Mitigation:

    • use a stable YYYY-MM-DD display for now
    • defer localization improvements to a follow-up task if desired

    File Impact Summary

    New file:

    • foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/datepicker/FoliaryDatePicker.kt

    Updated files:

    • foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt
    • foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModel.kt
    • foliary/src/commonMain/composeResources/values/strings.xml
    • foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModelTest.kt
    • foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt

    Recommended new test file:

    • foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/datepicker/FoliaryDatePickerTest.kt

    Acceptance Criteria Mapping

    • Due date unset by default: handled by CreateTaskViewData(dueDate = null).
    • Add due date shown when empty: handled by FoliaryDatePicker placeholder state.
    • Tap opens date picker: handled by clickable card opening bottom sheet.
    • Selected date shown after picking: handled by selected-state rendering in FoliaryDatePicker.
    • Selected date persisted: handled by CreateTaskViewModel saving state.dueDate.
    • No date persists null: handled by default state and clear behavior.
    • Clear selected date: handled by the trailing X button calling onDateSelected(null).
    • Design reviewed explicitly: handled by the review checklist above.

    Final Recommendation

    Proceed with a minimal implementation that:

    • introduces FoliaryDatePicker as a reusable card-based component
    • keeps FoliaryCard unchanged
    • stores selected dates as end-of-day local Instant
    • formats displayed dates using local YYYY-MM-DD
    • supports inline clearing with a trailing X icon button that emits null
    • adds focused ViewModel, screen, and component tests
  3. opencode-agent commented on Jul 26, 2026

    @opencode-agent
    Contributor

    Created PR #197

    New%20session%20-%202026-07-26T14%3A36%3A45.446Z
    opencode session  |  github run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions