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