package com.margelo.nitro.lunardatepicker.ui.fragments import android.annotation.SuppressLint import android.app.Dialog import android.graphics.Typeface import android.graphics.drawable.GradientDrawable import android.os.Bundle import android.view.Gravity import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.FrameLayout import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.core.view.doOnLayout import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.margelo.nitro.lunardatepicker.LDP_NativeAssetSource import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.kizitonwose.calendar.core.CalendarDay import com.kizitonwose.calendar.core.CalendarMonth import com.kizitonwose.calendar.view.CalendarView import com.kizitonwose.calendar.view.MarginValues import com.kizitonwose.calendar.view.MonthDayBinder import com.kizitonwose.calendar.view.MonthHeaderFooterBinder import com.margelo.nitro.lunardatepicker.LDP_Range import com.margelo.nitro.lunardatepicker.R import com.margelo.nitro.lunardatepicker.constants.DataConstants import com.margelo.nitro.lunardatepicker.constants.LayoutConstants import com.margelo.nitro.lunardatepicker.models.DateSelection import com.margelo.nitro.lunardatepicker.models.PickerConfig import com.margelo.nitro.lunardatepicker.models.SerializableRange import com.margelo.nitro.lunardatepicker.ui.builders.UIBuilder import com.margelo.nitro.lunardatepicker.ui.viewholders.DayViewHolder import com.margelo.nitro.lunardatepicker.ui.viewholders.MonthViewHolder import com.margelo.nitro.lunardatepicker.utils.DateConverter import com.margelo.nitro.lunardatepicker.utils.DimensionUtils.dpToPx import com.margelo.nitro.lunardatepicker.utils.ScaleUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.time.DayOfWeek import java.time.LocalDate import java.time.YearMonth import java.time.ZoneId class LunarDatePickerFragment : BottomSheetDialogFragment() { companion object { private const val TAG = DataConstants.LogTags.FRAGMENT private const val ARG_CONFIG = DataConstants.BundleKeys.ARG_CONFIG private const val ARG_MIN_DATE = DataConstants.BundleKeys.ARG_MIN_DATE private const val ARG_MAX_DATE = DataConstants.BundleKeys.ARG_MAX_DATE private const val ARG_INITIAL_VALUE = DataConstants.BundleKeys.ARG_INITIAL_VALUE fun newInstance( config: PickerConfig, minimumDate: LocalDate? = null, maximumDate: LocalDate? = null, initialValue: LDP_Range? = null, onResult: (LDP_Range) -> Unit ): LunarDatePickerFragment { val fragment = LunarDatePickerFragment() fragment.setupCallbacks(onResult) fragment.arguments = createArguments(config, minimumDate, maximumDate, initialValue) return fragment } private fun createArguments( config: PickerConfig, minimumDate: LocalDate?, maximumDate: LocalDate?, initialValue: LDP_Range? ): Bundle { return Bundle().apply { putSerializable(ARG_CONFIG, config) minimumDate?.let { putString(ARG_MIN_DATE, it.toString()) } maximumDate?.let { putString(ARG_MAX_DATE, it.toString()) } initialValue?.let { putSerializable(ARG_INITIAL_VALUE, SerializableRange.fromRange(it)) } } } } // Dependencies private lateinit var config: PickerConfig private lateinit var dateConverter: DateConverter private lateinit var uiBuilder: UIBuilder private lateinit var calendarView: CalendarView private lateinit var confirmButton: Button // State private var minimumDate: LocalDate? = null private var maximumDate: LocalDate? = null private var initialValue: LDP_Range? = null private var selection = DateSelection() private val timeZone: ZoneId by lazy { config.calendar.timeZone.toZoneId() } // Callbacks private var onResultCallback: ((LDP_Range) -> Unit)? = null var imageLoader: ((com.margelo.nitro.lunardatepicker.LDP_NativeAssetSource?, (android.graphics.Bitmap?) -> Unit) -> Unit)? = null // UI Elements for Top Header private var topHeaderContainer: LinearLayout? = null private var departTitleLabel: TextView? = null private var departDateLabel: TextView? = null private var returnTitleLabel: TextView? = null private var returnDateLabel: TextView? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) initializeFromArguments() initializeDependencies() setupInitialSelection() prewarmLunarCache() } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val bottomSheetDialog = BottomSheetDialog(requireContext()) val view = createBottomSheetView() bottomSheetDialog.setContentView(view) setupBottomSheetBehavior(bottomSheetDialog) return bottomSheetDialog } override fun onDestroyView() { super.onDestroyView() // Clear binders to prevent potential memory leaks calendarView.dayBinder = null calendarView.monthHeaderBinder = null } override fun onDestroy() { super.onDestroy() cleanup() } private fun initializeFromArguments() { arguments?.let { args -> config = args.getSerializable(ARG_CONFIG) as PickerConfig minimumDate = args.getString(ARG_MIN_DATE)?.let { LocalDate.parse(it) } maximumDate = args.getString(ARG_MAX_DATE)?.let { LocalDate.parse(it) } val serializableRange = args.getSerializable(ARG_INITIAL_VALUE) as? SerializableRange initialValue = serializableRange?.toRange() } } private fun initializeDependencies() { dateConverter = DateConverter() uiBuilder = UIBuilder(requireContext(), config) } private fun setupCallbacks( onResult: (LDP_Range) -> Unit ) { onResultCallback = onResult } private fun createBottomSheetView(): View { val rootView = LinearLayout(requireContext()).apply { orientation = LinearLayout.VERTICAL setBackgroundColor(config.controller.backgroundColor) } // Always add header bar (contains title and close button) rootView.addView(createHeaderBar()) // Only add gradient background and date selection labels if showHeader is true if (config.controller.showHeader) { rootView.addView(createDateSelectionHeader()) } rootView.addView(uiBuilder.createWeekView()) rootView.addView(createCalendarContainer()) if (config.controller.showSubmitButton) { rootView.addView(createBottomBar()) } return rootView } @SuppressLint("DiscouragedApi") private fun createDateSelectionHeader(): View { val context = requireContext() topHeaderContainer = LinearLayout(context).apply { orientation = LinearLayout.VERTICAL layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) setBackgroundResource(com.margelo.nitro.lunardatepicker.R.drawable.gradient_background) // Assuming you have a gradient_background drawable setPadding( LayoutConstants.Padding.TOP_HEADER_HORIZONTAL, LayoutConstants.Padding.TOP_HEADER_VERTICAL, LayoutConstants.Padding.TOP_HEADER_HORIZONTAL, LayoutConstants.Padding.TOP_HEADER_VERTICAL ) } // Note: createHeaderBar() is now called separately in createBottomSheetView() // This method is only called when showHeader is true, so no need to check again val titleContainer = LinearLayout(context).apply { orientation = LinearLayout.HORIZONTAL gravity = Gravity.CENTER_HORIZONTAL layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) } departTitleLabel = TextView(context).apply { text = config.controller.fromText ?: "Ngày đi" setTextColor(android.graphics.Color.WHITE) textSize = LayoutConstants.TextSize.TOP_HEADER_TITLE gravity = Gravity.CENTER_HORIZONTAL layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) } departDateLabel = TextView(context).apply { text = config.controller.notSelectedText ?: "Chưa chọn" setTextColor(config.controller.secondaryTextColor) textSize = LayoutConstants.TextSize.TOP_HEADER_DATE setTypeface(null, Typeface.BOLD) gravity = Gravity.CENTER_HORIZONTAL layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ).apply { topMargin = dpToPx(8) } } val divider = View(context).apply { layoutParams = LinearLayout.LayoutParams( LayoutConstants.Dimensions.TOP_HEADER_DIVIDER_WIDTH, LayoutConstants.Dimensions.TOP_HEADER_DIVIDER_HEIGHT ).apply { gravity = Gravity.CENTER_VERTICAL } setBackgroundColor(config.controller.borderColor) } returnTitleLabel = TextView(context).apply { text = config.controller.toText ?: "Ngày về" setTextColor(android.graphics.Color.WHITE) textSize = LayoutConstants.TextSize.TOP_HEADER_TITLE gravity = Gravity.CENTER_HORIZONTAL layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) } returnDateLabel = TextView(context).apply { text = config.controller.notSelectedText ?: "Chưa chọn" setTextColor(config.controller.secondaryTextColor) textSize = LayoutConstants.TextSize.TOP_HEADER_DATE setTypeface(null, Typeface.BOLD) gravity = Gravity.CENTER_HORIZONTAL layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ).apply { topMargin = dpToPx(8) } } val leftSection = LinearLayout(context).apply { orientation = LinearLayout.VERTICAL gravity = Gravity.CENTER_HORIZONTAL layoutParams = LinearLayout.LayoutParams( 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f ) setPadding(0, dpToPx(8), 0, dpToPx(8)) } val rightSection = LinearLayout(context).apply { orientation = LinearLayout.VERTICAL gravity = Gravity.CENTER_HORIZONTAL layoutParams = LinearLayout.LayoutParams( 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f ) setPadding(0, dpToPx(8), 0, dpToPx(8)) } leftSection.addView(departTitleLabel) leftSection.addView(departDateLabel) if (!config.controller.isSingleMode) { rightSection.addView(returnTitleLabel) rightSection.addView(returnDateLabel) } titleContainer.addView(leftSection) if (!config.controller.isSingleMode) { titleContainer.addView(divider) titleContainer.addView(rightSection) } // If single mode, ensure divider visibility is gone if (config.controller.isSingleMode) { divider.visibility = View.GONE } // Wrap titleContainer in a FrameLayout to overlay absolute-positioned icons val overlayContainer = FrameLayout(context).apply { layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) } overlayContainer.addView(titleContainer, FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT )) // Left absolute icon (calendar_from) val leftIcon = ImageView(context).apply { val resId = resources.getIdentifier("calendar_from", "mipmap", context.packageName) if (resId != 0) setImageResource(resId) scaleType = ImageView.ScaleType.CENTER_CROP config.controller.fromImage.let { source -> imageLoader?.invoke(source) { bitmap -> bitmap?.let { setImageBitmap(it) } } } } overlayContainer.addView(leftIcon, FrameLayout.LayoutParams(dpToPx(ScaleUtils.scaleDp(40)), dpToPx(ScaleUtils.scaleDp(40)), Gravity.START or Gravity.TOP)) // Right absolute icon (calendar_to) val rightIcon = ImageView(context).apply { val resId = resources.getIdentifier("calendar_to", "mipmap", context.packageName) if (resId != 0) setImageResource(resId) scaleType = ImageView.ScaleType.CENTER_CROP config.controller.toImage.let { source -> imageLoader?.invoke(source) { bitmap -> bitmap?.let { setImageBitmap(it) } } } } if (!config.controller.isSingleMode) { overlayContainer.addView(rightIcon, FrameLayout.LayoutParams(dpToPx(ScaleUtils.scaleDp(40)), dpToPx(ScaleUtils.scaleDp(40)), Gravity.END or Gravity.TOP)) } topHeaderContainer!!.addView(overlayContainer) return topHeaderContainer!! } /** * Creates the header bar with close icon and centered title * This is always needed regardless of showHeader value */ private fun createHeaderBar(): View { val context = requireContext() val headerBar = LinearLayout(context).apply { orientation = LinearLayout.HORIZONTAL setBackgroundColor(config.controller.backgroundColor) layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) setPadding( dpToPx(ScaleUtils.scaleDp(16)), dpToPx(ScaleUtils.scaleDp(15)), dpToPx(ScaleUtils.scaleDp(16)), dpToPx(ScaleUtils.scaleDp(15)) ) gravity = Gravity.CENTER_VERTICAL } val closeIcon = ImageView(context).apply { val resId = resources.getIdentifier("location_cross", "mipmap", context.packageName) if (resId != 0) setImageResource(resId) config.controller.closeImage.let { source -> imageLoader?.invoke(source) { bitmap -> bitmap?.let { setImageBitmap(it) // Re-apply color filter after setting bitmap setColorFilter(config.controller.cancelColor, android.graphics.PorterDuff.Mode.SRC_IN) } } } setColorFilter(config.controller.cancelColor, android.graphics.PorterDuff.Mode.SRC_IN) setOnClickListener { dismiss() } } val titleCenterLabel = TextView(context).apply { text = config.controller.title setTextColor(config.controller.titleColor) textSize = LayoutConstants.TextSize.TITLE typeface = Typeface.DEFAULT_BOLD gravity = Gravity.CENTER layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) } // Right placeholder to keep title centered val rightPlaceholder = ImageView(context).apply { alpha = 0f val res = android.R.drawable.ic_menu_close_clear_cancel setImageResource(res) } headerBar.addView(closeIcon, LinearLayout.LayoutParams( LayoutConstants.Dimensions.CLOSE_ICON_SIZE, LayoutConstants.Dimensions.CLOSE_ICON_SIZE )) headerBar.addView(titleCenterLabel) headerBar.addView(rightPlaceholder, LinearLayout.LayoutParams( LayoutConstants.Dimensions.CLOSE_ICON_SIZE, LayoutConstants.Dimensions.CLOSE_ICON_SIZE )) return headerBar } private fun createCalendarContainer(): FrameLayout { val calendarContainer = FrameLayout(requireContext()).apply { layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f ) } calendarView = CalendarView(requireContext()).apply { dayViewResource = R.layout.kiz_day_cell monthMargins = MarginValues(LayoutConstants.Padding.MONTH_VIEW_HORIZONTAL, 0) monthHeaderResource = R.layout.kiz_month_header orientation = RecyclerView.VERTICAL clipToPadding = false } setupCalendarView() calendarContainer.addView( calendarView, FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT ) ) return calendarContainer } private fun createBottomBar(): View { val context = requireContext() val bottomBarLayout = LinearLayout(context).apply { orientation = LinearLayout.VERTICAL setBackgroundColor(config.controller.backgroundColor) gravity = Gravity.CENTER_HORIZONTAL // No horizontal padding here so the top border can span edge-to-edge if (config.controller.showSubmitButton) { setPadding( 0, LayoutConstants.Padding.BOTTOM_BAR_VERTICAL, 0, LayoutConstants.Padding.BOTTOM_BAR_VERTICAL ) } else { setPadding(0, 0, 0, 0) } } val topBorder = View(context).apply { layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LayoutConstants.Dimensions.BORDER_THICKNESS ) setBackgroundColor(config.controller.borderColor) } if (config.controller.showSubmitButton) { confirmButton = Button(context).apply { layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LayoutConstants.Dimensions.BUTTON_HEIGHT ).apply { topMargin = LayoutConstants.Dimensions.BUTTON_TOP_MARGIN } text = config.controller.submitText setTextColor(config.controller.selectedTextColor) background = GradientDrawable().apply { cornerRadius = LayoutConstants.Dimensions.BUTTON_CORNER_RADIUS.toFloat() // Use gradient colors from config colors = config.controller.confirmGradientColors orientation = GradientDrawable.Orientation.LEFT_RIGHT } setOnClickListener { handleSubmit() } isEnabled = false // Initially disabled } } // Add border full-width bottomBarLayout.addView(topBorder) // Add content container with horizontal padding for the button val contentContainer = LinearLayout(context).apply { orientation = LinearLayout.VERTICAL setPadding( LayoutConstants.Padding.BOTTOM_BAR_HORIZONTAL, 0, LayoutConstants.Padding.BOTTOM_BAR_HORIZONTAL, 0 ) } if (config.controller.showSubmitButton) { contentContainer.addView(confirmButton) } bottomBarLayout.addView(contentContainer) return bottomBarLayout } private fun setupCalendarView() { setupCalendarBinders() setupCalendarRange() // No additional listeners required currently } private fun setupCalendarBinders() { calendarView.dayBinder = object : MonthDayBinder { override fun create(view: View): DayViewHolder { return DayViewHolder( view = view, config = config, dateConverter = dateConverter, timeZone = timeZone, isDateEnabled = ::isDateEnabled, onDateClick = ::handleDateSelection ) } override fun bind(container: DayViewHolder, data: CalendarDay) { container.apply { selectedFromDate = selection.startDate selectedToDate = selection.endDate bind(data) } } } calendarView.monthHeaderBinder = object : MonthHeaderFooterBinder { override fun create(view: View) = MonthViewHolder(view, config) override fun bind(container: MonthViewHolder, data: CalendarMonth) = container.bind(data) } } private fun setupCalendarRange() { val currentDate = LocalDate.now() val startMonth = minimumDate?.let { YearMonth.from(it) } ?: YearMonth.from(currentDate.minusYears(config.yearRangeOffset.toLong())) val endMonth = maximumDate?.let { YearMonth.from(it) } ?: YearMonth.from(currentDate.plusYears(config.yearRangeOffset.toLong())) calendarView.setup(startMonth, endMonth, DayOfWeek.MONDAY) calendarView.scrollToMonth(YearMonth.from(currentDate)) } // Removed unused scroll listeners private fun setupBottomSheetBehavior(bottomSheetDialog: BottomSheetDialog) { bottomSheetDialog.setOnShowListener { dialog -> val d = dialog as BottomSheetDialog val bottomSheet = d.findViewById(com.google.android.material.R.id.design_bottom_sheet) bottomSheet?.let { val behavior = BottomSheetBehavior.from(it) behavior.state = BottomSheetBehavior.STATE_EXPANDED behavior.skipCollapsed = true behavior.isDraggable = true } scrollToCurrentDateIfNeeded() if (config.controller.showSubmitButton) { setConfirmButtonEnabled( if (config.controller.isSingleMode) selection.startDate != null else (selection.startDate != null && selection.endDate != null) ) } updateTopHeader() } } private fun handleDateSelection(date: LocalDate) { if (!config.controller.showSubmitButton) { if (handleAutoSubmit(date)) return } if (config.controller.isSingleMode) { handleSingleSelection(date) return } val shouldComplete = processRangeSelection(date, initialValue != null) if (config.controller.showSubmitButton) { if (shouldComplete) { setConfirmButtonEnabled(true) } else { setConfirmButtonEnabled(false) } } calendarView.notifyCalendarChanged() updateTopHeader() } // region Helpers private fun handleSingleSelection(date: LocalDate) { selection = DateSelection(startDate = date, endDate = null) if (config.controller.showSubmitButton) setConfirmButtonEnabled(true) calendarView.notifyCalendarChanged() updateTopHeader() } private fun handleAutoSubmit(date: LocalDate): Boolean { if (config.controller.isSingleMode) { selection = DateSelection(startDate = date, endDate = null) onResultCallback?.invoke(LDP_Range(from = dateConverter.stringFromDate(date, timeZone), to = null)) dismiss() return true } val currentFrom = selection.startDate val currentTo = selection.endDate return when { currentFrom == null || currentTo != null -> { selection = DateSelection(startDate = date, endDate = null) calendarView.notifyCalendarChanged() updateTopHeader() true } date.isBefore(currentFrom) -> { selection = DateSelection(startDate = date, endDate = null) calendarView.notifyCalendarChanged() updateTopHeader() true } else -> { selection = DateSelection(startDate = currentFrom, endDate = date) calendarView.notifyCalendarChanged() updateTopHeader() onResultCallback?.invoke(LDP_Range(from = dateConverter.stringFromDate(currentFrom, timeZone), to = dateConverter.stringFromDate(date, timeZone))) dismiss() true } } } // endregion private fun processRangeSelection(date: LocalDate, hasInitialValue: Boolean): Boolean { var shouldComplete = false val currentFromDate = selection.startDate val currentToDate = selection.endDate when { currentFromDate == null -> { // Case 1 (iOS): No current selection, start a new range selection = DateSelection(startDate = date, endDate = null) } currentToDate != null -> { // Case 2 (iOS): Range is already complete, start a new selection selection = DateSelection(startDate = date, endDate = null) } else -> { // Case 3 (iOS): Only start date is selected, now selecting end date val startDate = currentFromDate!! if (date.isBefore(startDate)) { // If selected date is before start date, swap and complete the range selection = DateSelection(startDate = date, endDate = startDate) } else { // Otherwise, set as end date selection = DateSelection(startDate = startDate, endDate = date) } shouldComplete = true } } // Handle initial value case (iOS specific logic) // This applies if the picker opened with a pre-selected range and user is making first tap // After first tap, hasInitialValue is effectively false for subsequent taps if (!shouldComplete && hasInitialValue && currentFromDate != null && currentToDate == null) { if (date.isBefore(currentFromDate)) { // If selected date is before initial fromDate, start new range from selectedDate selection = DateSelection(startDate = date, endDate = null) shouldComplete = false // Not complete yet } else { // If selected date is after initial fromDate, complete the range selection = DateSelection(startDate = currentFromDate, endDate = date) shouldComplete = true // Complete } } return shouldComplete } private fun setupInitialSelection() { initialValue?.let { value -> val fromDate = dateConverter.dateFromString(value.from, timeZone) fromDate?.let { selection = selection.copy(startDate = it) } value.to?.let { toValue -> val toDate = dateConverter.dateFromString(toValue, timeZone) toDate?.let { selection = selection.copy(endDate = it) } } } // Defer UI updates until views are created (handled in onShow) } private fun scrollToCurrentDateIfNeeded() { val targetDate = initialValue?.let { dateConverter.dateFromString(it.from, timeZone) } ?: LocalDate.now().let { current -> maximumDate?.let { max -> if (max.isBefore(current)) max else current } ?: current } calendarView.doOnLayout { calendarView.scrollToMonth(YearMonth.from(targetDate)) } } private fun isDateEnabled(date: LocalDate): Boolean { minimumDate?.let { if (date.isBefore(it)) return false } maximumDate?.let { if (date.isAfter(it)) return false } return true } private fun cleanup() { onResultCallback = null } private fun prewarmLunarCache() { lifecycleScope.launch(Dispatchers.IO) { val startDate = LocalDate.now() val endDate = startDate.plusMonths(3) dateConverter.preloadLunarDateCache(startDate, endDate, timeZone) } } private fun handleSubmit() { if (config.controller.isSingleMode) { selection.startDate?.let { from -> onResultCallback?.invoke( LDP_Range( from = dateConverter.stringFromDate(from, timeZone), to = null ) ) dismiss() } } else { if (selection.startDate != null && selection.endDate != null) { onResultCallback?.invoke( LDP_Range( from = dateConverter.stringFromDate(selection.startDate!!, timeZone), to = dateConverter.stringFromDate(selection.endDate!!, timeZone) ) ) dismiss() } } } private fun setConfirmButtonEnabled(enabled: Boolean) { if (::confirmButton.isInitialized) { confirmButton.isEnabled = enabled confirmButton.alpha = if (enabled) 1.0f else 0.5f // Visual feedback } } private fun updateTopHeader() { // Only update header labels if showHeader is true and labels exist if (config.controller.showHeader) { val fromDateString = selection.startDate?.let { formatHeaderDate(it) } ?: (config.controller.notSelectedText ?: "Chưa chọn") departDateLabel?.text = fromDateString if (!config.controller.isSingleMode) { val toDateString = selection.endDate?.let { formatHeaderDate(it) } ?: (config.controller.notSelectedText ?: "Chưa chọn") returnDateLabel?.text = toDateString } } } private fun formatHeaderDate(date: LocalDate): String { val locale = config.calendar.locale val formatter = java.time.format.DateTimeFormatter.ofPattern("EEEE, dd MMMM yyyy", locale) return date.format(formatter) } }