App con inicio y base de datos pero sin direccion

This commit is contained in:
JaredAyala
2026-05-22 22:09:49 -06:00
parent 2cd7fe285d
commit f914682198
42 changed files with 1894 additions and 50 deletions

View File

@@ -1,22 +1,18 @@
plugins {
alias(libs.plugins.android.application)
id("com.google.gms.google-services")
}
android {
namespace = "com.example.basurapp"
compileSdk {
version = release(36) {
minorApiLevel = 1
}
}
compileSdk = 34
defaultConfig {
applicationId = "com.example.basurapp"
minSdk = 24
targetSdk = 36
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -29,19 +25,38 @@ android {
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
// AndroidX (versiones compatibles con SDK 34)
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.activity:activity-ktx:1.8.2")
implementation("androidx.fragment:fragment-ktx:1.6.2")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.recyclerview:recyclerview:1.3.2")
// Firebase
implementation(platform("com.google.firebase:firebase-bom:32.7.4"))
implementation("com.google.firebase:firebase-auth-ktx")
implementation("com.google.firebase:firebase-firestore-ktx")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}

29
app/google-services.json Normal file
View File

@@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "168486816372",
"project_id": "hackon-4abe2",
"storage_bucket": "hackon-4abe2.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:168486816372:android:aff8739eafd01d6e025f21",
"android_client_info": {
"package_name": "com.example.basurapp"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyB-UnjmxI61A4V4VxsJ1ukIYt0idhfocu8"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View File

@@ -2,6 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -10,9 +13,13 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.BasurApp">
android:theme="@style/Theme.BasurApp"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:name=".RegisterActivity"
android:exported="false" />
<activity
android:name=".LoginActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -20,6 +27,9 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:exported="false" />
</application>
</manifest>

View File

@@ -0,0 +1,88 @@
package com.example.basurapp
import android.content.Intent
import android.os.Bundle
import android.util.Patterns
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.basurapp.databinding.ActivityLoginBinding
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.firestore.FirebaseFirestore
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
private val auth: FirebaseAuth by lazy { FirebaseAuth.getInstance() }
private val db: FirebaseFirestore by lazy { FirebaseFirestore.getInstance() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
// Padding para system bars
ViewCompat.setOnApplyWindowInsetsListener(binding.main) { v, insets ->
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(bars.left, bars.top, bars.right, bars.bottom)
insets
}
// Si ya hay sesión activa, ir directo al Main
if (auth.currentUser != null) {
goToMain()
return
}
binding.tvCreateAccount.setOnClickListener {
startActivity(Intent(this, RegisterActivity::class.java))
}
binding.btnEnter.setOnClickListener {
attemptLogin()
}
}
private fun attemptLogin() {
val email = binding.etEmail.text?.toString()?.trim().orEmpty()
val password = binding.etPassword.text?.toString().orEmpty()
// Validaciones
if (email.isEmpty() || password.isEmpty()) {
toast(getString(R.string.error_empty_fields))
return
}
if (!Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
binding.tilEmail.error = getString(R.string.error_invalid_email)
return
}
binding.tilEmail.error = null
setLoading(true)
auth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener { task ->
if (task.isSuccessful) {
goToMain()
} else {
setLoading(false)
toast(task.exception?.localizedMessage ?: "Error al iniciar sesión")
}
}
}
private fun goToMain() {
startActivity(Intent(this, MainActivity::class.java))
finish()
}
private fun setLoading(loading: Boolean) {
binding.progressBar.visibility = if (loading) View.VISIBLE else View.GONE
binding.btnEnter.isEnabled = !loading
}
private fun toast(msg: String) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
}

View File

@@ -1,20 +1,136 @@
package com.example.basurapp
import android.content.Intent
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import android.view.MenuItem
import android.widget.TextView
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.GravityCompat
import androidx.fragment.app.Fragment
import com.example.basurapp.databinding.ActivityMainBinding
import com.example.basurapp.fragments.ControlFragment
import com.example.basurapp.fragments.HomeFragment
import com.example.basurapp.fragments.InfoDesperdiciosFragment
import com.example.basurapp.fragments.ReportarFragment
import com.example.basurapp.models.User
import com.google.android.material.navigation.NavigationView
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.firestore.FirebaseFirestore
class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
private lateinit var binding: ActivityMainBinding
private lateinit var toggle: ActionBarDrawerToggle
private val auth: FirebaseAuth by lazy { FirebaseAuth.getInstance() }
private val db: FirebaseFirestore by lazy { FirebaseFirestore.getInstance() }
private var currentUser: User? = null
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
supportActionBar?.title = ""
// Botón hamburguesa
toggle = ActionBarDrawerToggle(
this,
binding.drawerLayout,
binding.toolbar,
R.string.drawer_open,
R.string.drawer_close
)
binding.drawerLayout.addDrawerListener(toggle)
toggle.syncState()
binding.navigationView.setNavigationItemSelectedListener(this)
loadCurrentUser()
}
private fun loadCurrentUser() {
val uid = auth.currentUser?.uid
if (uid == null) {
goToLogin()
return
}
db.collection("users").document(uid).get()
.addOnSuccessListener { doc ->
val user = doc.toObject(User::class.java)
if (user == null) {
goToLogin()
return@addOnSuccessListener
}
currentUser = user
setupForRole(user)
}
.addOnFailureListener {
goToLogin()
}
}
private fun setupForRole(user: User) {
binding.navigationView.menu.clear()
if (user.role == User.ROLE_WORKER) {
binding.navigationView.inflateMenu(R.menu.drawer_menu_worker)
loadFragment(ControlFragment.newInstance(ControlFragment.MODE_USER_REPORTS))
} else {
binding.navigationView.inflateMenu(R.menu.drawer_menu_user)
loadFragment(HomeFragment())
}
val header = binding.navigationView.getHeaderView(0)
header.findViewById<TextView>(R.id.tvUserName)?.text = user.name
header.findViewById<TextView>(R.id.tvUserEmail)?.text = user.email
}
override fun onNavigationItemSelected(item: MenuItem): Boolean {
val fragment: Fragment? = when (item.itemId) {
R.id.nav_home -> HomeFragment()
R.id.nav_info -> InfoDesperdiciosFragment()
R.id.nav_report -> ReportarFragment()
R.id.nav_user_reports -> ControlFragment.newInstance(ControlFragment.MODE_USER_REPORTS)
R.id.nav_worker_reports -> ControlFragment.newInstance(ControlFragment.MODE_WORKER_REPORTS)
R.id.nav_logout -> {
auth.signOut()
goToLogin()
null
}
R.id.nav_exit -> {
finishAffinity()
null
}
else -> null
}
fragment?.let { loadFragment(it) }
binding.drawerLayout.closeDrawer(GravityCompat.START)
return true
}
private fun loadFragment(fragment: Fragment) {
supportFragmentManager.beginTransaction()
.replace(R.id.fragmentContainer, fragment)
.commit()
}
private fun goToLogin() {
val intent = Intent(this, LoginActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
finish()
}
override fun onBackPressed() {
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
binding.drawerLayout.closeDrawer(GravityCompat.START)
} else {
super.onBackPressed()
}
}
fun getCurrentUser(): User? = currentUser
}

