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

1
.idea/gradle.xml generated
View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>

2
.idea/misc.xml generated
View File

@@ -1,6 +1,6 @@
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="jbr-21" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

1
.idea/vcs.xml generated
View File

@@ -2,5 +2,6 @@
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" /> <mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/hackathon-gituber-4fbb7edead2842bd8517a7b78b908771" vcs="Git" />
</component> </component>
</project> </project>

View File

@@ -1,22 +1,18 @@
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
id("com.google.gms.google-services")
} }
android { android {
namespace = "com.example.basurapp" namespace = "com.example.basurapp"
compileSdk { compileSdk = 34
version = release(36) {
minorApiLevel = 1
}
}
defaultConfig { defaultConfig {
applicationId = "com.example.basurapp" applicationId = "com.example.basurapp"
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 34
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -29,19 +25,38 @@ android {
) )
} }
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_1_8
}
buildFeatures {
viewBinding = true
} }
} }
dependencies { dependencies {
implementation(libs.androidx.core.ktx) // AndroidX (versiones compatibles con SDK 34)
implementation(libs.androidx.appcompat) implementation("androidx.core:core-ktx:1.12.0")
implementation(libs.material) implementation("androidx.appcompat:appcompat:1.6.1")
implementation(libs.androidx.activity) implementation("com.google.android.material:material:1.11.0")
implementation(libs.androidx.constraintlayout) implementation("androidx.constraintlayout:constraintlayout:2.1.4")
testImplementation(libs.junit) implementation("androidx.activity:activity-ktx:1.8.2")
androidTestImplementation(libs.androidx.junit) implementation("androidx.fragment:fragment-ktx:1.6.2")
androidTestImplementation(libs.androidx.espresso.core) 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" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
@@ -10,9 +13,13 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.BasurApp"> android:theme="@style/Theme.BasurApp"
tools:targetApi="31">
<activity <activity
android:name=".MainActivity" android:name=".RegisterActivity"
android:exported="false" />
<activity
android:name=".LoginActivity"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@@ -20,6 +27,9 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".MainActivity"
android:exported="false" />
</application> </application>
</manifest> </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 package com.example.basurapp
import android.content.Intent
import android.os.Bundle 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.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.core.view.GravityCompat
import androidx.core.view.WindowInsetsCompat 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(R.layout.activity_main) setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) setSupportActionBar(binding.toolbar)
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) supportActionBar?.title = ""
insets
// 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"?> <?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main" android:id="@+id/drawerLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".MainActivity"> tools:context=".MainActivity">
<TextView <androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="wrap_content" android:id="@+id/main"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:text="Hello Adri!" android:layout_height="match_parent">
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="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"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="black">#FF000000</color> <!-- Paleta verde (reciclaje) -->
<color name="white">#FFFFFFFF</color> <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> </resources>

View File

@@ -1,3 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">BasurApp</string> <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> </resources>

View File

@@ -1,9 +1,15 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. --> <style name="Theme.BasurApp" parent="Theme.Material3.DayNight.NoActionBar">
<style name="Base.Theme.BasurApp" parent="Theme.Material3.DayNight.NoActionBar"> <item name="colorPrimary">@color/green_primary</item>
<!-- Customize your light theme here. --> <item name="colorPrimaryVariant">@color/green_dark</item>
<!-- <item name="colorPrimary">@color/my_light_primary</item> --> <item name="colorOnPrimary">@color/white</item>
</style>
<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> </resources>

View File

@@ -1,4 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
id("com.google.gms.google-services") version "4.4.4" apply false
} }

Submodule hackathon-gituber-4fbb7edead2842bd8517a7b78b908771 updated: ec8d93ba60...165d9d2e96