Compare commits
10 Commits
46b74d7099
...
0f0192b00e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f0192b00e | ||
|
|
462f9c315a | ||
| ed435ffbc1 | |||
| 926d3e0a14 | |||
| cddaf87d88 | |||
| c9cc99baa2 | |||
| 2a00b834c7 | |||
| 4d7fc844e2 | |||
| d4d1b27209 | |||
| 3e66ebcf9e |
BIN
.gitignore
vendored
BIN
.gitignore
vendored
Binary file not shown.
BIN
.gradle/8.13/checksums/checksums.lock
Normal file
BIN
.gradle/8.13/checksums/checksums.lock
Normal file
Binary file not shown.
BIN
.gradle/8.13/checksums/md5-checksums.bin
Normal file
BIN
.gradle/8.13/checksums/md5-checksums.bin
Normal file
Binary file not shown.
BIN
.gradle/8.13/checksums/sha1-checksums.bin
Normal file
BIN
.gradle/8.13/checksums/sha1-checksums.bin
Normal file
Binary file not shown.
BIN
.gradle/8.13/executionHistory/executionHistory.bin
Normal file
BIN
.gradle/8.13/executionHistory/executionHistory.bin
Normal file
Binary file not shown.
BIN
.gradle/8.13/executionHistory/executionHistory.lock
Normal file
BIN
.gradle/8.13/executionHistory/executionHistory.lock
Normal file
Binary file not shown.
BIN
.gradle/8.13/fileChanges/last-build.bin
Normal file
BIN
.gradle/8.13/fileChanges/last-build.bin
Normal file
Binary file not shown.
BIN
.gradle/8.13/fileHashes/fileHashes.bin
Normal file
BIN
.gradle/8.13/fileHashes/fileHashes.bin
Normal file
Binary file not shown.
BIN
.gradle/8.13/fileHashes/fileHashes.lock
Normal file
BIN
.gradle/8.13/fileHashes/fileHashes.lock
Normal file
Binary file not shown.
BIN
.gradle/8.13/fileHashes/resourceHashesCache.bin
Normal file
BIN
.gradle/8.13/fileHashes/resourceHashesCache.bin
Normal file
Binary file not shown.
0
.gradle/8.13/gc.properties
Normal file
0
.gradle/8.13/gc.properties
Normal file
BIN
.gradle/buildOutputCleanup/buildOutputCleanup.lock
Normal file
BIN
.gradle/buildOutputCleanup/buildOutputCleanup.lock
Normal file
Binary file not shown.
2
.gradle/buildOutputCleanup/cache.properties
Normal file
2
.gradle/buildOutputCleanup/cache.properties
Normal file
@ -0,0 +1,2 @@
|
||||
#Wed Jan 14 14:27:06 WIB 2026
|
||||
gradle.version=8.13
|
||||
BIN
.gradle/buildOutputCleanup/outputFiles.bin
Normal file
BIN
.gradle/buildOutputCleanup/outputFiles.bin
Normal file
Binary file not shown.
2
.gradle/config.properties
Normal file
2
.gradle/config.properties
Normal file
@ -0,0 +1,2 @@
|
||||
#Wed Jan 14 14:26:49 WIB 2026
|
||||
java.home=C\:\\Program Files\\Android\\Android Studio\\jbr
|
||||
BIN
.gradle/file-system.probe
Normal file
BIN
.gradle/file-system.probe
Normal file
Binary file not shown.
0
.gradle/vcs-1/gc.properties
Normal file
0
.gradle/vcs-1/gc.properties
Normal file
1258
.idea/caches/deviceStreaming.xml
generated
Normal file
1258
.idea/caches/deviceStreaming.xml
generated
Normal file
File diff suppressed because it is too large
Load Diff
8
.idea/deploymentTargetSelector.xml
generated
8
.idea/deploymentTargetSelector.xml
generated
@ -4,6 +4,14 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2026-01-13T05:20:56.137492Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=RR8TA08RD8Z" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
|
||||
13
.idea/deviceManager.xml
generated
Normal file
13
.idea/deviceManager.xml
generated
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DeviceTable">
|
||||
<option name="columnSorters">
|
||||
<list>
|
||||
<ColumnSorterState>
|
||||
<option name="column" value="Name" />
|
||||
<option name="order" value="ASCENDING" />
|
||||
</ColumnSorterState>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
|
||||
50
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
50
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,50 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
8
.idea/markdown.xml
generated
Normal file
8
.idea/markdown.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MarkdownSettings">
|
||||
<option name="previewPanelProviderInfo">
|
||||
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
1
.idea/misc.xml
generated
1
.idea/misc.xml
generated
@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
|
||||
6
.idea/studiobot.xml
generated
Normal file
6
.idea/studiobot.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="StudioBotProjectSettings">
|
||||
<option name="shareContext" value="OptedIn" />
|
||||
</component>
|
||||
</project>
|
||||
17
README.md
17
README.md
@ -79,4 +79,19 @@ Aplikasi memerlukan izin berikut:
|
||||
|
||||
---
|
||||
|
||||
## 📂 Struktur Proyek (Contoh)
|
||||
## 📂 Mockup
|
||||