View File

@@ -0,0 +1,111 @@
package com.example.basurapp
import android.content.Intent
import android.os.Bundle
import android.util.Patterns
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.basurapp.databinding.ActivityRegisterBinding
import com.example.basurapp.models.User
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.firestore.FirebaseFirestore
class RegisterActivity : AppCompatActivity() {
private lateinit var binding: ActivityRegisterBinding
private val auth: FirebaseAuth by lazy { FirebaseAuth.getInstance() }
private val db: FirebaseFirestore by lazy { FirebaseFirestore.getInstance() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityRegisterBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(binding.main) { v, insets ->
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(bars.left, bars.top, bars.right, bars.bottom)
insets
}
binding.btnRegister.setOnClickListener {
attemptRegister()
}
}
private fun attemptRegister() {
val name = binding.etName.text?.toString()?.trim().orEmpty()
val email = binding.etEmail.text?.toString()?.trim().orEmpty()
val password = binding.etPassword.text?.toString().orEmpty()
val confirmPassword = binding.etConfirmPassword.text?.toString().orEmpty()
val role = if (binding.rbWorker.isChecked) User.ROLE_WORKER else User.ROLE_USER
// Validaciones
if (name.isEmpty() || email.isEmpty() || password.isEmpty() || confirmPassword.isEmpty()) {
toast(getString(R.string.error_empty_fields))
return
}
if (!Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
binding.tilEmail.error = getString(R.string.error_invalid_email)
return
}
binding.tilEmail.error = null
if (password.length < 6) {
binding.tilPassword.error = getString(R.string.error_short_password)
return
}
binding.tilPassword.error = null
if (password != confirmPassword) {
binding.tilConfirmPassword.error = getString(R.string.error_passwords_dont_match)
return
}
binding.tilConfirmPassword.error = null
setLoading(true)
auth.createUserWithEmailAndPassword(email, password)
.addOnCompleteListener { task ->
if (task.isSuccessful) {
val uid = auth.currentUser?.uid ?: return@addOnCompleteListener
val user = User(
id = uid,
name = name,
email = email,
role = role
)
// Guardar en Firestore
db.collection("users").document(uid).set(user)
.addOnSuccessListener {
toast(getString(R.string.success_register))
goToMain()
}
.addOnFailureListener { e ->
setLoading(false)
toast(e.localizedMessage ?: "Error guardando usuario")
}
} else {
setLoading(false)
toast(task.exception?.localizedMessage ?: "Error al registrar")
}
}
}
private fun goToMain() {
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
finish()
}
private fun setLoading(loading: Boolean) {
binding.progressBar.visibility = if (loading) View.VISIBLE else View.GONE
binding.btnRegister.isEnabled = !loading
}
private fun toast(msg: String) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
}

