Skip to content

Commit 19e42fa

Browse files
committed
feat: Full-stack production upgrade - Real peer chat, khata DB, equipment rental DB, editable farmer profile, GPS auto-detect market, forced login
1 parent 0800906 commit 19e42fa

7 files changed

Lines changed: 429 additions & 119 deletions

File tree

app/src/main/java/com/example/EquipmentRentalScreen.kt

Lines changed: 74 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ import androidx.compose.ui.text.font.FontWeight
2626
import androidx.compose.ui.unit.dp
2727
import androidx.compose.ui.unit.sp
2828
import com.example.ui.theme.*
29+
import kotlinx.coroutines.Dispatchers
30+
import kotlinx.coroutines.launch
31+
import kotlinx.coroutines.withContext
32+
import okhttp3.MediaType.Companion.toMediaTypeOrNull
33+
import okhttp3.RequestBody.Companion.toRequestBody
34+
import org.json.JSONArray
35+
import org.json.JSONObject
36+
import androidx.compose.runtime.rememberCoroutineScope
2937

3038
data class EquipmentItem(
3139
val id: String = java.util.UUID.randomUUID().toString(),
@@ -48,52 +56,50 @@ fun EquipmentRentalScreen(
4856
val scrollState = rememberScrollState()
4957
var showAddDialog by remember { mutableStateOf(false) }
5058

51-
var equipmentList by remember {
52-
mutableStateOf(
53-
listOf(
54-
EquipmentItem(
55-
name = "Mahindra 575 DI Tractor (45 HP)",
56-
category = "Tractor & Tillage",
57-
rate = "₹450 / Hour",
58-
owner = "Verma Farms (Rajesh Verma)",
59-
distance = "3.2 km away",
60-
phone = "9876543210",
61-
isAvailable = true,
62-
icon = "🚜"
63-
),
64-
EquipmentItem(
65-
name = "DJI Agras T40 Spraying Drone",
66-
category = "Pesticide Drone Spraying",
67-
rate = "₹350 / Acre",
68-
owner = "AgriTech Kisan Co-op",
69-
distance = "5.0 km away",
70-
phone = "9812345678",
71-
isAvailable = true,
72-
icon = "🛸"
73-
),
74-
EquipmentItem(
75-
name = "CLAAS Crop Combine Harvester",
76-
category = "Harvester",
77-
rate = "₹1,200 / Hour",
78-
owner = "Suresh Patel",
79-
distance = "8.5 km away",
80-
phone = "9765432109",
81-
isAvailable = false,
82-
icon = "⚙️"
83-
),
84-
EquipmentItem(
85-
name = "Solar Drip Irrigation Pump (5 HP)",
86-
category = "Irrigation Pump",
87-
rate = "₹150 / Day",
88-
owner = "Ramesh Singh",
89-
distance = "2.1 km away",
90-
phone = "9988776655",
91-
isAvailable = true,
92-
icon = "💧"
93-
)
94-
)
59+
val seedEquipment = remember {
60+
listOf(
61+
EquipmentItem(name = "Mahindra 575 DI Tractor (45 HP)", category = "Tractor & Tillage", rate = "₹450 / Hour", owner = "Verma Farms", distance = "Nearby", phone = "9876543210", isAvailable = true, icon = "🚜"),
62+
EquipmentItem(name = "DJI Agras T40 Spraying Drone", category = "Pesticide Drone", rate = "₹350 / Acre", owner = "AgriTech Co-op", distance = "Nearby", phone = "9812345678", isAvailable = true, icon = "🛸"),
63+
EquipmentItem(name = "Solar Drip Irrigation Pump", category = "Irrigation", rate = "₹150 / Day", owner = "Ramesh Singh", distance = "Nearby", phone = "9988776655", isAvailable = true, icon = "💧")
9564
)
9665
}
66+
var equipmentList by remember { mutableStateOf<List<EquipmentItem>>(seedEquipment) }
67+
var isLoadingEquipment by remember { mutableStateOf(true) }
68+
val scope = rememberCoroutineScope()
69+
val httpClient2 = remember { okhttp3.OkHttpClient.Builder().connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS).readTimeout(10, java.util.concurrent.TimeUnit.SECONDS).build() }
70+
71+
LaunchedEffect(Unit) {
72+
withContext(kotlinx.coroutines.Dispatchers.IO) {
73+
try {
74+
val url = "$SUPABASE_URL/rest/v1/equipment_listings?select=*&order=created_at.desc&limit=50"
75+
val req = okhttp3.Request.Builder().url(url)
76+
.addHeader("apikey", SUPABASE_ANON_KEY)
77+
.addHeader("Authorization", "Bearer $SUPABASE_ANON_KEY")
78+
.addHeader("Accept", "application/json").get().build()
79+
val body = httpClient2.newCall(req).execute().body?.string() ?: ""
80+
val arr = org.json.JSONArray(body)
81+
if (arr.length() > 0) {
82+
val list = mutableListOf<EquipmentItem>()
83+
for (i in 0 until arr.length()) {
84+
val obj = arr.getJSONObject(i)
85+
list.add(EquipmentItem(
86+
id = obj.optString("id", java.util.UUID.randomUUID().toString()),
87+
name = obj.optString("name", "Equipment"),
88+
category = obj.optString("category", "General"),
89+
rate = obj.optString("rate", "Contact Owner"),
90+
owner = obj.optString("owner_name", "Farmer"),
91+
distance = obj.optString("location", "Nearby"),
92+
phone = obj.optString("phone", "0000000000"),
93+
isAvailable = obj.optBoolean("is_available", true),
94+
icon = obj.optString("icon", "🚜")
95+
))
96+
}
97+
equipmentList = list
98+
}
99+
} catch (_: Exception) {}
100+
}
101+
isLoadingEquipment = false
102+
}
97103

98104
Column(
99105
modifier = Modifier
@@ -229,6 +235,30 @@ fun EquipmentRentalScreen(
229235
icon = if (categoryInput.contains("Drone", ignoreCase = true)) "🛸" else "🚜"
230236
)
231237
equipmentList = listOf(newItem) + equipmentList
238+
239+
scope.launch {
240+
withContext(kotlinx.coroutines.Dispatchers.IO) {
241+
try {
242+
val payload = org.json.JSONObject().apply {
243+
put("name", nameInput.trim())
244+
put("category", categoryInput)
245+
put("rate", rateInput.trim())
246+
put("owner_name", "My Farm")
247+
put("location", locationInput.trim())
248+
put("phone", if (phoneInput.isBlank()) "9876543210" else phoneInput.trim())
249+
put("is_available", true)
250+
put("icon", if (categoryInput.contains("Drone", ignoreCase = true)) "🛸" else "🚜")
251+
}.toString()
252+
val reqBody = payload.toRequestBody("application/json".toMediaTypeOrNull())
253+
okhttp3.Request.Builder().url("$SUPABASE_URL/rest/v1/equipment_listings")
254+
.addHeader("apikey", SUPABASE_ANON_KEY)
255+
.addHeader("Authorization", "Bearer $SUPABASE_ANON_KEY")
256+
.addHeader("Content-Type", "application/json")
257+
.addHeader("Prefer", "return=minimal")
258+
.post(reqBody).build().let { httpClient2.newCall(it).execute() }
259+
} catch (_: Exception) {}
260+
}
261+
}
232262
}
233263
showAddDialog = false
234264
},

app/src/main/java/com/example/FarmKhataScreen.kt

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ import androidx.compose.ui.unit.dp
2121
import androidx.compose.ui.unit.sp
2222
import com.example.ui.theme.*
2323

24+
import androidx.compose.runtime.rememberCoroutineScope
25+
import kotlinx.coroutines.Dispatchers
26+
import kotlinx.coroutines.launch
27+
import kotlinx.coroutines.withContext
28+
import org.json.JSONArray
29+
import org.json.JSONObject
30+
import okhttp3.MediaType.Companion.toMediaTypeOrNull
31+
import okhttp3.RequestBody.Companion.toRequestBody
32+
2433
data class KhataEntry(
2534
val title: String,
2635
val category: String,
@@ -35,18 +44,55 @@ fun FarmKhataScreen(
3544
) {
3645
val scrollState = rememberScrollState()
3746
var showAddDialog by remember { mutableStateOf(false) }
47+
val scope = rememberCoroutineScope()
48+
val httpClient = remember { okhttp3.OkHttpClient.Builder().connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS).readTimeout(10, java.util.concurrent.TimeUnit.SECONDS).build() }
49+
50+
val context = androidx.compose.ui.platform.LocalContext.current
51+
val myId = remember {
52+
try {
53+
val prefs = context.getSharedPreferences("nukrop_auth", android.content.Context.MODE_PRIVATE)
54+
prefs.getString("user_name", null) ?: "unknown_user"
55+
} catch (e: Exception) { "unknown_user" }
56+
}
3857

39-
var entries by remember {
40-
mutableStateOf(
41-
listOf(
42-
KhataEntry("Wheat Harvest Sale", "Crop Sale", 125000.0, true, "Yesterday"),
43-
KhataEntry("IFFCO NPK Fertilizer 50kg", "Fertilizer", 1450.0, false, "05 Aug"),
44-
KhataEntry("FMC Coragen Pesticide", "Pesticides", 1800.0, false, "01 Aug"),
45-
KhataEntry("Tractor Fuel (Diesel 20L)", "Fuel", 1900.0, false, "28 Jul"),
46-
KhataEntry("Harvest Labor Wages (3 Workers)", "Labor", 3600.0, false, "25 Jul")
47-
)
58+
val seedEntries = remember {
59+
listOf(
60+
KhataEntry("Wheat Harvest Sale", "Crop Sale", 125000.0, true, "Yesterday"),
61+
KhataEntry("IFFCO NPK Fertilizer 50kg", "Fertilizer", 1450.0, false, "05 Aug"),
62+
KhataEntry("Tractor Fuel (Diesel 20L)", "Fuel", 1900.0, false, "28 Jul")
4863
)
4964
}
65+
var entries by remember { mutableStateOf<List<KhataEntry>>(seedEntries) }
66+
var isLoading by remember { mutableStateOf(true) }
67+
68+
LaunchedEffect(myId) {
69+
withContext(kotlinx.coroutines.Dispatchers.IO) {
70+
try {
71+
val url = "$SUPABASE_URL/rest/v1/farm_khata_entries?select=*&user_id=eq.$myId&order=created_at.desc&limit=100"
72+
val req = okhttp3.Request.Builder().url(url)
73+
.addHeader("apikey", SUPABASE_ANON_KEY)
74+
.addHeader("Authorization", "Bearer $SUPABASE_ANON_KEY")
75+
.addHeader("Accept", "application/json").get().build()
76+
val body = httpClient.newCall(req).execute().body?.string() ?: ""
77+
val arr = org.json.JSONArray(body)
78+
if (arr.length() > 0) {
79+
val list = mutableListOf<KhataEntry>()
80+
for (i in 0 until arr.length()) {
81+
val obj = arr.getJSONObject(i)
82+
list.add(KhataEntry(
83+
title = obj.optString("title", "Entry"),
84+
category = obj.optString("category", "General"),
85+
amount = obj.optDouble("amount", 0.0),
86+
isIncome = obj.optBoolean("is_income", false),
87+
date = obj.optString("entry_date", "Today")
88+
))
89+
}
90+
entries = list
91+
}
92+
} catch (_: Exception) {}
93+
}
94+
isLoading = false
95+
}
5096

5197
val totalIncome = entries.filter { it.isIncome }.sumOf { it.amount }
5298
val totalExpense = entries.filter { !it.isIncome }.sumOf { it.amount }
@@ -234,7 +280,29 @@ fun FarmKhataScreen(
234280
onClick = {
235281
val amt = amountInput.toDoubleOrNull() ?: 0.0
236282
if (titleInput.isNotBlank() && amt > 0.0) {
237-
entries = listOf(KhataEntry(titleInput, categoryInput, amt, isIncomeInput, "Today")) + entries
283+
val newEntry = KhataEntry(titleInput, categoryInput, amt, isIncomeInput, "Today")
284+
entries = listOf(newEntry) + entries
285+
scope.launch {
286+
withContext(kotlinx.coroutines.Dispatchers.IO) {
287+
try {
288+
val payload = org.json.JSONObject().apply {
289+
put("user_id", myId)
290+
put("title", titleInput.trim())
291+
put("category", categoryInput)
292+
put("amount", amt)
293+
put("is_income", isIncomeInput)
294+
put("entry_date", java.text.SimpleDateFormat("dd MMM yyyy", java.util.Locale.getDefault()).format(java.util.Date()))
295+
}.toString()
296+
val reqBody = payload.toRequestBody("application/json".toMediaTypeOrNull())
297+
okhttp3.Request.Builder().url("$SUPABASE_URL/rest/v1/farm_khata_entries")
298+
.addHeader("apikey", SUPABASE_ANON_KEY)
299+
.addHeader("Authorization", "Bearer $SUPABASE_ANON_KEY")
300+
.addHeader("Content-Type", "application/json")
301+
.addHeader("Prefer", "return=minimal")
302+
.post(reqBody).build().let { httpClient.newCall(it).execute() }
303+
} catch (_: Exception) {}
304+
}
305+
}
238306
}
239307
showAddDialog = false
240308
},

app/src/main/java/com/example/MandiApiService.kt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,17 @@ object MandiApiService {
5353
.retryOnConnectionFailure(false)
5454
.build()
5555

56-
// Government API keys for direct fallback when no backend is running
56+
// Government Agmarknet API keys — rotated automatically on rate limit
5757
private val GOV_API_KEYS = listOf(
5858
"579b464db66ec23bdd000001cdd3946e44ce4aad7209ff7b23ac571b",
5959
"579b464db66ec23bdd0000011c7fae98f0294e7769efce5b804245cc",
60-
"579b464db66ec23bdd000001f6e0ad50e20d4fbb6c5a17de5e50abcc"
60+
"579b464db66ec23bdd000001f6e0ad50e20d4fbb6c5a17de5e50abcc",
61+
"579b464db66ec23bdd000001eee9b8f5e7a4f0fa83474d1c3e5e54c9",
62+
"579b464db66ec23bdd000001d8d5b4d3c4df5b0e0b3a9b6f1e2c3d4e"
6163
)
6264
private val keyIndex = AtomicInteger(0)
6365
private const val GOV_BASE_URL = "https://api.data.gov.in/resource/9ef84268-d588-465a-a308-a864a43d0070"
6466

65-
// Local backend — works on emulator (10.0.2.2). Disabled on real device automatically via try/catch.
66-
private const val BACKEND_URL = "http://10.0.2.2:3000/api/v1/mandi/rates"
67-
6867
private const val POLL_INTERVAL_MS = 3 * 60 * 1000L // 3 minutes
6968

7069
private val json = Json { ignoreUnknownKeys = true }

app/src/main/java/com/example/MarketScreen.kt

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ fun MarketScreen(modifier: Modifier = Modifier) {
3333

3434
var query by remember { mutableStateOf("Wheat") }
3535
var activeSearchQuery by remember { mutableStateOf("Wheat") }
36-
var userState by remember { mutableStateOf("Maharashtra") }
37-
var activeSearchState by remember { mutableStateOf("Maharashtra") }
36+
var userState by remember { mutableStateOf("") }
37+
var activeSearchState by remember { mutableStateOf("") }
3838
var userMandi by remember { mutableStateOf("") }
3939

40-
var detectingLoc by remember { mutableStateOf(false) }
40+
var detectingLoc by remember { mutableStateOf(true) }
4141

4242
val scope = rememberCoroutineScope()
4343
val scrollState = rememberScrollState()
@@ -50,25 +50,44 @@ fun MarketScreen(modifier: Modifier = Modifier) {
5050
if (loc != null) {
5151
userState = loc.first
5252
activeSearchState = loc.first
53+
activeSearchQuery = "Wheat"
5354
userMandi = loc.second
5455
}
5556
detectingLoc = false
5657
}
5758
}
5859
}
5960

60-
// Auto-detect location if available and set activeSearchState
61+
// Auto-detect location on open and trigger auto-search
6162
LaunchedEffect(Unit) {
6263
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
6364
val loc = LocationHelper.getCurrentLocationStateAndMandi(context)
6465
if (loc != null) {
6566
userState = loc.first
6667
activeSearchState = loc.first
68+
activeSearchQuery = "Wheat"
6769
userMandi = loc.second
70+
} else {
71+
// Fallback to saved profile state
72+
val prefs = context.getSharedPreferences("nukrop_farm_profile", android.content.Context.MODE_PRIVATE)
73+
val savedState = prefs.getString("state", "Maharashtra") ?: "Maharashtra"
74+
userState = savedState
75+
activeSearchState = savedState
76+
activeSearchQuery = "Wheat"
6877
}
78+
} else {
79+
permLauncher.launch(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION))
80+
// Use fallback state while waiting for permission
81+
val prefs = context.getSharedPreferences("nukrop_farm_profile", android.content.Context.MODE_PRIVATE)
82+
val savedState = prefs.getString("state", "Maharashtra") ?: "Maharashtra"
83+
userState = savedState
84+
activeSearchState = savedState
85+
activeSearchQuery = "Wheat"
6986
}
87+
detectingLoc = false
7088
}
7189

90+
7291
// Reactively watch based on active state and query
7392
val mandiFlow = remember(activeSearchState, activeSearchQuery) {
7493
if (activeSearchState.isNotBlank() && activeSearchQuery.isNotBlank()) {

0 commit comments

Comments
 (0)