|
||||
gambar mockup dibuat oleh AI
|
||||
|
||||
## Catatan:
|
||||
- Starter project ini dibuat berbantukan AI
|
||||
- Kembangkan project dari starter yang sudah disediakan, jangan membuat dari awal.
|
||||
- Untuk koordinat bisa ditambah/kurangi angka tertentu agar tidak memunculkan koordinat rumah masing-masing, data awal tetap dari GPS.
|
||||
|
||||
## Pengecekan:
|
||||
- https://ntfy.ubharajaya.ac.id/EAS
|
||||
- https://docs.google.com/spreadsheets/d/1jH15MfnNgpPGuGeid0hYfY7fFUHCEFbCmg8afTyyLZs/edit?gid=0#gid=0
|
||||
|
||||
## Webhook:
|
||||
- test: https://n8n.lab.ubharajaya.ac.id/webhook-test/23c6993d-1792-48fb-ad1c-ffc78a3e6254
|
||||
- production: https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254
|
||||
@ -45,11 +45,15 @@ dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation("androidx.activity:activity-compose:1.9.0")
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
// Location (GPS)
|
||||
implementation("com.google.android.gms:play-services-location:21.0.1")
|
||||
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
||||
@ -2,6 +2,15 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
@ -11,17 +20,27 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.SistemAkademik">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.SistemAkademik">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<!-- 🔥 SPLASH SCREEN (LAUNCHER) -->
|
||||
<activity
|
||||
android:name=".SplashActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- LOGIN -->
|
||||
<activity
|
||||
android:name=".LoginActivity"
|
||||
android:exported="true"/>
|
||||
|
||||
<!-- MAIN -->
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
44
app/src/main/java/id/ac/ubharajaya/sistemakademik/Absensi.kt
Normal file
44
app/src/main/java/id/ac/ubharajaya/sistemakademik/Absensi.kt
Normal file
@ -0,0 +1,44 @@
|
||||
package id.ac.ubharajaya.sistemakademik
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.util.Base64
|
||||
import org.json.JSONObject
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
/**
|
||||
* Data class untuk menyimpan informasi absensi mahasiswa
|
||||
*/
|
||||
data class Absensi(
|
||||
val npm: String,
|
||||
val nama: String,
|
||||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
val waktu: String,
|
||||
val foto: Bitmap? // ⬅️ HARUS nullable
|
||||
) {
|
||||
/**
|
||||
* Convert objek Absensi menjadi JSON
|
||||
*/
|
||||
fun toJson(): JSONObject {
|
||||
val json = JSONObject()
|
||||
json.put("npm", npm)
|
||||
json.put("nama", nama)
|
||||
json.put("latitude", latitude)
|
||||
json.put("longitude", longitude)
|
||||
json.put("waktu", waktu)
|
||||
json.put("timestamp", System.currentTimeMillis())
|
||||
json.put("foto_base64", foto?.let { bitmapToBase64(it) })
|
||||
return json
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun bitmapToBase64(bitmap: Bitmap): String {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
|
||||
return Base64.encodeToString(
|
||||
outputStream.toByteArray(),
|
||||
Base64.NO_WRAP
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,182 @@
|
||||
package id.ac.ubharajaya.sistemakademik
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
class LoginActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
|
||||
val typography = Typography(
|
||||
titleLarge = MaterialTheme.typography.titleLarge.copy(
|
||||
fontFamily = FontFamily.SansSerif,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 22.sp
|
||||
)
|
||||
)
|
||||
|
||||
MaterialTheme(
|
||||
typography = typography,
|
||||
colorScheme = if (isSystemInDarkTheme())
|
||||
darkColorScheme()
|
||||
else
|
||||
lightColorScheme()
|
||||
) {
|
||||
LoginScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoginScreen() {
|
||||
val context = LocalContext.current
|
||||
var npm by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var showCard by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) { showCard = true }
|
||||
|
||||
val buttonGradient = Brush.horizontalGradient(
|
||||
listOf(Color(0xFF1565C0), Color(0xFF42A5F5))
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black) // 🔥 BACKGROUND HITAM
|
||||
.padding(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = showCard,
|
||||
enter = fadeIn() + scaleIn(initialScale = 0.9f)
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
elevation = CardDefaults.cardElevation(14.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(28.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.logo),
|
||||
contentDescription = "Logo",
|
||||
modifier = Modifier
|
||||
.size(120.dp)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
Text(
|
||||
"Absensi PPB",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
|
||||
Text(
|
||||
"Login Mahasiswa",
|
||||
fontSize = 14.sp,
|
||||
color = Color.Gray
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(28.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = npm,
|
||||
onValueChange = { npm = it },
|
||||
label = { Text("NPM") },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.AccountCircle, contentDescription = null)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text("Password") },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
if (npm == "202310715307" && password == "yoo23") {
|
||||
context.startActivity(
|
||||
Intent(context, MainActivity::class.java)
|
||||
)
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"NPM atau password salah",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent),
|
||||
contentPadding = PaddingValues(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)
|
||||
.background(
|
||||
brush = buttonGradient,
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
"Login",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,28 +1,205 @@
|
||||
package id.ac.ubharajaya.sistemakademik
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.MediaStore
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.android.gms.location.LocationServices
|
||||
import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
/* ================== KONFIG LOCKOUT ================== */
|
||||
const val LOCKOUT_DURATION = 10 * 60 * 1000L // 1 menit
|
||||
|
||||
/* ================== REPOSITORY ================== */
|
||||
class AbsensiRepository {
|
||||
fun kirimAbsensi(absensi: Absensi, onResult: (Boolean) -> Unit) {
|
||||
thread {
|
||||
try {
|
||||
val url = java.net.URL(
|
||||
"https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254"
|
||||
)
|
||||
val conn = url.openConnection() as java.net.HttpURLConnection
|
||||
conn.requestMethod = "POST"
|
||||
conn.setRequestProperty("Content-Type", "application/json")
|
||||
conn.doOutput = true
|
||||
|
||||
conn.outputStream.use {
|
||||
it.write(absensi.toJson().toString().toByteArray())
|
||||
}
|
||||
|
||||
onResult(conn.responseCode == 200)
|
||||
conn.disconnect()
|
||||
} catch (e: Exception) {
|
||||
onResult(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ================== VIEWMODEL ================== */
|
||||
class AbsensiViewModel(
|
||||
private val repo: AbsensiRepository,
|
||||
private val activity: ComponentActivity
|
||||
) : ViewModel() {
|
||||
|
||||
var lokasi by mutableStateOf("Koordinat: -")
|
||||
private set
|
||||
|
||||
var latitude by mutableStateOf<Double?>(null)
|
||||
private set
|
||||
|
||||
var longitude by mutableStateOf<Double?>(null)
|
||||
private set
|
||||
|
||||
var foto by mutableStateOf<Bitmap?>(null)
|
||||
private set
|
||||
|
||||
var absensiList = mutableStateListOf<Absensi>()
|
||||
|
||||
var waktuAbsensi by mutableStateOf<String?>(null)
|
||||
private set
|
||||
|
||||
var isLoading by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
var sisaLockout by mutableStateOf(0L)
|
||||
private set
|
||||
|
||||
init {
|
||||
startLockoutCountdown()
|
||||
}
|
||||
|
||||
fun updateLokasi(lat: Double, lon: Double) {
|
||||
latitude = lat
|
||||
longitude = lon
|
||||
lokasi = "Lat: $lat\nLon: $lon"
|
||||
}
|
||||
|
||||
fun updateFoto(bitmap: Bitmap) {
|
||||
foto = bitmap
|
||||
}
|
||||
|
||||
private fun getLastAbsensiTime(): Long {
|
||||
val prefs = activity.getSharedPreferences("absensi_prefs", Activity.MODE_PRIVATE)
|
||||
return prefs.getLong("last_absensi_time", 0L)
|
||||
}
|
||||
|
||||
private fun simpanWaktuAbsensi() {
|
||||
val prefs = activity.getSharedPreferences("absensi_prefs", Activity.MODE_PRIVATE)
|
||||
prefs.edit().putLong("last_absensi_time", System.currentTimeMillis()).apply()
|
||||
}
|
||||
|
||||
private fun startLockoutCountdown() {
|
||||
viewModelScope.launch {
|
||||
while (true) {
|
||||
val diff = System.currentTimeMillis() - getLastAbsensiTime()
|
||||
sisaLockout =
|
||||
if (diff < LOCKOUT_DURATION) LOCKOUT_DURATION - diff else 0L
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun kirimAbsensi() {
|
||||
if (sisaLockout > 0) {
|
||||
Toast.makeText(
|
||||
activity,
|
||||
"Sudah absen, tunggu ${(sisaLockout / 1000) / 60} menit",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
return
|
||||
}
|
||||
|
||||
if (latitude == null || longitude == null || foto == null) {
|
||||
Toast.makeText(activity, "Lokasi atau foto belum lengkap", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
val waktu = SimpleDateFormat(
|
||||
"dd MMM yyyy HH:mm:ss",
|
||||
Locale.getDefault()
|
||||
).format(System.currentTimeMillis())
|
||||
|
||||
val absensi = Absensi(
|
||||
npm = "202310715307",
|
||||
nama = "Satrio Putra Wardani",
|
||||
latitude = latitude!!,
|
||||
longitude = longitude!!,
|
||||
waktu = waktu,
|
||||
foto = foto
|
||||
)
|
||||
|
||||
isLoading = true
|
||||
repo.kirimAbsensi(absensi) { success ->
|
||||
viewModelScope.launch {
|
||||
isLoading = false
|
||||
if (success) {
|
||||
absensiList.add(absensi)
|
||||
waktuAbsensi = waktu
|
||||
simpanWaktuAbsensi()
|
||||
Toast.makeText(activity, "Absensi berhasil", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(activity, "Gagal kirim absensi", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ================== MAIN ACTIVITY ================== */
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
private lateinit var viewModel: AbsensiViewModel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
|
||||
viewModel = AbsensiViewModel(AbsensiRepository(), this)
|
||||
|
||||
setContent {
|
||||
SistemAkademikTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
Greeting(
|
||||
name = "Android",
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
Scaffold(
|
||||
topBar = { TopBarLogout(this) }
|
||||
) { padding ->
|
||||
AbsensiScreen(
|
||||
modifier = Modifier.padding(padding),
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -30,18 +207,174 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
/* ================== TOP BAR ================== */
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = "Hello $name!",
|
||||
modifier = modifier
|
||||
fun TopBarLogout(activity: ComponentActivity) {
|
||||
val context = LocalContext.current
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
" Sistem Akademik ",
|
||||
style = MaterialTheme.typography.titleLarge.copy(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
TextButton(onClick = {
|
||||
val intent = Intent(context, LoginActivity::class.java)
|
||||
intent.flags =
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
context.startActivity(intent)
|
||||
}) {
|
||||
Text(
|
||||
"Logout",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = Color.Red
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
/* ================== UI ================== */
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun GreetingPreview() {
|
||||
SistemAkademikTheme {
|
||||
Greeting("Android")
|
||||
fun AbsensiScreen(modifier: Modifier, viewModel: AbsensiViewModel) {
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
val locationPermissionLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||
if (granted) {
|
||||
LocationServices.getFusedLocationProviderClient(context)
|
||||
.lastLocation.addOnSuccessListener {
|
||||
it?.let { loc ->
|
||||
viewModel.updateLokasi(loc.latitude, loc.longitude)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val cameraLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
val bitmap =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||
it.data?.extras?.getParcelable("data", Bitmap::class.java)
|
||||
else it.data?.extras?.getParcelable("data")
|
||||
bitmap?.let { bmp -> viewModel.updateFoto(bmp) }
|
||||
}
|
||||
}
|
||||
|
||||
val cameraPermissionLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||
if (it) cameraLauncher.launch(Intent(MediaStore.ACTION_IMAGE_CAPTURE))
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.White)
|
||||
.padding(24.dp)
|
||||
) {
|
||||
|
||||
Text(
|
||||
text = "Absensi Akademik",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.headlineSmall.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.Black
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(6.dp)
|
||||
) {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
"Lokasi:",
|
||||
style = MaterialTheme.typography.bodyLarge.copy(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
)
|
||||
Text(
|
||||
viewModel.lokasi,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = Color.White
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Button(
|
||||
onClick = { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Ambil Foto")
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
Button(
|
||||
onClick = { viewModel.kirimAbsensi() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = !viewModel.isLoading
|
||||
) {
|
||||
if (viewModel.isLoading)
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(22.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
else Text("Kirim Absensi")
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
LazyColumn {
|
||||
items(viewModel.absensiList) { item ->
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp)
|
||||
.animateContentSize()
|
||||
) {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
item.nama,
|
||||
style = MaterialTheme.typography.bodyLarge.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White
|
||||
)
|
||||
)
|
||||
Text(item.npm, color = Color.White)
|
||||
Text(item.waktu, color = Color.White)
|
||||
item.foto?.let {
|
||||
Image(
|
||||
bitmap = it.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(150.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
package id.ac.ubharajaya.sistemakademik
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
class SplashActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
SplashScreen {
|
||||
startActivity(Intent(this, LoginActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SplashScreen(onFinish: () -> Unit) {
|
||||
LaunchedEffect(Unit) {
|
||||
delay(2000) // 2 detik
|
||||
onFinish()
|
||||
}
|
||||
|
||||
// 🔥 GRADIENT ABU-ABU MODERN
|
||||
val gradient = Brush.verticalGradient(
|
||||
listOf(
|
||||
Color(0xFF2C2C2C), // Dark Grey
|
||||
Color(0xFF757575) // Light Grey
|
||||
)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(gradient),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.logo),
|
||||
contentDescription = "Logo",
|
||||
modifier = Modifier
|
||||
.size(140.dp)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
}
|
||||
}
|
||||
BIN
app/src/main/res/drawable/logo.png
Normal file
BIN
app/src/main/res/drawable/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
app/src/main/res/logo.png
Normal file
BIN
app/src/main/res/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
8
local.properties
Normal file
8
local.properties
Normal file
@ -0,0 +1,8 @@
|
||||
## This file must *NOT* be checked into Version Control Systems,
|
||||
# as it contains information specific to your local configuration.
|
||||
#
|
||||
# Location of the SDK. This is only used by Gradle.
|
||||
# For customization when using a Version Control System, please read the
|
||||
# header note.
|
||||
#Wed Jan 14 14:26:50 WIB 2026
|
||||
sdk.dir=C\:\\Users\\Lenovo\\AppData\\Local\\Android\\Sdk
|
||||
225
n8n-workflow-EAS.json
Normal file
225
n8n-workflow-EAS.json
Normal file
@ -0,0 +1,225 @@
|
||||
{
|
||||
"name": "EAS",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://ntfy.ubharajaya.ac.id/EAS",
|
||||
"sendBody": true,
|
||||
"contentType": "raw",
|
||||
"body": "=Absensi: {{ $json.body.nama }} NPM: {{ $json.body.npm }}",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.3,
|
||||
"position": [
|
||||
-272,
|
||||
-240
|
||||
],
|
||||
"id": "83504eec-6d20-46d7-9ea1-509ae4ee8660",
|
||||
"name": "NTFY HTTP Request"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "23c6993d-1792-48fb-ad1c-ffc78a3e6254",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2.1,
|
||||
"position": [
|
||||
-864,
|
||||
-112
|
||||
],
|
||||
"id": "9ed3d2db-2d50-40b5-8408-7404edd48442",
|
||||
"name": "Webhook Absensi",
|
||||
"webhookId": "23c6993d-1792-48fb-ad1c-ffc78a3e6254"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "append",
|
||||
"documentId": {
|
||||
"__rl": true,
|
||||
"value": "1jH15MfnNgpPGuGeid0hYfY7fFUHCEFbCmg8afTyyLZs",
|
||||
"mode": "id"
|
||||
},
|
||||
"sheetName": {
|
||||
"__rl": true,
|
||||
"value": "Absensi",
|
||||
"mode": "name"
|
||||
},
|
||||
"columns": {
|
||||
"mappingMode": "defineBelow",
|
||||
"value": {
|
||||
"latitude": "={{ $json.body.latitude }}",
|
||||
"longitude": "={{ $json.body.longitude }}",
|
||||
"timestamp": "={{ $json.body.timestamp }}",
|
||||
"foto_base64": "={{ $json.body.foto_base64 }}",
|
||||
"nama": "={{ $json.body.nama }}",
|
||||
"npm": "={{ $json.body.npm }}"
|
||||
},
|
||||
"matchingColumns": [],
|
||||
"schema": [
|
||||
{
|
||||
"id": "timestamp",
|
||||
"displayName": "timestamp",
|
||||
"required": false,
|
||||
"defaultMatch": false,
|
||||
"display": true,
|
||||
"type": "string",
|
||||
"canBeUsedToMatch": true,
|
||||
"removed": false
|
||||
},
|
||||
{
|
||||
"id": "npm",
|
||||
"displayName": "npm",
|
||||
"required": false,
|
||||
"defaultMatch": false,
|
||||
"display": true,
|
||||
"type": "string",
|
||||
"canBeUsedToMatch": true,
|
||||
"removed": false
|
||||
},
|
||||
{
|
||||
"id": "nama",
|
||||
"displayName": "nama",
|
||||
"required": false,
|
||||
"defaultMatch": false,
|
||||
"display": true,
|
||||
"type": "string",
|
||||
"canBeUsedToMatch": true,
|
||||
"removed": false
|
||||
},
|
||||
{
|
||||
"id": "latitude",
|
||||
"displayName": "latitude",
|
||||
"required": false,
|
||||
"defaultMatch": false,
|
||||
"display": true,
|
||||
"type": "string",
|
||||
"canBeUsedToMatch": true,
|
||||
"removed": false
|
||||
},
|
||||
{
|
||||
"id": "longitude",
|
||||
"displayName": "longitude",
|
||||
"required": false,
|
||||
"defaultMatch": false,
|
||||
"display": true,
|
||||
"type": "string",
|
||||
"canBeUsedToMatch": true,
|
||||
"removed": false
|
||||
},
|
||||
{
|
||||
"id": "photo",
|
||||
"displayName": "photo",
|
||||
"required": false,
|
||||
"defaultMatch": false,
|
||||
"display": true,
|
||||
"type": "string",
|
||||
"canBeUsedToMatch": true,
|
||||
"removed": false
|
||||
},
|
||||
{
|
||||
"id": "status",
|
||||
"displayName": "status",
|
||||
"required": false,
|
||||
"defaultMatch": false,
|
||||
"display": true,
|
||||
"type": "string",
|
||||
"canBeUsedToMatch": true,
|
||||
"removed": false
|
||||
},
|
||||
{
|
||||
"id": "foto_base64",
|
||||
"displayName": "foto_base64",
|
||||
"required": false,
|
||||
"defaultMatch": false,
|
||||
"display": true,
|
||||
"type": "string",
|
||||
"canBeUsedToMatch": true,
|
||||
"removed": false
|
||||
}
|
||||
],
|
||||
"attemptToConvertTypes": false,
|
||||
"convertFieldsToString": false
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.googleSheets",
|
||||
"typeVersion": 4.7,
|
||||
"position": [
|
||||
-272,
|
||||
-32
|
||||
],
|
||||
"id": "cd83a9fa-ea00-4a20-aa31-846bfe044aeb",
|
||||
"name": "Append row in sheet",
|
||||
"credentials": {
|
||||
"googleSheetsOAuth2Api": {
|
||||
"id": "hNVNhkTQbqkJ3C56",
|
||||
"name": "Google Sheets account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.myNewField = 1;\n}\n\nreturn $input.all();"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
-528,
|
||||
-240
|
||||
],
|
||||
"id": "4ed9edf6-4562-41b6-afd0-89c96991454a",
|
||||
"name": "Code in JavaScript"
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Webhook Absensi": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Append row in sheet",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Code in JavaScript",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"NTFY HTTP Request": {
|
||||
"main": [
|
||||
[]
|
||||
]
|
||||
},
|
||||
"Code in JavaScript": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "NTFY HTTP Request",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": true,
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"availableInMCP": false
|
||||
},
|
||||
"versionId": "49466b31-67ce-49b7-af37-33cd28d7092d",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "b8ffac81bb85d267c3296e074b3e692ecef11caeef79fa72af892085548f350a"
|
||||
},
|
||||
"id": "E_gxZpNrN3G5ibejHcTFS",
|
||||
"tags": []
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user