View File

@@ -0,0 +1,45 @@
package com.example.basurapp.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.basurapp.databinding.ItemReportBinding
import com.example.basurapp.models.Report
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class ReportsAdapter : ListAdapter<Report, ReportsAdapter.ReportViewHolder>(DIFF) {
private val dateFormat = SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.getDefault())
inner class ReportViewHolder(val binding: ItemReportBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(report: Report) {
binding.tvSubject.text = report.subject
binding.tvMessage.text = report.message
binding.tvDate.text = dateFormat.format(Date(report.timestamp))
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReportViewHolder {
val binding = ItemReportBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return ReportViewHolder(binding)
}
override fun onBindViewHolder(holder: ReportViewHolder, position: Int) {
holder.bind(getItem(position))
}
companion object {
val DIFF = object : DiffUtil.ItemCallback<Report>() {
override fun areItemsTheSame(oldItem: Report, newItem: Report) = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Report, newItem: Report) = oldItem == newItem
}
}
}

View File

@@ -0,0 +1,42 @@
package com.example.basurapp.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.basurapp.databinding.ItemRouteBinding
import com.example.basurapp.models.Route
class RoutesAdapter(
private val onClick: (Route) -> Unit
) : ListAdapter<Route, RoutesAdapter.RouteViewHolder>(DIFF) {
inner class RouteViewHolder(val binding: ItemRouteBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(route: Route) {
binding.tvRouteName.text = route.name
binding.truckProgress.setRoute(route.stops.size, route.truckPosition)
binding.root.setOnClickListener { onClick(route) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RouteViewHolder {
val binding = ItemRouteBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return RouteViewHolder(binding)
}
override fun onBindViewHolder(holder: RouteViewHolder, position: Int) {
holder.bind(getItem(position))
}
companion object {
val DIFF = object : DiffUtil.ItemCallback<Route>() {
override fun areItemsTheSame(oldItem: Route, newItem: Route) = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Route, newItem: Route) = oldItem == newItem
}
}
}

View File

@@ -0,0 +1,69 @@
package com.example.basurapp.dialogs
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.example.basurapp.R
import com.example.basurapp.databinding.DialogRouteInfoBinding
import com.example.basurapp.models.Route
import com.example.basurapp.models.RouteStop
class RouteInfoDialog : DialogFragment() {
private var route: Route? = null
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
route = arguments?.getSerializable(ARG_ROUTE) as? Route
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val binding = DialogRouteInfoBinding.inflate(LayoutInflater.from(requireContext()))
val r = route
if (r != null) {
binding.tvRouteTitle.text = "${getString(R.string.route_label_prefix)} ${r.name}"
// Agregar dinámicamente cada parada
binding.layoutStops.removeAllViews()
r.stops.forEach { stop ->
binding.layoutStops.addView(buildStopRow(stop))
}
}
binding.btnAccept.setOnClickListener { dismiss() }
binding.btnExit.setOnClickListener { dismiss() }
return AlertDialog.Builder(requireContext())
.setView(binding.root)
.create()
}
private fun buildStopRow(stop: RouteStop): TextView {
return TextView(requireContext()).apply {
text = "Paso por ${stop.name} a las ${stop.time}"
textSize = 14f
setTextColor(resources.getColor(R.color.gray_900, null))
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply { topMargin = 12 }
}
}
companion object {
private const val ARG_ROUTE = "route"
fun newInstance(route: Route): RouteInfoDialog {
return RouteInfoDialog().apply {
arguments = Bundle().apply {
putSerializable(ARG_ROUTE, route)
}
}
}
}
}

View File

@@ -0,0 +1,88 @@
package com.example.basurapp.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.basurapp.adapters.ReportsAdapter
import com.example.basurapp.databinding.FragmentControlBinding
import com.example.basurapp.models.Report
import com.example.basurapp.models.User
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.Query
class ControlFragment : Fragment() {
private var _binding: FragmentControlBinding? = null
private val binding get() = _binding!!
private val db: FirebaseFirestore by lazy { FirebaseFirestore.getInstance() }
private lateinit var reportsAdapter: ReportsAdapter
private var mode: String = MODE_USER_REPORTS
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mode = arguments?.getString(ARG_MODE) ?: MODE_USER_REPORTS
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
_binding = FragmentControlBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
reportsAdapter = ReportsAdapter()
binding.rvReports.layoutManager = LinearLayoutManager(requireContext())
binding.rvReports.adapter = reportsAdapter
loadReports()
}
private fun loadReports() {
val roleFilter = if (mode == MODE_WORKER_REPORTS) User.ROLE_WORKER else User.ROLE_USER
db.collection("reports")
.whereEqualTo("userRole", roleFilter)
.orderBy("timestamp", Query.Direction.DESCENDING)
.get()
.addOnSuccessListener { result ->
val reports = result.documents.mapNotNull { it.toObject(Report::class.java) }
if (reports.isEmpty()) {
binding.tvEmpty.visibility = View.VISIBLE
binding.rvReports.visibility = View.GONE
} else {
binding.tvEmpty.visibility = View.GONE
binding.rvReports.visibility = View.VISIBLE
reportsAdapter.submitList(reports)
}
}
.addOnFailureListener {
binding.tvEmpty.visibility = View.VISIBLE
binding.rvReports.visibility = View.GONE
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
const val MODE_USER_REPORTS = "user_reports"
const val MODE_WORKER_REPORTS = "worker_reports"
private const val ARG_MODE = "mode"
fun newInstance(mode: String): ControlFragment {
return ControlFragment().apply {
arguments = Bundle().apply { putString(ARG_MODE, mode) }
}
}
}
}

View File

@@ -0,0 +1,117 @@
package com.example.basurapp.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.basurapp.adapters.RoutesAdapter
import com.example.basurapp.databinding.HomeFragmentBinding
import com.example.basurapp.dialogs.RouteInfoDialog
import com.example.basurapp.models.Route
import com.example.basurapp.models.RouteStop
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.firestore.FirebaseFirestore
class HomeFragment : Fragment() {
private var _binding: HomeFragmentBinding? = null
private val binding get() = _binding!!
private val auth: FirebaseAuth by lazy { FirebaseAuth.getInstance() }
private val db: FirebaseFirestore by lazy { FirebaseFirestore.getInstance() }
private lateinit var routesAdapter: RoutesAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
_binding = HomeFragmentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Adapter de rutas
routesAdapter = RoutesAdapter { route ->
RouteInfoDialog.newInstance(route).show(parentFragmentManager, "route_info")
}
binding.rvRoutes.layoutManager = LinearLayoutManager(requireContext())
binding.rvRoutes.adapter = routesAdapter
binding.btnAddAddress.setOnClickListener {
Toast.makeText(requireContext(), "Función Agregar dirección (pendiente)", Toast.LENGTH_SHORT).show()
// TODO: Abrir Activity o dialog para agregar dirección
}
checkUserAddress()
}
private fun checkUserAddress() {
val uid = auth.currentUser?.uid ?: return
db.collection("addresses")
.whereEqualTo("userId", uid)
.get()
.addOnSuccessListener { result ->
if (result.isEmpty) {
showNoAddressState()
} else {
showRoutesState()
}
}
.addOnFailureListener {
showNoAddressState()
}
}
private fun showNoAddressState() {
binding.layoutNoAddress.visibility = View.VISIBLE
binding.rvRoutes.visibility = View.GONE
}
private fun showRoutesState() {
binding.layoutNoAddress.visibility = View.GONE
binding.rvRoutes.visibility = View.VISIBLE
loadRoutes()
}
private fun loadRoutes() {
// Por ahora datos de ejemplo (después se conectan a Firestore)
val sampleRoutes = listOf(
Route(
id = "1",
name = "Dirección 1",
neighborhood = "Centro",
stops = listOf(
RouteStop("Inicio", "07:00"),
RouteStop("Calle 5 de Mayo", "07:30"),
RouteStop("Plaza Central", "08:00"),
RouteStop("Av. Juárez", "08:30"),
RouteStop("Final de ruta", "09:00")
),
truckPosition = 2
),
Route(
id = "2",
name = "Dirección 1",
neighborhood = "Zona Norte",
stops = listOf(
RouteStop("Inicio", "10:00"),
RouteStop("Mercado Norte", "10:30"),
RouteStop("Parque Hidalgo", "11:00"),
RouteStop("Final de ruta", "11:30")
),
truckPosition = 1
)
)
routesAdapter.submitList(sampleRoutes)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,26 @@
package com.example.basurapp.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.example.basurapp.databinding.FragmentInfoDesperdiciosBinding
class InfoDesperdiciosFragment : Fragment() {
private var _binding: FragmentInfoDesperdiciosBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
_binding = FragmentInfoDesperdiciosBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,80 @@
package com.example.basurapp.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.example.basurapp.MainActivity
import com.example.basurapp.R
import com.example.basurapp.databinding.FragmentReportarBinding
import com.example.basurapp.models.Report
import com.example.basurapp.models.User
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.firestore.FirebaseFirestore
class ReportarFragment : Fragment() {
private var _binding: FragmentReportarBinding? = null
private val binding get() = _binding!!
private val auth: FirebaseAuth by lazy { FirebaseAuth.getInstance() }
private val db: FirebaseFirestore by lazy { FirebaseFirestore.getInstance() }
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
_binding = FragmentReportarBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.btnSend.setOnClickListener {
sendReport()
}
}
private fun sendReport() {
val subject = binding.etSubject.text?.toString()?.trim().orEmpty()
val message = binding.etMessage.text?.toString()?.trim().orEmpty()
if (subject.isEmpty() || message.isEmpty()) {
Toast.makeText(requireContext(), getString(R.string.error_empty_fields), Toast.LENGTH_SHORT).show()
return
}
val uid = auth.currentUser?.uid ?: return
val userRole = (activity as? MainActivity)?.getCurrentUser()?.role ?: User.ROLE_USER
val reportId = db.collection("reports").document().id
val report = Report(
id = reportId,
userId = uid,
userRole = userRole,
subject = subject,
message = message,
timestamp = System.currentTimeMillis()
)
binding.btnSend.isEnabled = false
db.collection("reports").document(reportId).set(report)
.addOnSuccessListener {
Toast.makeText(requireContext(), getString(R.string.success_report), Toast.LENGTH_SHORT).show()
binding.etSubject.setText("")
binding.etMessage.setText("")
binding.btnSend.isEnabled = true
}
.addOnFailureListener { e ->
binding.btnSend.isEnabled = true
Toast.makeText(requireContext(), e.localizedMessage ?: "Error", Toast.LENGTH_SHORT).show()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,14 @@
package com.example.basurapp.models
/**
* Dirección de un usuario. Colección "addresses" en Firestore.
*/
data class Address(
val id: String = "",
val userId: String = "",
val street: String = "",
val number: String = "",
val neighborhood: String = "",
val city: String = "",
val reference: String = ""
)

View File

@@ -0,0 +1,14 @@
package com.example.basurapp.models
/**
* Reporte hecho por un usuario o trabajador.
* Colección "reports" en Firestore.
*/
data class Report(
val id: String = "",
val userId: String = "",
val userRole: String = "", // "user" | "worker"
val subject: String = "",
val message: String = "",
val timestamp: Long = System.currentTimeMillis()
)

View File

@@ -0,0 +1,16 @@
package com.example.basurapp.models
import java.io.Serializable
data class Route(
val id: String = "",
val name: String = "",
val neighborhood: String = "",
val stops: List<RouteStop> = emptyList(),
val truckPosition: Int = 0
) : Serializable
data class RouteStop(
val name: String = "",
val time: String = ""
) : Serializable

View File

@@ -0,0 +1,18 @@
package com.example.basurapp.models
/**
* Modelo de usuario. Se guarda en Firestore en la colección "users".
* El id corresponde al UID de Firebase Auth.
*/
data class User(
val id: String = "",
val name: String = "",
val email: String = "",
val role: String = ROLE_USER,
val photoUrl: String = ""
) {
companion object {
const val ROLE_USER = "user"
const val ROLE_WORKER = "worker"
}
}

View File

@@ -0,0 +1,83 @@
package com.example.basurapp.views
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import com.example.basurapp.R
class TruckProgressView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var stopCount: Int = 4
private var truckPosition: Int = 0
private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLACK
strokeWidth = 6f
}
private val stopPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLACK
style = Paint.Style.FILL
}
private val truckBodyPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = context.getColor(R.color.green_primary)
style = Paint.Style.FILL
}
private val truckCabinPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = context.getColor(R.color.green_dark)
style = Paint.Style.FILL
}
private val wheelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLACK
style = Paint.Style.FILL
}
fun setRoute(stops: Int, position: Int) {
stopCount = stops.coerceAtLeast(2)
truckPosition = position.coerceIn(0, stopCount - 1)
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val w = width.toFloat()
val h = height.toFloat()
val padding = 40f
val lineY = h / 2 + 20
// Línea horizontal
canvas.drawLine(padding, lineY, w - padding, lineY, linePaint)
// Paradas (círculos)
val step = (w - padding * 2) / (stopCount - 1)
for (i in 0 until stopCount) {
val cx = padding + step * i
canvas.drawCircle(cx, lineY, 14f, stopPaint)
}
// Camión sobre la parada actual
val truckX = padding + step * truckPosition
drawTruck(canvas, truckX, lineY - 30f)
}
private fun drawTruck(canvas: Canvas, x: Float, y: Float) {
// Caja del camión
canvas.drawRect(x - 30f, y - 20f, x + 10f, y + 10f, truckBodyPaint)
// Cabina
canvas.drawRect(x + 10f, y - 10f, x + 30f, y + 10f, truckCabinPaint)
// Ruedas
canvas.drawCircle(x - 20f, y + 12f, 6f, wheelPaint)
canvas.drawCircle(x + 20f, y + 12f, 6f, wheelPaint)
}
}

