39 KiB
39 KiB
🏗️ Architecture & Component Diagram
System Architecture Overview
┌─────────────────────────────────────────────────────────────────────────┐
│ APLIKASI ABSENSI AKADEMIK │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ (Jetpack Compose UI) │
│ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ MainActivity (AbsensiScreen) │ │
│ │ • State Management (AttendanceState) │ │
│ │ • Permission Handling (Location + Camera) │ │
│ │ • User Interactions (Button clicks, form inputs) │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────┬──────────────────────┬───────────────────┐ │
│ │ PhotoPreviewCard │ LocationStatusCard │ ErrorAlertCard │ │
│ │ │ │ │ │
│ │ • Show captured │ • Display latitude │ • Show error │ │
│ │ photo │ • Display longitude │ messages │ │
│ │ • Retake button │ • Show validation │ • Dismissable │ │
│ │ │ status │ │ │
│ └──────────────────────┴──────────────────────┴───────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ SubmitButtonWithLoader │ │
│ │ • Loading spinner during submission │ │
│ │ • Disabled until all validations pass │ │
│ └────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────┐
│ BUSINESS LOGIC LAYER │
│ (Models + Utilities) │
│ │
│ ┌──────────────────────────────┐ ┌────────────────────────────────┐ │
│ │ AttendanceState │ │ LocationValidator │ │
│ │ │ │ │ │
│ │ • location: LocationData │ │ • calculateDistance() │ │
│ │ • foto: Bitmap │ │ • isLocationValid() │ │
│ │ • validationResult │ │ • getValidationMessage() │ │
│ │ • errorMessage │ │ • adjustCoordinates() │ │
│ │ • loading states │ │ │ │
│ └──────────────────────────────┘ │ [Haversine Formula] │ │
│ └────────────────────────────────┘ │
│ ┌──────────────────────────────┐ ┌────────────────────────────────┐ │
│ │ AttendanceRecord │ │ ErrorHandler │ │
│ │ │ │ │ │
│ │ • npm │ │ • getErrorMessage() │ │
│ │ • nama │ │ • getUserFriendlyMessage() │ │
│ │ • latitude/longitude │ │ │ │
│ │ • timestamp │ │ Error Types: │ │
│ │ • foto │ │ • NetworkError │ │
│ │ • validation info │ │ • LocationError │ │
│ └──────────────────────────────┘ │ • PermissionError │ │
│ │ • ValidationError │ │
│ │ • UnknownError │ │
│ └────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ AttendanceConfig (Configuration) │ │
│ │ │ │
│ │ • REFERENCE_LATITUDE / REFERENCE_LONGITUDE │ │
│ │ • ALLOWED_RADIUS_METERS │ │
│ │ • STUDENT_NPM / STUDENT_NAMA │ │
│ │ • WEBHOOK URLs (PRODUCTION + TEST) │ │
│ │ • API_TIMEOUT_MS │ │
│ │ • PHOTO_QUALITY │ │
│ └──────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────┐
│ NETWORK LAYER │
│ (API Communication) │
│ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ N8nService │ │
│ │ │ │
│ │ submitAttendance() │ │
│ │ ├─ Serialize data to JSON │ │
│ │ ├─ Encode photo to Base64 │ │
│ │ ├─ POST to webhook URL │ │
│ │ ├─ Parse response code │ │
│ │ └─ Call callback (onSuccess / onError) │ │
│ │ │ │
│ │ submitCallback Interface: │ │
│ │ ├─ onSuccess(responseCode, message) │ │
│ │ └─ onError(throwable, message) │ │
│ └────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────┐
│ EXTERNAL SERVICES LAYER │
│ │
│ ┌─────────────────────┬──────────────────┬────────────────────────┐ │
│ │ Location Services │ Camera Services │ N8n Webhook Server │ │
│ │ │ │ │ │
│ │ • Google Play │ • Android Camera │ • Receive JSON data │ │
│ │ Services │ • Camera Intent │ • Process attendance │ │
│ │ • Fused Location │ • Photo capture │ • Store in database │ │
│ │ Provider │ │ • Return HTTP status │ │
│ │ • GPS coordinates │ │ │ │
│ │ │ │ │ │
│ └─────────────────────┴──────────────────┴────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
Data Flow Diagram
┌────────────┐
│ App Start │
└─────┬──────┘
│
▼
┌─────────────────────────────────────┐
│ Request Location Permission │
│ [LocationPermissionLauncher] │
└─────────────────────────────────────┘
│
├─ GRANTED ──────┐
│ │ DENIED
│ ▼
│ ┌─────────────────┐
│ │ Show Error │
│ │ Message │
│ └─────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Acquire GPS Location │
│ [FusedLocationProvider.lastLocation]│
└─────────────────────────────────────┘
│
├─ SUCCESS ──────────────┐
│ │ FAILED
│ ▼
│ ┌────────────────────┐
│ │ LocationError │
│ │ Show in ErrorAlert │
│ └────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Validate Location │
│ [LocationValidator.isLocationValid] │
│ - Calculate distance using │
│ Haversine formula │
│ - Compare with allowed radius │
└─────────────────────────────────────┘
│
├─ VALID ────┐
│ │ INVALID
│ ▼
│ ┌─────────────────────┐
│ │ Show Invalid Status │
│ │ Disable Submit Button│
│ └─────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Display LocationStatusCard │
│ - Show latitude/longitude │
│ - Show validation message │
│ - Show distance from reference │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ User clicks "Ambil Foto" │
│ [Request Camera Permission] │
└─────────────────────────────────────┘
│
├─ GRANTED ──────────────┐
│ │ DENIED
│ ▼
│ ┌────────────────────┐
│ │ PermissionError │
│ │ Show in ErrorAlert │
│ └────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Launch Camera Intent │
│ [MediaStore.ACTION_IMAGE_CAPTURE] │
└─────────────────────────────────────┘
│
├─ CAPTURED ─────┐
│ │ CANCELLED
│ ▼
│ ┌─────────────────┐
│ │ Retry or Skip │
│ └─────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Display Photo Preview │
│ [PhotoPreviewCard] │
│ - Show Bitmap │
│ - "Ambil Ulang" button │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Validate All Data │
│ ✓ Location acquired? │
│ ✓ Location valid? │
│ ✓ Photo captured? │
└─────────────────────────────────────┘
│
├─ ALL VALID ──┐
│ │ MISSING DATA
│ ▼
│ ┌──────────────────────┐
│ │ Show ValidationError │
│ │ Keep Submit Disabled │
│ └──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Enable Submit Button │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ User clicks "Kirim Absensi" │
│ Show Loading Spinner │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Prepare Request │
│ [N8nService.submitAttendance] │
│ - Serialize to JSON: │
│ {npm, nama, lat, lon, ts, foto} │
│ - Encode photo to Base64 │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ POST to N8n Webhook │
│ [HttpURLConnection] │
│ Timeout: 30 seconds │
└─────────────────────────────────────┘
│
├─ RESPONSE 200 ──┐
│ │ RESPONSE 4xx/5xx
│ │ or TIMEOUT
│ ▼
│ ┌────────────────────────┐
│ │ onError Called │
│ │ Show NetworkError │
│ │ Suggest Retry │
│ └────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ onSuccess Called │
│ - Hide loading spinner │
│ - Show success toast │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Wait 2 seconds │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Reset Form │
│ - Clear photo │
│ - Clear location │
│ - Reset validation state │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Ready for Next Attendance │
└─────────────────────────────────────┘
Class Diagram
┌─────────────────────────────────────────────────────────────────────────┐
│ DATA MODELS │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────┐ ┌──────────────────────────────┐ │
│ │ AttendanceState │ │ LocationData │ │
│ ├───────────────────────────┤ ├──────────────────────────────┤ │
│ │ + location: LocationData? │ │ + latitude: Double │ │
│ │ + foto: Bitmap? │ │ + longitude: Double │ │
│ │ + isLoadingLocation: Bool │ │ + accuracy: Float │ │
│ │ + isLoadingSubmit: Bool │ │ + timestamp: Long │ │
│ │ + validationResult │ └──────────────────────────────┘ │
│ │ + errorMessage: String? │ │
│ │ + isLocationPermission │ ┌──────────────────────────────┐ │
│ │ + isCameraPermission │ │ ValidationResult │ │
│ └───────────────────────────┘ ├──────────────────────────────┤ │
│ │ + isValid: Boolean │ │
│ ┌───────────────────────────┐ │ + message: String │ │
│ │ AttendanceRecord │ │ + status: ValidationStatus │ │
│ ├───────────────────────────┤ └──────────────────────────────┘ │
│ │ + npm: String │ │
│ │ + nama: String │ ┌──────────────────────────────┐ │
│ │ + latitude: Double │ │ ValidationStatus (enum) │ │
│ │ + longitude: Double │ ├──────────────────────────────┤ │
│ │ + timestamp: Long │ │ IDLE │ │
│ │ + foto: Bitmap? │ │ ACQUIRING │ │
│ │ + isValid: Boolean │ │ VALIDATING │ │
│ │ + validationMessage │ │ SUCCESS │ │
│ └───────────────────────────┘ │ OUT_OF_RANGE │ │
│ │ ERROR │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ SERVICES & UTILITIES │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ LocationValidator (object) │ │
│ ├──────────────────────────────────────────────────────────────────┤ │
│ │ + calculateDistance(lat1,lon1,lat2,lon2): Double │ │
│ │ + isLocationValid(lat,lon,radius): Boolean │ │
│ │ + getValidationMessage(lat,lon,radius): String │ │
│ │ + adjustCoordinates(lat,lon,latOff,lonOff): Pair<Double> │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ N8nService │ │
│ ├──────────────────────────────────────────────────────────────────┤ │
│ │ - activity: ComponentActivity │ │
│ ├──────────────────────────────────────────────────────────────────┤ │
│ │ + submitAttendance(npm,nama,lat,lon,foto,isTest,callback) │ │
│ │ - bitmapToBase64(bitmap): String │ │
│ │ + interface SubmitCallback │ │
│ │ - onSuccess(responseCode, message) │ │
│ │ - onError(error, message) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ ErrorHandler (object) │ │
│ ├──────────────────────────────────────────────────────────────────┤ │
│ │ + sealed class AttendanceError │ │
│ │ - NetworkError(message) │ │
│ │ - LocationError(message) │ │
│ │ - PermissionError(message) │ │
│ │ - ValidationError(message) │ │
│ │ - UnknownError(throwable) │ │
│ ├──────────────────────────────────────────────────────────────────┤ │
│ │ + getErrorMessage(error): String │ │
│ │ + getUserFriendlyMessage(error): String │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ AttendanceConfig (object) │ │
│ ├──────────────────────────────────────────────────────────────────┤ │
│ │ + REFERENCE_LATITUDE: Double │ │
│ │ + REFERENCE_LONGITUDE: Double │ │
│ │ + ALLOWED_RADIUS_METERS: Double │ │
│ │ + STUDENT_NPM: String │ │
│ │ + STUDENT_NAMA: String │ │
│ │ + WEBHOOK_PRODUCTION: String │ │
│ │ + WEBHOOK_TEST: String │ │
│ │ + API_TIMEOUT_MS: Int │ │
│ │ + PHOTO_QUALITY: Int │ │
│ └──────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ UI COMPONENTS (Compose) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ PhotoPreviewCard │ │
│ ├──────────────────────────────────────────────────────────────────┤ │
│ │ Parameters: │ │
│ │ - bitmap: Bitmap? │ │
│ │ - onRetake: () -> Unit │ │
│ │ - modifier: Modifier │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ LocationStatusCard │ │
│ ├──────────────────────────────────────────────────────────────────┤ │
│ │ Parameters: │ │
│ │ - latitude: Double? │ │
│ │ - longitude: Double? │ │
│ │ - validationMessage: String │ │
│ │ - isLoading: Boolean │ │
│ │ - modifier: Modifier │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ ErrorAlertCard │ │
│ ├──────────────────────────────────────────────────────────────────┤ │
│ │ Parameters: │ │
│ │ - message: String? │ │
│ │ - onDismiss: () -> Unit │ │
│ │ - modifier: Modifier │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ SubmitButtonWithLoader │ │
│ ├──────────────────────────────────────────────────────────────────┤ │
│ │ Parameters: │ │
│ │ - text: String │ │
│ │ - onClick: () -> Unit │ │
│ │ - isLoading: Boolean │ │
│ │ - isEnabled: Boolean │ │
│ │ - modifier: Modifier │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ AbsensiScreen (Composable) │ │
│ ├──────────────────────────────────────────────────────────────────┤ │
│ │ - Manages AttendanceState │ │
│ │ - Handles permissions │ │
│ │ - Orchestrates all UI components │ │
│ │ - Manages location acquisition │ │
│ │ - Handles photo capture │ │
│ │ - Manages form submission │ │
│ └──────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
Dependencies & Imports
AndroidX
├── androidx.core:core-ktx
├── androidx.lifecycle:lifecycle-runtime-ktx
├── androidx.activity:activity-compose
├── androidx.compose.* (UI, Material3, Tooling)
│
Android Platform
├── android.location (Location APIs)
├── android.hardware.camera (Camera)
├── android.content.pm (Permissions)
│
Google Play Services
├── com.google.android.gms:play-services-location
│ └── Fused Location Provider
│
Standard Libraries
├── java.net.HttpURLConnection (API calls)
├── org.json.JSONObject (JSON serialization)
├── java.util.Base64 (Photo encoding)
│
Testing
└── junit, androidx.test (Unit tests)
Architecture Design: ✅ Clean, Modular, Testable
Data Flow: ✅ Unidirectional with State Management
UI Pattern: ✅ Compose with Material 3
Error Handling: ✅ Sealed Classes + Callbacks
Scalability: ✅ Ready for future enhancements