Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions LeaseGuard/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.gradle
build
*.apk
captures
67 changes: 67 additions & 0 deletions LeaseGuard/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp)
alias(libs.plugins.kotlin.serialization)
}

android {
namespace = "com.leaseguard.android"
compileSdk = 35

defaultConfig {
applicationId = "com.leaseguard.android"
minSdk = 28
targetSdk = 35
versionCode = 1
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
}
}

dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.navigation.compose)
implementation(libs.kotlinx.serialization.json)

implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)

implementation(libs.androidx.work.runtime.ktx)
implementation(libs.billing.ktx)

testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
}
37 changes: 37 additions & 0 deletions LeaseGuard/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.leaseguard.android">

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-feature android:name="android.hardware.camera" android:required="false" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="LeaseGuard"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.LeaseGuard">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.LeaseGuard">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.leaseguard.android.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>

</manifest>
106 changes: 106 additions & 0 deletions LeaseGuard/app/src/main/kotlin/com/leaseguard/android/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.leaseguard.android

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.*
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.selection.DisableSelection
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Dashboard
import androidx.compose.material.icons.filled.Description
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.leaseguard.android.ui.dashboard.DashboardScreen
import com.leaseguard.android.ui.detail.LeaseDetailScreen
import com.leaseguard.android.ui.settings.SettingsScreen
import com.leaseguard.android.ui.theme.LeaseGuardTheme

sealed class Screen(val route: String, val label: String, val icon: ImageVector) {
object Dashboard : Screen("dashboard", "Dashboard", Icons.Default.Dashboard)
object Leases : Screen("leases", "Leases", Icons.Default.Description)
object Settings : Screen("settings", "Settings", Icons.Default.Settings)
object Detail : Screen("detail/{leaseId}", "Detail", Icons.Default.Description)
object Paywall : Screen("paywall", "Paywall", Icons.Default.Description)
}

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
LeaseGuardTheme {
DisableSelection {
val navController = rememberNavController()
val items = listOf(
Screen.Dashboard,
Screen.Leases,
Screen.Settings
)

Scaffold(
bottomBar = {
NavigationBar {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
items.forEach { screen ->
NavigationBarItem(
icon = { Icon(screen.icon, contentDescription = null) },
label = { Text(screen.label) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
) { innerPadding ->
NavHost(
navController,
startDestination = Screen.Dashboard.route,
modifier = Modifier.padding(innerPadding),
enterTransition = { fadeIn() + slideInHorizontally { it } },
exitTransition = { fadeOut() + slideOutHorizontally { -it } },
popEnterTransition = { fadeIn() + slideInHorizontally { -it } },
popExitTransition = { fadeOut() + slideOutHorizontally { it } }
) {
composable(Screen.Dashboard.route) {
DashboardScreen(navController)
}
composable(Screen.Leases.route) {
DashboardScreen(navController)
}
composable(Screen.Settings.route) {
SettingsScreen()
}
composable(Screen.Detail.route) { backStackEntry ->
val leaseId = backStackEntry.arguments?.getString("leaseId")?.toLong() ?: 0L
LeaseDetailScreen(leaseId, navController)
}
composable(Screen.Paywall.route) {
com.leaseguard.android.ui.components.PaywallScreen(onDismiss = { navController.popBackStack() })
}
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.leaseguard.android.data

import kotlinx.serialization.Serializable

@Serializable
data class BackupData(
val tenants: List<Tenant>,
val leases: List<Lease>,
val documents: List<Document>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.leaseguard.android.data

import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable

@Serializable
@Entity(tableName = "tenants")
data class Tenant(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val name: String,
val email: String,
val phone: String,
val property_address: String,
val unit_number: String,
val created_at: Long = System.currentTimeMillis()
)

@Serializable
@Entity(tableName = "leases")
data class Lease(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val tenant_id: Long,
val start_date: Long,
val end_date: Long,
val monthly_rent: Double,
val status: String = "active", // 'active', 'renewed'
val reminder_90_scheduled: Boolean = false,
val reminder_60_scheduled: Boolean = false,
val reminder_30_scheduled: Boolean = false,
val created_at: Long = System.currentTimeMillis()
)

@Serializable
@Entity(tableName = "documents")
data class Document(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val lease_id: Long,
val display_name: String,
val uri: String,
val doc_type: String, // 'lease_agreement', 'inspection', 'receipt', 'other'
val uploaded_at: Long = System.currentTimeMillis()
)

@Serializable
@Entity(tableName = "app_meta")
data class AppMeta(
@PrimaryKey val key: String,
val value: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.leaseguard.android.data

import androidx.room.*
import kotlinx.coroutines.flow.Flow

@Dao
interface LeaseDao {
@Query("""
SELECT * FROM leases
JOIN tenants ON leases.tenant_id = tenants.id
WHERE leases.status = 'active'
ORDER BY leases.end_date ASC
""")
fun getActiveLeasesWithTenants(): Flow<Map<Lease, Tenant>>

@Insert
suspend fun insertTenant(tenant: Tenant): Long

@Insert
suspend fun insertLease(lease: Lease): Long

@Insert
suspend fun insertDocument(document: Document): Long

@Query("SELECT * FROM tenants WHERE id = :id")
suspend fun getTenantById(id: Long): Tenant?

@Query("SELECT * FROM leases WHERE id = :id")
suspend fun getLeaseById(id: Long): Lease?

@Query("SELECT * FROM documents WHERE lease_id = :leaseId")
fun getDocumentsForLease(leaseId: Long): Flow<List<Document>>

@Update
suspend fun updateLease(lease: Lease)

@Delete
suspend fun deleteLease(lease: Lease)

@Query("DELETE FROM leases WHERE id = :leaseId")
suspend fun deleteLeaseById(leaseId: Long)

@Query("SELECT COUNT(*) FROM leases WHERE status = 'active'")
suspend fun getActiveLeaseCount(): Int

@Query("SELECT * FROM leases WHERE end_date <= :threshold AND status = 'active'")
suspend fun getExpiringLeases(threshold: Long): List<Lease>

@Transaction
suspend fun deleteEverything() {
deleteAllTenants()
deleteAllLeases()
deleteAllDocuments()
deleteAllAppMeta()
}

@Query("DELETE FROM tenants")
suspend fun deleteAllTenants()

@Query("DELETE FROM leases")
suspend fun deleteAllLeases()

@Query("DELETE FROM documents")
suspend fun deleteAllDocuments()

@Query("DELETE FROM app_meta")
suspend fun deleteAllAppMeta()

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAppMeta(meta: AppMeta)

@Query("SELECT * FROM app_meta WHERE `key` = :key")
suspend fun getAppMeta(key: String): AppMeta?

@Query("SELECT * FROM tenants")
suspend fun getAllTenants(): List<Tenant>

@Query("SELECT * FROM leases")
suspend fun getAllLeases(): List<Lease>

@Query("SELECT * FROM documents")
suspend fun getAllDocuments(): List<Document>

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTenants(tenants: List<Tenant>)

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertLeases(leases: List<Lease>)

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertDocuments(documents: List<Document>)
}
Loading