View File

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M7,11v2h10v-2L7,11zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M440,680L520,680L520,520L680,520L680,440L520,440L520,280L440,280L440,440L280,440L280,520L440,520L440,680ZM200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760ZM200,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200Z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M18.92,6.01C18.72,5.42 18.16,5 17.5,5h-11c-0.66,0 -1.21,0.42 -1.42,1.01L3,12v8c0,0.55 0.45,1 1,1h1c0.55,0 1,-0.45 1,-1v-1h12v1c0,0.55 0.45,1 1,1h1c0.55,0 1,-0.45 1,-1v-8l-2.08,-5.99zM6.5,16c-0.83,0 -1.5,-0.67 -1.5,-1.5S5.67,13 6.5,13s1.5,0.67 1.5,1.5S7.33,16 6.5,16zM17.5,16c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM5,11l1.5,-4.5h11L19,11L5,11z"/>
</vector>

View File

@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:padding="24dp"
tools:context=".LoginActivity">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:text="@string/login_title"
android:textColor="@color/gray_900"
android:textSize="20sp"
android:textStyle="bold"
android:letterSpacing="0.05"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/ivLogo"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginTop="32dp"
android:background="@color/gray_300"
android:contentDescription="@string/app_name"
android:src="@mipmap/ic_launcher_round"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvTitle" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilEmail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:hint="@string/hint_email"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ivLogo">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilPassword"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="@string/hint_password"
app:endIconMode="password_toggle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tilEmail">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/tvCreateAccount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:padding="8dp"
android:text="@string/create_account"
android:textColor="@color/green_primary"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tilPassword" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnEnter"
android:layout_width="0dp"
android:layout_height="56dp"
android:layout_marginTop="32dp"
android:text="@string/btn_enter"
android:textSize="16sp"
app:cornerRadius="28dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvCreateAccount" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,19 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello Adri!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/gray_200">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/gray_200"
app:titleTextColor="@color/gray_900" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/navigationView"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="@color/gray_100"
app:headerLayout="@layout/nav_header"
app:menu="@menu/drawer_menu_user" />
</androidx.drawerlayout.widget.DrawerLayout>

