package com.dmc.callcrm.ui import android.app.DatePickerDialog import android.os.Bundle import android.provider.CallLog import android.view.LayoutInflater import android.view.PopupMenu import android.view.View import android.view.ViewGroup import android.widget.GridLayout import android.widget.TextView import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import com.dmc.callcrm.R import com.dmc.callcrm.data.CallStats import com.dmc.callcrm.data.Prefs import com.dmc.callcrm.network.ApiClient import com.google.android.material.tabs.TabLayout import kotlinx.coroutines.launch import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale class AnalyticsFragment : Fragment() { private lateinit var prefs: Prefs private var from: Long = 0 private var to: Long = 0 private var rangeLabel = "This Month" private var showAnalysis = false private val typeOptions = listOf( "Incoming" to CallLog.Calls.INCOMING_TYPE, "Outgoing" to CallLog.Calls.OUTGOING_TYPE, "Missed" to CallLog.Calls.MISSED_TYPE, "Rejected" to CallLog.Calls.REJECTED_TYPE ) override fun onCreateView(i: LayoutInflater, c: ViewGroup?, s: Bundle?): View = i.inflate(R.layout.fragment_analytics, c, false) override fun onViewCreated(v: View, s: Bundle?) { prefs = Prefs(requireContext()) setThisMonthRange() val tabs = v.findViewById(R.id.tabs) tabs.addTab(tabs.newTab().setText("Summary")) tabs.addTab(tabs.newTab().setText("Analysis")) tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabSelected(t: TabLayout.Tab) { showAnalysis = t.position == 1; render(v) } override fun onTabUnselected(t: TabLayout.Tab) {} override fun onTabReselected(t: TabLayout.Tab) {} }) v.findViewById(R.id.btnFilter).setOnClickListener { showFilterDialog(v) } v.findViewById(R.id.btnRange).setOnClickListener { showRangeMenu(v, it) } v.findViewById(R.id.btnExNo).setOnClickListener { showExNoMenu(v, it) } refreshExcludedNumbersIfStale() render(v) } private fun setThisMonthRange() { val cal = Calendar.getInstance() cal.set(Calendar.DAY_OF_MONTH, 1); cal.set(Calendar.HOUR_OF_DAY, 0) cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0) from = cal.timeInMillis to = System.currentTimeMillis() } private fun showFilterDialog(v: View) { val checked = typeOptions.map { !prefs.analyticsDeselectedTypes.contains(it.second.toString()) }.toBooleanArray() val labels = typeOptions.map { it.first }.toTypedArray() androidx.appcompat.app.AlertDialog.Builder(requireContext()) .setTitle("Filter call types") .setMultiChoiceItems(labels, checked) { _, which, isChecked -> checked[which] = isChecked } .setPositiveButton("Apply") { _, _ -> val deselected = typeOptions.filterIndexed { i, _ -> !checked[i] }.map { it.second.toString() }.toSet() prefs.analyticsDeselectedTypes = deselected updateFilterBadge(v) render(v) } .setNegativeButton("Cancel", null) .show() } private fun updateFilterBadge(v: View) { val n = prefs.analyticsDeselectedTypes.size + if (prefs.analyticsExcludeExcluded) 1 else 0 v.findViewById(R.id.btnFilter).text = if (n > 0) "⏷ Filter ($n)" else "⏷ Filter" } private fun showRangeMenu(v: View, anchor: View) { val menu = PopupMenu(requireContext(), anchor) listOf("Today", "Yesterday", "This Week", "Last Week", "This Month", "Last Month", "Custom Range").forEach { menu.menu.add(it) } menu.setOnMenuItemClickListener { mi -> when (mi.title) { "Custom Range" -> pickCustomRange(v) else -> { applyPreset(mi.title.toString()); render(v) } } true } menu.show() } private fun applyPreset(name: String) { val cal = Calendar.getInstance() rangeLabel = name when (name) { "Today" -> { cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0) from = cal.timeInMillis; to = System.currentTimeMillis() } "Yesterday" -> { cal.add(Calendar.DAY_OF_MONTH, -1) cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0) from = cal.timeInMillis cal.set(Calendar.HOUR_OF_DAY, 23); cal.set(Calendar.MINUTE, 59); cal.set(Calendar.SECOND, 59) to = cal.timeInMillis } "This Week" -> { cal.set(Calendar.DAY_OF_WEEK, cal.firstDayOfWeek) cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0) from = cal.timeInMillis; to = System.currentTimeMillis() } "Last Week" -> { cal.add(Calendar.WEEK_OF_YEAR, -1) cal.set(Calendar.DAY_OF_WEEK, cal.firstDayOfWeek) cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0) from = cal.timeInMillis cal.add(Calendar.DAY_OF_MONTH, 6) cal.set(Calendar.HOUR_OF_DAY, 23); cal.set(Calendar.MINUTE, 59); cal.set(Calendar.SECOND, 59) to = cal.timeInMillis } "Last Month" -> { cal.add(Calendar.MONTH, -1) cal.set(Calendar.DAY_OF_MONTH, 1) cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0) from = cal.timeInMillis cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH)) cal.set(Calendar.HOUR_OF_DAY, 23); cal.set(Calendar.MINUTE, 59); cal.set(Calendar.SECOND, 59) to = cal.timeInMillis } else -> setThisMonthRange() // "This Month" } } private fun pickCustomRange(v: View) { val cal = Calendar.getInstance() DatePickerDialog(requireContext(), { _, y1, m1, d1 -> val fromCal = Calendar.getInstance().apply { set(y1, m1, d1, 0, 0, 0) } DatePickerDialog(requireContext(), { _, y2, m2, d2 -> val toCal = Calendar.getInstance().apply { set(y2, m2, d2, 23, 59, 59) } from = fromCal.timeInMillis; to = toCal.timeInMillis rangeLabel = "Custom" render(v) }, cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH)).show() }, cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH)).show() } private fun showExNoMenu(v: View, anchor: View) { val menu = PopupMenu(requireContext(), anchor) menu.menu.add("No — include excluded numbers") menu.menu.add("Yes — leave excluded numbers out") menu.setOnMenuItemClickListener { mi -> val exclude = mi.title.toString().startsWith("Yes") prefs.analyticsExcludeExcluded = exclude v.findViewById(R.id.btnExNo).text = "Ex. No.: ${if (exclude) "Yes" else "No"} ⌄" updateFilterBadge(v) render(v) true } menu.show() } private fun refreshExcludedNumbersIfStale() { if (!prefs.isRegistered) return val staleAfterMs = 24 * 60 * 60 * 1000L if (System.currentTimeMillis() - prefs.excludedLastFetch < staleAfterMs) return viewLifecycleOwner.lifecycleScope.launch { try { val resp = ApiClient.service.excludedNumbers(prefs.bearer()) val nums = resp.body()?.numbers if (resp.isSuccessful && nums != null) { prefs.excludedNumberDigits = nums.toSet() prefs.excludedLastFetch = System.currentTimeMillis() view?.let { render(it) } } } catch (_: Exception) { /* keep using the last cached list */ } } } private fun render(v: View) { val df = SimpleDateFormat("dd-MMM-yyyy", Locale.getDefault()) v.findViewById(R.id.tvRange).text = "${df.format(from)} – ${df.format(to)} ($rangeLabel)" v.findViewById(R.id.btnRange).text = "$rangeLabel ⌄" updateFilterBadge(v) val excludeDigits = if (prefs.analyticsExcludeExcluded) prefs.excludedNumberDigits else emptySet() val stats = CallStats(requireContext()) val grid = v.findViewById(R.id.grid) val table = v.findViewById(R.id.analysisTable) if (!showAnalysis) { grid.visibility = View.VISIBLE table.visibility = View.GONE renderSummary(grid, stats.analytics(from, to, excludeDigits, prefs.analyticsDeselectedTypes)) } else { grid.visibility = View.GONE table.visibility = View.VISIBLE renderAnalysis(table, stats.dayBreakdown(from, to)) } } private fun renderSummary(grid: GridLayout, a: CallStats.Analytics) { grid.removeAllViews() data class Card(val label: String, val count: Int, val dur: Long?) val cards = listOf( Card("Total Phone Calls", a.total, a.totalDur), Card("Incoming Calls", a.incoming, a.incomingDur), Card("Outgoing Calls", a.outgoing, a.outgoingDur), Card("Missed Calls", a.missed, 0), Card("Rejected Calls", a.rejected, 0), Card("Never Attended", a.neverAttended, null) ) val inflater = LayoutInflater.from(context) val colW = resources.displayMetrics.widthPixels / 2 - dp(24) for (card in cards) { val cv = inflater.inflate(R.layout.item_stat_card, grid, false) cv.findViewById(R.id.label).text = card.label cv.findViewById(R.id.count).text = card.count.toString() val durTv = cv.findViewById(R.id.dur) if (card.dur != null) durTv.text = fmt(card.dur) else durTv.visibility = View.GONE val lp = GridLayout.LayoutParams().apply { width = colW } grid.addView(cv, lp) } } private fun renderAnalysis(container: ViewGroup, rows: List) { container.removeAllViews() val inflater = LayoutInflater.from(context) fun rowView(date: String, total: String, inc: String, out: String, missed: String, header: Boolean): View { val row = android.widget.LinearLayout(context).apply { orientation = android.widget.LinearLayout.HORIZONTAL setPadding(dp(8), dp(10), dp(8), dp(10)) setBackgroundColor(if (header) 0xFFE2E8F0.toInt() else 0xFFFFFFFF.toInt()) } listOf(date to 2f, total to 1f, inc to 1f, out to 1f, missed to 1f).forEach { (text, weight) -> row.addView(TextView(context).apply { this.text = text setTextColor(if (header) 0xFF334155.toInt() else 0xFF0F172A.toInt()) textSize = 12f layoutParams = android.widget.LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, weight) }) } return row } container.addView(rowView("Date", "Total", "In", "Out", "Missed", header = true)) if (rows.isEmpty()) { container.addView(TextView(context).apply { text = "No calls in this range"; setPadding(dp(8), dp(16), dp(8), dp(16)); setTextColor(0xFF64748B.toInt()) }) } rows.forEach { d -> container.addView(rowView(d.label, d.total.toString(), d.incoming.toString(), d.outgoing.toString(), d.missed.toString(), header = false)) } } private fun fmt(s: Long): String { val h = s / 3600; val m = (s % 3600) / 60; val sec = s % 60 return "%dh %dm %ds".format(h, m, sec) } private fun dp(x: Int) = (x * resources.displayMetrics.density).toInt() }