View File

@@ -0,0 +1,173 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:fillViewport="true"
tools:context=".RegisterActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="24dp">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/register_title"
android:textColor="@color/gray_900"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/ivLogo"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="24dp"
android:background="@color/gray_300"
android:contentDescription="@string/app_name"
android:src="@mipmap/ic_launcher_round"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvTitle" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:hint="@string/hint_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ivLogo">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPersonName" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilEmail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="@string/hint_email"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tilName">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilPassword"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="@string/hint_password"
app:endIconMode="password_toggle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tilEmail">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilConfirmPassword"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="@string/hint_confirm_password"
app:endIconMode="password_toggle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tilPassword">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etConfirmPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/tvRoleLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/select_role"
android:textColor="@color/gray_700"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tilConfirmPassword" />
<RadioGroup
android:id="@+id/rgRole"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvRoleLabel">
<RadioButton
android:id="@+id/rbUser"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:checked="true"
android:text="@string/role_user" />
<RadioButton
android:id="@+id/rbWorker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/role_worker" />
</RadioGroup>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnRegister"
android:layout_width="0dp"
android:layout_height="56dp"
android:layout_marginTop="32dp"
android:layout_marginBottom="24dp"
android:text="@string/btn_enter"
android:textSize="16sp"
app:cornerRadius="28dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/rgRole" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/btnRegister"
app:layout_constraintEnd_toEndOf="@id/btnRegister"
app:layout_constraintStart_toStartOf="@id/btnRegister"
app:layout_constraintTop_toTopOf="@id/btnRegister"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/gray_200"
android:padding="20dp">
<TextView
android:id="@+id/tvRouteTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/gray_900"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/layoutStops"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@id/tvRouteTitle" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnAccept"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="8dp"
android:text="@string/btn_accept"
app:cornerRadius="24dp"
app:layout_constraintEnd_toStartOf="@id/btnExit"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/layoutStops" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnExit"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginTop="24dp"
android:layout_marginStart="8dp"
android:text="@string/btn_exit"
app:cornerRadius="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/btnAccept"
app:layout_constraintTop_toBottomOf="@id/layoutStops" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/control_title"
android:textColor="@color/gray_900"
android:textSize="18sp"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvReports"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="48dp"
android:clipToPadding="false"
tools:listitem="@layout/item_report" />
<TextView
android:id="@+id/tvEmpty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="No hay reportes"
android:textColor="@color/gray_500"
android:textSize="14sp"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>

View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/info_waste_title"
android:textColor="@color/gray_900"
android:textSize="18sp"
android:textStyle="bold" />
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_marginTop="16dp"
android:background="@color/green_primary" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Tipos de desperdicios"
android:textColor="@color/green_primary"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:lineSpacingExtra="4dp"
android:text="• Orgánico: restos de comida, cáscaras, hojas\n• Inorgánico reciclable: papel, cartón, plástico, vidrio, metal\n• Inorgánico no reciclable: pañales, papel sanitario\n• Peligroso: pilas, baterías, medicamentos, electrónicos"
android:textColor="@color/gray_900"
android:textSize="14sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Cómo separar correctamente"
android:textColor="@color/green_primary"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:lineSpacingExtra="4dp"
android:text="Utiliza bolsas o contenedores separados para cada tipo de residuo. Limpia y seca los reciclables antes de desecharlos. No mezcles residuos peligrosos con la basura común."
android:textColor="@color/gray_900"
android:textSize="14sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Consejos para reducir"
android:textColor="@color/green_primary"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="24dp"
android:lineSpacingExtra="4dp"
android:text="• Usa bolsas reutilizables\n• Compra a granel cuando puedas\n• Composta los residuos orgánicos\n• Repara antes de reemplazar"
android:textColor="@color/gray_900"
android:textSize="14sp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/report_title"
android:textColor="@color/gray_900"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilSubject"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:hint="@string/hint_subject">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etSubject"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/hint_message">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etMessage"
android:layout_width="match_parent"
android:layout_height="180dp"
android:gravity="top|start"
android:inputType="textMultiLine"
android:scrollbars="vertical" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSend"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="24dp"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:text="@string/btn_send"
app:cornerRadius="24dp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white">
<!-- Estado "sin dirección" -->
<LinearLayout
android:id="@+id/layoutNoAddress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/no_address_title"
android:textColor="@color/gray_900"
android:textSize="16sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/no_routes"
android:textColor="@color/gray_900"
android:textSize="16sp"
android:textStyle="bold" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnAddAddress"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginTop="24dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/add_address"
app:cornerRadius="24dp" />
</LinearLayout>
<!-- Lista de rutas -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvRoutes"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="16dp"
android:visibility="gone"
tools:listitem="@layout/item_route"
tools:visibility="visible" />
</FrameLayout>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardCornerRadius="8dp"
app:cardElevation="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:id="@+id/tvSubject"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/green_primary"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/gray_900"
android:textSize="13sp" />
<TextView
android:id="@+id/tvDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:textColor="@color/gray_500"
android:textSize="11sp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="12dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:id="@+id/tvRouteName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/gray_900"
android:textSize="14sp"
android:textStyle="bold" />
<com.example.basurapp.views.TruckProgressView
android:id="@+id/truckProgress"
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_marginTop="8dp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/gray_200"
android:gravity="center"
android:orientation="vertical"
android:padding="24dp">
<ImageView
android:id="@+id/ivUserPhoto"
android:layout_width="80dp"
android:layout_height="80dp"
android:background="@color/gray_300"
android:contentDescription="@string/app_name"
android:src="@mipmap/ic_launcher_round" />
<TextView
android:id="@+id/tvUserName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="usuario"
android:textColor="@color/gray_900"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvUserEmail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textColor="@color/gray_700"
android:textSize="12sp" />
</LinearLayout>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/nav_home"
android:title="@string/add_address" />
<item
android:id="@+id/nav_info"
android:title="@string/menu_info_waste" />
<item
android:id="@+id/nav_report"
android:title="@string/menu_report" />
</group>
<group android:id="@+id/group_bottom">
<item
android:id="@+id/nav_logout"
android:title="@string/menu_logout" />
<item
android:id="@+id/nav_exit"
android:title="@string/menu_exit" />
</group>
</menu>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/nav_user_reports"
android:title="@string/menu_user_reports" />
<item
android:id="@+id/nav_worker_reports"
android:title="@string/menu_worker_reports" />
</group>
<group android:id="@+id/group_bottom">
<item
android:id="@+id/nav_logout"
android:title="@string/menu_logout" />
<item
android:id="@+id/nav_exit"
android:title="@string/menu_exit" />
</group>
</menu>

View File

@@ -1,5 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<!-- Paleta verde (reciclaje) -->
<color name="green_primary">#2E7D32</color>
<color name="green_dark">#1B5E20</color>
<color name="green_light">#66BB6A</color>
<color name="green_pale">#E8F5E9</color>
<!-- Neutros -->
<color name="white">#FFFFFF</color>
<color name="black">#000000</color>
<color name="gray_100">#F5F5F5</color>
<color name="gray_200">#EEEEEE</color>
<color name="gray_300">#E0E0E0</color>
<color name="gray_400">#BDBDBD</color>
<color name="gray_500">#9E9E9E</color>
<color name="gray_700">#616161</color>
<color name="gray_900">#212121</color>
<!-- Estados -->
<color name="error">#D32F2F</color>
<color name="success">#388E3C</color>
</resources>

View File

@@ -1,3 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">BasurApp</string>
<!-- Login -->
<string name="login_title">Inicio de sesión</string>
<string name="hint_email">Correo electrónico</string>
<string name="hint_password">Contraseña</string>
<string name="create_account">Crear usuario</string>
<string name="btn_enter">Entrar</string>
<!-- Register -->
<string name="register_title">Crear usuario</string>
<string name="hint_name">Nombre completo</string>
<string name="hint_confirm_password">Confirmar contraseña</string>
<string name="role_user">Usuario</string>
<string name="role_worker">Trabajador</string>
<string name="select_role">Selecciona tu rol:</string>
<!-- Main -->
<string name="no_address_title">Sin dirección</string>
<string name="no_routes">NO RUTAS</string>
<string name="add_address">Agregar dirección</string>
<!-- Drawer -->
<string name="drawer_open">Abrir menú</string>
<string name="drawer_close">Cerrar menú</string>
<string name="menu_info_waste">Información sobre desperdicios</string>
<string name="menu_report">Reportar problema</string>
<string name="menu_logout">Cerrar sesión</string>
<string name="menu_exit">Salir</string>
<string name="menu_user_reports">Reportes de usuarios</string>
<string name="menu_worker_reports">Reportes de trabajadores</string>
<!-- Info / Reportar -->
<string name="info_waste_title">Información de desperdicios</string>
<string name="report_title">Reportes</string>
<string name="hint_subject">Asunto</string>
<string name="hint_message">Mensaje aquí…</string>
<string name="btn_send">Enviar</string>
<!-- Trabajador -->
<string name="control_title">Control</string>
<!-- Diálogo de ruta -->
<string name="btn_accept">Aceptar</string>
<string name="btn_exit">Salir</string>
<!-- Mensajes -->
<string name="error_empty_fields">Completa todos los campos</string>
<string name="error_passwords_dont_match">Las contraseñas no coinciden</string>
<string name="error_invalid_email">Correo inválido</string>
<string name="error_short_password">La contraseña debe tener al menos 6 caracteres</string>
<string name="success_register">Cuenta creada correctamente</string>
<string name="success_report">Reporte enviado</string>
<string name="route_label_prefix">Ruta</string>
</resources>

View File

@@ -1,9 +1,15 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.BasurApp" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<style name="Theme.BasurApp" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">@color/green_primary</item>
<item name="colorPrimaryVariant">@color/green_dark</item>
<item name="colorOnPrimary">@color/white</item>
<style name="Theme.BasurApp" parent="Base.Theme.BasurApp" />
<item name="colorSecondary">@color/green_light</item>
<item name="colorOnSecondary">@color/white</item>
<item name="android:statusBarColor">@color/green_dark</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowBackground">@color/white</item>
</style>
</resources>