137 Commits

Author SHA1 Message Date
sathwikcs
44c1f77bc8 Merge remote-tracking branch 'origin/generic-dev-sacnner-app' into generic-dev-sacnner-app 2025-03-17 17:49:38 +05:30
sathwikcs
f00774af08 new version 3.1.5 , fixed the bug where check out is showing after check in 2025-03-17 17:49:13 +05:30
Sathwik C S
e7c52b9f43 Edit ExampleUnitTest.kt 2025-03-03 09:23:13 +00:00
Sathwik C S
b507137b82 Edit ExampleUnitTest.kt 2025-03-03 09:22:48 +00:00
sathwikcs
b395df6268 new version 3.1.1 2025-03-03 14:37:41 +05:30
sathwikcs
27058baf99 new version 3.1.1 2025-02-10 15:33:00 +05:30
sathwikcs
419d5b6cf1 Removed the 5 min dialogue 2025-02-07 14:59:49 +05:30
sathwikcs
e0d13f3bce Removed the 5 min dialogue 2025-02-07 14:59:44 +05:30
sathwikcs
2ad327cde8 Added the Auto Update Check Feature 2025-02-07 14:59:00 +05:30
sathwikcs
196a127110 Added the Auto Update Check Feature 2025-02-07 14:58:50 +05:30
sathwikcs
951bd2f8c6 Added the Auto Update Check Feature 2025-02-07 14:58:42 +05:30
sathwikcs
9925b9b8aa Added the Auto Update Check Feature 2025-02-07 14:58:37 +05:30
sathwikcs
48b336aade Added the Auto Update Check Feature 2025-02-07 14:58:31 +05:30
sathwikcs
0b8320ce29 UpdateDialogue.kt 2025-02-06 16:03:47 +05:30
sathwikcs
767bc0ea75 NetworkErrorDialog 2025-02-06 16:01:30 +05:30
sathwikcs
00359d3b8d AttendanceScreenContent.kt 2025-02-06 15:30:52 +05:30
sathwikcs
4f40559417 AttendanceScreenContent.kt 2025-02-06 15:30:34 +05:30
sathwikcs
2fefeee7ac home 2025-02-06 15:29:44 +05:30
sathwikcs
02b660d1ca homeVM 2025-02-06 15:29:17 +05:30
sathwikcs
5f969ac651 Network Error Dialogue 2025-02-06 15:25:59 +05:30
sathwikcs
057f71e292 UpdateDialog 2025-02-06 15:23:01 +05:30
sathwikcs
74300239a3 AppUpdator.kt 2025-02-06 15:22:37 +05:30
sathwikcs
5ade8d4d83 removed unit test 2025-02-05 18:22:06 +05:30
sathwikcs
d501d806e7 final 2025-02-05 18:19:44 +05:30
sathwikcs
abbbd28095 homVM 2025-02-05 18:06:06 +05:30
sathwikcs
8f9c912f41 homVM 2025-02-05 18:05:34 +05:30
sathwikcs
70aafe73be homVM 2025-02-05 18:05:14 +05:30
sathwikcs
5a5da9fe72 Attend 2025-02-05 18:04:42 +05:30
sathwikcs
c9fd5d7993 icons 2025-02-05 17:16:43 +05:30
sathwikcs
30adc892d4 homeVM 2025-02-05 17:15:52 +05:30
sathwikcs
ed001bd266 NetworkErrorDialog 2025-02-05 17:15:13 +05:30
sathwikcs
60cb946518 Settings.kt 2025-02-05 17:13:30 +05:30
sathwikcs
053579b685 home.kt 2025-02-05 17:13:09 +05:30
sathwikcs
0736a481c2 bye Connectivity.kt 2025-02-05 17:12:42 +05:30
sathwikcs
ec9ae99e03 AttendanceScreenContent 2025-02-05 17:12:07 +05:30
sathwikcs
3ebfd949eb about Card 2025-02-05 17:11:43 +05:30
sathwikcs
af34397f88 removed unnecessary resources 2025-02-05 15:54:11 +05:30
sathwikcs
b1a0b7b753 other 2025-02-04 12:01:30 +05:30
sathwikcs
95a2cd7e93 DeveloperCard.kt 2025-02-04 12:01:07 +05:30
sathwikcs
945f9920fd homeVM 2025-02-04 12:00:42 +05:30
sathwikcs
7adc89b4f3 home 2025-02-04 12:00:14 +05:30
sathwikcs
1883ef2316 Color.kt 2025-02-03 17:00:58 +05:30
sathwikcs
97c3ac172d data 2025-02-03 17:00:47 +05:30
sathwikcs
9f69174cc7 ExampleUnitTest 2025-02-03 17:00:37 +05:30
sathwikcs
d7cb04a782 Settings.kt 2025-02-03 17:00:11 +05:30
sathwikcs
273fbd50be UpdateDialog 2025-02-03 16:59:45 +05:30
sathwikcs
140a01c208 NetworkMonitor 2025-02-03 16:59:23 +05:30
sathwikcs
10eac95eec UpdateChecker 2025-02-03 16:58:58 +05:30
sathwikcs
183904838d AttendanceScreenContent 2025-02-03 16:58:34 +05:30
sathwikcs
fa75748e2c AppUpdator.kt 2025-02-03 16:57:59 +05:30
sathwikcs
cccbecf934 AppModule.kt 2025-02-03 16:57:10 +05:30
sathwikcs
b5012f12a9 AboutCard.kt 2025-02-03 16:56:40 +05:30
sathwikcs
972ca81b03 updated unit test for time difference 2025-01-31 12:05:32 +05:30
sathwikcs
53a339c8a6 updated unit test for time difference 2025-01-30 18:27:03 +05:30
sathwikcs
0eb3f8290d other 2025-01-30 18:19:20 +05:30
sathwikcs
e41adc5720 homeVM 2025-01-30 18:19:09 +05:30
sathwikcs
b49997889d homeV 2025-01-30 18:18:42 +05:30
sathwikcs
6e2092a2b0 unit test 2025-01-30 17:20:46 +05:30
sathwikcs
89577cf867 homeVm 2025-01-30 17:20:23 +05:30
sathwikcs
506ac3a9b9 Error Content 2025-01-30 17:19:39 +05:30
sathwikcs
c1f60baffb Merge remote-tracking branch 'origin/generic-dev-sacnner-app' into generic-dev-sacnner-app 2025-01-27 18:01:17 +05:30
sathwikcs
f41af9f130 MainScreen.kt 2025-01-27 18:01:03 +05:30
sathwikcs
fe823fea1d MainScreen.kt 2025-01-27 18:00:26 +05:30
sathwikcs
77e9f5d2ac home 2025-01-27 18:00:01 +05:30
sathwikcs
78b408e41a NavigationItem.kt 2025-01-27 17:59:27 +05:30
sathwikcs
b18c9e33c2 Bye bye Navigation.kt 2025-01-27 17:59:07 +05:30
sathwikcs
3a976d85d1 Bye bye NavAnimation.kt 2025-01-27 17:58:42 +05:30
sathwikcs
72a1cea7a5 Bye bye BottomNavBar.kt 2025-01-27 17:58:18 +05:30
sathwikcs
beabe015ca AppClass.kt 2025-01-27 17:57:53 +05:30
sathwikcs
5b507bbefd minor update 2025-01-27 17:48:09 +05:30
sathwikcs
4fc60e4180 minor update 2025-01-24 16:04:32 +05:30
sathwikcs
edb933146b other.xml 2025-01-23 11:57:54 +05:30
sathwikcs
9684559e70 Update 2025-01-23 11:57:14 +05:30
sathwikcs
3de809e23f scanner 2025-01-23 11:56:48 +05:30
sathwikcs
9275095d66 LoadingOverlay 2025-01-23 11:56:13 +05:30
sathwikcs
744d92221e build.gradle.kts 2025-01-23 11:55:54 +05:30
sathwikcs
161ac01820 AttendanceStatusCard.kt 2025-01-22 12:50:11 +05:30
sathwikcs
d4641e8f4d ScannerContent 2025-01-22 12:49:48 +05:30
sathwikcs
e32ff01664 homeVM 2025-01-22 12:49:08 +05:30
sathwikcs
fc47c86049 AttendanceScreenContent 2025-01-22 12:48:34 +05:30
sathwikcs
e6e4586ee9 Navigation 2025-01-17 15:21:53 +05:30
sathwikcs
32124886c5 Navigation 2025-01-17 15:21:36 +05:30
sathwikcs
fdf160b7a1 homeVM 2025-01-17 15:21:16 +05:30
sathwikcs
cf6b44b083 NetworkMonitor 2025-01-16 14:59:02 +05:30
sathwikcs
f7d540c6eb NetworkMonitor 2025-01-16 14:57:11 +05:30
sathwikcs
ff3a90aa9a MainScreen 2025-01-16 14:55:21 +05:30
sathwikcs
196d9f8eb5 DeveloperCard.kt 2025-01-16 14:54:32 +05:30
sathwikcs
65b95eda2f data 2025-01-16 14:54:01 +05:30
sathwikcs
7a22437bd5 AppUpdater 2025-01-16 14:53:31 +05:30
sathwikcs
1482c2b548 AppUpdater 2025-01-16 14:53:16 +05:30
sathwikcs
bdda880414 other.xml 2025-01-16 14:52:08 +05:30
sathwikcs
e137b0695f build.gradle.kts 2025-01-16 14:51:55 +05:30
sathwikcs
52882ac8a8 AndroidManifest.xml 2025-01-16 14:51:26 +05:30
sathwikcs
bc7cb275db provider_paths.xml 2025-01-16 14:51:00 +05:30
sathwikcs
ac10e6ac01 Qrscanner.kt 2025-01-16 14:50:41 +05:30
sathwikcs
58581101a5 TopComponents 2025-01-16 14:50:25 +05:30
sathwikcs
6054ba4ac1 UpdateDialog 2025-01-16 14:50:06 +05:30
sathwikcs
bbfd204b7d fixed battery issue 2024-12-27 13:28:26 +05:30
sathwikcs
0920d72d2e fixed battery issue 2024-12-27 13:16:11 +05:30
sathwikcs
c45af45481 fixed battery issue 2024-12-27 13:15:39 +05:30
sathwikcs
8731f27f83 fixed battery issue 2024-12-27 13:15:12 +05:30
sathwikcs
ac9728174a minor changes 2024-12-27 13:14:47 +05:30
sathwikcs
f1c9af72b7 removed request permission 2024-12-12 15:44:34 +05:30
sathwikcs
bcd8c77887 removed request permission 2024-12-12 15:44:17 +05:30
sathwikcs
71dfac73f6 added the network check 2024-12-12 15:43:31 +05:30
sathwikcs
cb628a684b added the network check 2024-12-12 15:43:13 +05:30
sathwikcs
66832733d9 added the network check 2024-12-12 15:43:02 +05:30
sathwikcs
c47f75e9f2 added the network check 2024-12-11 12:32:46 +05:30
sathwikcs
3d144f3876 added the network check 2024-12-11 12:31:09 +05:30
sathwikcs
5afbcc6670 added the network check 2024-12-11 12:29:57 +05:30
sathwikcs
58dd00db11 added the network check 2024-12-11 12:29:43 +05:30
sathwikcs
810bbfc327 Integrated the permission within the home screen 2024-10-26 18:29:11 +05:30
sathwikcs
1efbb9af0f fixed the issue where login will automatically logout some people, fixed the ui bug 2024-10-26 18:28:23 +05:30
sathwikcs
204a0084da changed build.gradle.kts 2024-10-26 18:02:24 +05:30
sathwikcs
fcc831a63d QRscanne 2024-10-25 17:10:11 +05:30
sathwikcs
ac3a4ebe0f home kt 2024-10-25 17:09:41 +05:30
sathwikcs
ee1f6b8884 removed the old logo 2024-10-25 16:44:06 +05:30
sathwikcs
e435e0ff14 personalised error message , now it shows the check out time if already checked Out 2024-10-25 16:42:49 +05:30
sathwikcs
de07673a29 fixed the issue where the checkIn automatically checks out people , added the personalised error message , now it shows the check out time if already checked Out 2024-10-25 16:42:08 +05:30
sathwikcs
de4a5c9f8d - lifecycle process 2024-10-25 16:40:30 +05:30
sathwikcs
6c322efd93 removed the Large button 2024-10-25 16:39:30 +05:30
sathwikcs
72d1fcdaea updated the gradle 2024-10-25 16:37:20 +05:30
sathwikcs
7d51ac210e new Launcher Icon 2024-10-25 16:36:47 +05:30
sathwikcs
fc803c6fe2 libs 2024-10-24 09:43:08 +05:30
sathwikcs
7cc572f201 deploymentTargetSelector 2024-10-24 09:42:48 +05:30
sathwikcs
bb59f43dfe TopComponents 2024-10-24 09:42:30 +05:30
sathwikcs
a48f7ff007 Settings 2024-10-24 09:42:06 +05:30
sathwikcs
c5b52711d0 AppModule 2024-10-24 09:41:47 +05:30
sathwikcs
2f70040f54 manifest 2024-10-24 09:41:34 +05:30
sathwikcs
fcbda41419 App Class 2024-10-23 23:22:44 +05:30
sathwikcs
13556dd84c QR scanner 2024-10-23 23:22:27 +05:30
sathwikcs
7d63df0c83 Permission 2024-10-23 23:22:10 +05:30
sathwikcs
814f9c5c48 main 2024-10-23 23:21:59 +05:30
sathwikcs
e8cc833455 gradle 2024-10-23 23:21:41 +05:30
sathwikcs
cdc3a9cf75 gradle 2024-10-23 23:21:05 +05:30
sathwikcs
6b9f54184e Navigation 2024-10-23 23:18:58 +05:30
sathwikcs
10b06b2019 removed the unnecessary things 2024-10-23 23:18:29 +05:30
99 changed files with 1844 additions and 2173 deletions

View File

@@ -4,19 +4,16 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2024-10-16T06:25:13.029795100Z">
<DropdownSelection timestamp="2025-02-06T09:50:04.018268400Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\sathw\.android\avd\Pixel_3a_XL_API_34.avd" />
<DeviceId pluginId="PhysicalDevice" identifier="serial=R9ZW605ZHAY" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="lofpre">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="checkTime">
<SelectionState runConfigName="AnimatedPreloader">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>

222
.idea/other.xml generated
View File

@@ -14,6 +14,28 @@
<option name="screenX" value="720" />
<option name="screenY" value="1280" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="OnePlus" />
<option name="codename" value="OP5552L1" />
<option name="id" value="OP5552L1" />
<option name="manufacturer" value="OnePlus" />
<option name="name" value="CPH2415" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2412" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="OPPO" />
<option name="codename" value="OP573DL1" />
<option name="id" value="OP573DL1" />
<option name="manufacturer" value="OPPO" />
<option name="name" value="CPH2557" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="28" />
<option name="brand" value="DOCOMO" />
@@ -36,6 +58,28 @@
<option name="screenX" value="1840" />
<option name="screenY" value="2944" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="a15" />
<option name="id" value="a15" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="A15" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="a35x" />
<option name="id" value="a35x" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="A35" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />
@@ -58,6 +102,28 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="motorola" />
<option name="codename" value="arcfox" />
<option name="id" value="arcfox" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="razr plus 2024" />
<option name="screenDensity" value="360" />
<option name="screenX" value="1080" />
<option name="screenY" value="1272" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="motorola" />
<option name="codename" value="austin" />
<option name="id" value="austin" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="moto g 5G (2022)" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
@@ -113,6 +179,17 @@
<option name="screenX" value="2220" />
<option name="screenY" value="1080" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="dm2q" />
<option name="id" value="dm2q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="S23 Plus" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
@@ -135,6 +212,28 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="e3q" />
<option name="id" value="e3q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S24 Ultra" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1440" />
<option name="screenY" value="3120" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="eos" />
<option name="id" value="eos" />
<option name="manufacturer" value="Google" />
<option name="name" value="Eos" />
<option name="screenDensity" value="320" />
<option name="screenX" value="384" />
<option name="screenY" value="384" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
@@ -168,6 +267,50 @@
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="motorola" />
<option name="codename" value="fogona" />
<option name="id" value="fogona" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="moto g play - 2024" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="g0q" />
<option name="id" value="g0q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-S906U1" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gta9pwifi" />
<option name="id" value="gta9pwifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-X210" />
<option name="screenDensity" value="240" />
<option name="screenX" value="1200" />
<option name="screenY" value="1920" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts7xllite" />
<option name="id" value="gts7xllite" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-T738U" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
@@ -179,6 +322,28 @@
<option name="screenX" value="1848" />
<option name="screenY" value="2960" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts8wifi" />
<option name="id" value="gts8wifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S8" />
<option name="screenDensity" value="274" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts9fe" />
<option name="id" value="gts9fe" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S9 FE 5G" />
<option name="screenDensity" value="280" />
<option name="screenX" value="1440" />
<option name="screenY" value="2304" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
@@ -223,6 +388,28 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="motorola" />
<option name="codename" value="maui" />
<option name="id" value="maui" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="moto g play - 2023" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="o1q" />
<option name="id" value="o1q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S21" />
<option name="screenDensity" value="421" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="google" />
@@ -262,7 +449,7 @@
<option name="codename" value="q6q" />
<option name="id" value="q6q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-F956B" />
<option name="name" value="Galaxy Z Fold6" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1856" />
<option name="screenY" value="2160" />
@@ -279,6 +466,17 @@
<option name="screenY" value="384" />
<option name="type" value="WEAR_OS" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="r11q" />
<option name="id" value="r11q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-S711U" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
@@ -301,6 +499,17 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="t2q" />
<option name="id" value="t2q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S21 Plus" />
<option name="screenDensity" value="394" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
@@ -323,6 +532,17 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="35" />
<option name="brand" value="google" />
<option name="codename" value="tokay" />
<option name="id" value="tokay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
</list>
</option>
</component>

2
.idea/vcs.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -4,7 +4,6 @@ plugins {
id("com.google.gms.google-services")
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
}
android {
@@ -15,8 +14,9 @@ android {
applicationId = "in.sminnovations.attendanceapp.user"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0"
versionCode = 11
versionName = "3.1.5"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -26,11 +26,12 @@ android {
buildTypes {
release {
isMinifyEnabled = false
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
}
}
compileOptions {
@@ -55,6 +56,7 @@ android {
kapt {
correctErrorTypes = true
}
}
dependencies {
@@ -103,4 +105,7 @@ dependencies {
implementation("com.google.firebase:firebase-analytics")
implementation ("com.google.firebase:firebase-firestore-ktx:25.1.0") // Use the latest version
implementation("com.google.firebase:firebase-firestore")
implementation ("com.squareup.okhttp3:okhttp:4.11.0")
}

View File

@@ -4,11 +4,18 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-feature android:name="android.hardware.camera" android:required="false" />
<application
android:name=".MyApp"
android:name=".main.MyApp"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@@ -19,14 +26,25 @@
android:theme="@style/Theme.SMIAttendance"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:name=".main.MainAppActivity"
android:exported="true"
android:theme="@style/Theme.SMIAttendance">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,41 +0,0 @@
package `in`.sminnovations.attendanceapp
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import `in`.sminnovations.attendanceapp.screens.login.ui.LoginScreen
@Composable
fun AppNavigation() {
val navController = rememberNavController()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
NavigationHost(navController = navController)
}
}
@RequiresApi(Build.VERSION_CODES.O_MR1)
@Composable
fun NavigationHost(navController: NavHostController) {
NavHost(navController = navController, startDestination = "login") {
composable("login") {
LoginScreen(
loginViewModel = hiltViewModel(),
onLoginSuccess = {
navController.navigate("main") {
popUpTo("login") { inclusive = true }
}
}
)
}
composable("main") {
MainScreen(navController)
}
}
}

View File

@@ -1,22 +0,0 @@
package `in`.sminnovations.attendanceapp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import dagger.hilt.android.AndroidEntryPoint
import `in`.sminnovations.attendanceapp.ui.theme.SMIAttendanceTheme
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
SMIAttendanceTheme {
AppNavigation()
}
}
}
}

View File

@@ -1,30 +0,0 @@
package `in`.sminnovations.attendanceapp
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import `in`.sminnovations.attendanceapp.navigation.BottomNavigationBar
import `in`.sminnovations.attendanceapp.navigation.Navigation
import `in`.sminnovations.attendanceapp.navigation.data.navItems
@Composable
fun MainScreen(
mainNavController: NavController
) {
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigationBar(navController, navItems)
},
content = { padding ->
Box(modifier = Modifier.padding(padding)) {
Navigation(navController = navController, mainNavHostController = mainNavController, items = navItems)
}
}
)
}

View File

@@ -1,14 +0,0 @@
package `in`.sminnovations.attendanceapp.data.dataStore
data class Employee(
val androidID: String = "",
val dateOfJoining: String = "",
val dateOfLeaving: String = "",
val designation: String = "Android Developer",
val employeeStatus: Boolean = true,
val id: String = "",
val name: String = "",
val password: String = "",
val photo: String = "",
val dateOfBirth: String = ""
)

View File

@@ -1,17 +0,0 @@
package `in`.sminnovations.attendanceapp.data.dataStore
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
object EmployeePreferencesKeys {
val ANDROID_ID = stringPreferencesKey("android_id")
val DATE_OF_JOINING = stringPreferencesKey("date_of_joining")
val DATE_OF_LEAVING = stringPreferencesKey("date_of_leaving")
val DESIGNATION = stringPreferencesKey("designation")
val EMPLOYEE_STATUS = booleanPreferencesKey("employee_status")
val ID = stringPreferencesKey("id")
val NAME = stringPreferencesKey("name")
val PASSWORD = stringPreferencesKey("password")
val PHOTO = stringPreferencesKey("photo")
val DATE_OF_BIRTH = stringPreferencesKey("date_of_birth")
}

View File

@@ -1,83 +0,0 @@
package `in`.sminnovations.attendanceapp.data.dataStore
import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import `in`.sminnovations.attendanceapp.screens.settings.data.EmployeeDataSettings
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton
val Context.employeeDataStore by preferencesDataStore("employee_data")
@Singleton
class EmployeeRepository @Inject constructor(
@ApplicationContext private val context: Context
) {
suspend fun saveEmployeeData(employee: Employee) {
context.employeeDataStore.edit { preferences ->
preferences[EmployeePreferencesKeys.ANDROID_ID] = employee.androidID
preferences[EmployeePreferencesKeys.DATE_OF_JOINING] = employee.dateOfJoining
preferences[EmployeePreferencesKeys.DATE_OF_LEAVING] = employee.dateOfLeaving
preferences[EmployeePreferencesKeys.DESIGNATION] = employee.designation
preferences[EmployeePreferencesKeys.EMPLOYEE_STATUS] = employee.employeeStatus
preferences[EmployeePreferencesKeys.ID] = employee.id
preferences[EmployeePreferencesKeys.NAME] = employee.name
preferences[EmployeePreferencesKeys.PASSWORD] = employee.password
preferences[EmployeePreferencesKeys.PHOTO] = employee.photo
preferences[EmployeePreferencesKeys.DATE_OF_BIRTH] = employee.dateOfBirth
}
}
fun getEmployeeData(): Flow<Employee> {
return context.employeeDataStore.data
.catch { exception ->
// If an error occurs, emit an empty preferences object
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
Employee(
androidID = preferences[EmployeePreferencesKeys.ANDROID_ID] ?: "",
dateOfJoining = preferences[EmployeePreferencesKeys.DATE_OF_JOINING] ?: "",
dateOfLeaving = preferences[EmployeePreferencesKeys.DATE_OF_LEAVING] ?: "",
designation = preferences[EmployeePreferencesKeys.DESIGNATION] ?: "Android Developer",
employeeStatus = preferences[EmployeePreferencesKeys.EMPLOYEE_STATUS] ?: true,
id = preferences[EmployeePreferencesKeys.ID] ?: "",
name = preferences[EmployeePreferencesKeys.NAME] ?: "",
password = preferences[EmployeePreferencesKeys.PASSWORD] ?: "",
photo = preferences[EmployeePreferencesKeys.PHOTO] ?: "",
dateOfBirth = preferences[EmployeePreferencesKeys.DATE_OF_BIRTH] ?: ""
)
}
}
fun getEmployeeDataSettings(): Flow<EmployeeDataSettings> {
return context.employeeDataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
EmployeeDataSettings(
name = preferences[EmployeePreferencesKeys.NAME] ?: "",
id = preferences[EmployeePreferencesKeys.ID] ?: "",
designation = preferences[EmployeePreferencesKeys.DESIGNATION] ?: ""
)
}
}
}

View File

@@ -1,42 +1,25 @@
package `in`.sminnovations.attendanceapp.di
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import com.google.firebase.firestore.FirebaseFirestore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import `in`.sminnovations.attendanceapp.screens.home.data.CheckInCheckOutDataStore
import `in`.sminnovations.attendanceapp.screens.login.data.LoginDataStore
import `in`.sminnovations.attendanceapp.utils.NetworkMonitor
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
private val Context.dataStore by preferencesDataStore(name = "employee_data")
@Provides
@Singleton
fun provideAppContext(@ApplicationContext context: Context): Context {
return context
}
@Provides
@Singleton
fun provideLoginDataStore(@ApplicationContext context: Context): LoginDataStore {
return LoginDataStore(context)
}
@Provides
@Singleton
fun provideCheckInCheckOutDataStore(@ApplicationContext context: Context): CheckInCheckOutDataStore {
return CheckInCheckOutDataStore(context)
}
@Provides
@Singleton
fun provideFirebase(): FirebaseFirestore {
@@ -45,13 +28,7 @@ object AppModule {
@Provides
@Singleton
fun provideEmployeeDataStore(@ApplicationContext context: Context): DataStore<Preferences> {
return context.dataStore
fun provideNetworkMonitor(@ApplicationContext context: Context): NetworkMonitor {
return NetworkMonitor(context)
}
// fun provideFirebase(): FirebaseFirestore {
// return FirebaseFirestore.getInstance()
// }
}

View File

@@ -1,13 +1,9 @@
package `in`.sminnovations.attendanceapp
// File: MyApp.kt
package `in`.sminnovations.attendanceapp.main
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class MyApp : Application(){
}
class MyApp : Application()

View File

@@ -0,0 +1,42 @@
package `in`.sminnovations.attendanceapp.main
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import dagger.hilt.android.AndroidEntryPoint
import `in`.sminnovations.attendanceapp.ui.theme.SMIAttendanceTheme
@AndroidEntryPoint
class MainAppActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (shouldKeepScreenOn()) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
enableEdgeToEdge()
setContent {
SMIAttendanceTheme {
MainScreen()
}
}
}
private fun shouldKeepScreenOn(): Boolean {
val batteryStatus = registerReceiver(
null,
IntentFilter(Intent.ACTION_BATTERY_CHANGED)
) ?: return false
val level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
val isCharging = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) > 0
val batteryPct = level * 100 / scale
return batteryPct > 50 || isCharging
}
}

View File

@@ -0,0 +1,21 @@
package `in`.sminnovations.attendanceapp.main
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import `in`.sminnovations.attendanceapp.screens.home.ui.HomeScreen
@Composable
fun MainScreen(
) {
Scaffold(
content = { padding ->
Box(modifier = Modifier.padding(padding)) {
HomeScreen()
}
}
)
}

View File

@@ -1,48 +0,0 @@
package `in`.sminnovations.attendanceapp.navigation
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState
import `in`.sminnovations.attendanceapp.navigation.data.BottomNavItem
@Composable
fun BottomNavigationBar(
navController: NavController,
items: List<BottomNavItem>,
) {
NavigationBar {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
items.forEach { item ->
NavigationBarItem(icon = {
BottomNavAnimatedIcon(
isSelected = currentRoute == item.route,
icon = item.icon,
selectedIcon = item.selectedIcon,
contentDescription = item.label
)
},
label = { Text(text = item.label) },
alwaysShowLabel = true,
selected = currentRoute == item.route,
onClick = {
navController.navigate(item.route) {
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
})
}
}
}

View File

@@ -1,33 +0,0 @@
package `in`.sminnovations.attendanceapp.navigation
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
@Composable
fun BottomNavAnimatedIcon(
isSelected: Boolean,
icon: ImageVector,
selectedIcon: ImageVector,
contentDescription: String?
) {
AnimatedContent(
targetState = isSelected,
transitionSpec = {
fadeIn(animationSpec = tween(300)) togetherWith fadeOut(animationSpec = tween(300))
}, label = "animated icon"
) { targetState ->
Icon(
imageVector = if (targetState) selectedIcon else icon,
contentDescription = contentDescription,
tint = if (targetState) MaterialTheme.colorScheme.surface else MaterialTheme.colorScheme.onSurface
)
}
}

View File

@@ -1,33 +0,0 @@
package `in`.sminnovations.attendanceapp.navigation
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import `in`.sminnovations.attendanceapp.navigation.data.BottomNavItem
import `in`.sminnovations.attendanceapp.screens.history.ui.HistoryScreen
import `in`.sminnovations.attendanceapp.screens.home.ui.HomeScreen
import `in`.sminnovations.attendanceapp.screens.settings.ui.SettingsScreen
@Composable
fun Navigation(
mainNavHostController: NavController,
navController: NavHostController,
items: List<BottomNavItem>,
) {
NavHost(navController, startDestination = items[0].route) {
composable(items[0].route) {
HomeScreen()
}
composable(items[1].route) {
HistoryScreen()
}
composable(items[2].route) {
SettingsScreen(mainNavHostController)
}
}
}

View File

@@ -1,23 +0,0 @@
package `in`.sminnovations.attendanceapp.navigation.data
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddCircle
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.outlined.AddCircle
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.ui.graphics.vector.ImageVector
data class BottomNavItem(
val icon: ImageVector,
val selectedIcon: ImageVector,
val label: String,
val route: String
)
val navItems = listOf(
BottomNavItem(Icons.Outlined.Search, Icons.Filled.Search, "Scan", "scan"),
BottomNavItem(Icons.Outlined.AddCircle, Icons.Filled.AddCircle, "Records", "records"),
BottomNavItem(Icons.Outlined.Settings, Icons.Filled.Settings, "Settings", "settings")
)

View File

@@ -1,73 +0,0 @@
package `in`.sminnovations.attendanceapp.screens.history.components
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import `in`.sminnovations.attendanceapp.screens.history.data.AttendanceData
@Composable
fun EnhancedAttendanceCard(attendance: AttendanceData) {
var isExpanded by remember { mutableStateOf(false) }
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp)
.animateContentSize(), // Animate the size change for a smooth expand/collapse
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(6.dp)
) {
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.primaryContainer)
.padding(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = attendance.date,
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
color = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.weight(1f)
)
IconButton(
onClick = { isExpanded = !isExpanded }
) {
Icon(
imageVector = Icons.Default.ArrowDropDown,
contentDescription = "Expand or Collapse",
tint = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
if (isExpanded) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Check In: ${attendance.checkInTime ?: "N/A"}",
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Text(
text = "Check Out: ${attendance.checkOutTime ?: "N/A"}",
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
}
}

View File

@@ -1,9 +0,0 @@
package `in`.sminnovations.attendanceapp.screens.history.data
import com.google.firebase.Timestamp
data class AttendanceData(
val date: String,
val checkInTime: String?,
val checkOutTime: String?
)

View File

@@ -1,62 +0,0 @@
package `in`.sminnovations.attendanceapp.screens.history.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import `in`.sminnovations.attendanceapp.screens.history.components.EnhancedAttendanceCard
import `in`.sminnovations.attendanceapp.screens.history.viewModel.HistoryViewModel
import `in`.sminnovations.attendanceapp.screens.login.viewModel.LoginViewModel
@Composable
fun HistoryScreen(
historyViewModel: HistoryViewModel = hiltViewModel(),
) {
val attendanceData = historyViewModel.listOfAttendanceDates.collectAsState().value
Column(modifier = Modifier.fillMaxSize()) {
Row (
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
){
Text(
text = "Your Attendance History",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(16.dp),
color = MaterialTheme.colorScheme.primary
)
Button(onClick = {historyViewModel.refresh() }) {
Text(text = "Refresh")
}
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 8.dp)
) {
items(attendanceData) { attendance ->
EnhancedAttendanceCard(attendance = attendance)
}
}
}
}

View File

@@ -1,97 +0,0 @@
package `in`.sminnovations.attendanceapp.screens.history.viewModel
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.firebase.Timestamp
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.toObject
import dagger.hilt.android.lifecycle.HiltViewModel
import `in`.sminnovations.attendanceapp.screens.history.data.AttendanceData
import `in`.sminnovations.attendanceapp.screens.login.data.LoginDataStore
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject
@HiltViewModel
class HistoryViewModel @Inject constructor(
private val loginDataStore: LoginDataStore,
private val firebaseFirestore: FirebaseFirestore,
) : ViewModel() {
private val _isLoggedIn = MutableStateFlow(false)
val isLoggedIn = _isLoggedIn.asStateFlow()
private val _listOfAttendanceDates = MutableStateFlow<List<AttendanceData>>(emptyList())
val listOfAttendanceDates = _listOfAttendanceDates.asStateFlow()
init {
viewModelScope.launch {
getAttendanceData()
}
}
fun refresh() {
viewModelScope.launch {
getAttendanceData()
}
}
private fun getAttendanceData() {
viewModelScope.launch {
loginDataStore.loginId.collect { id ->
firebaseFirestore.collection("attendance_$id").get()
.addOnSuccessListener { result ->
viewModelScope.launch {
val attendanceList = result.documents.mapNotNull { document ->
val date = document.id // Assuming the document ID is the date
val checkInTimestamp = document.getString("checkIn") ?: "NA"
val checkOutTimestamp = document.getString("checkOut") ?: "NA"
val checkIn = formatTimestamp(checkInTimestamp)
val checkOut = formatTimestamp(checkOutTimestamp)
AttendanceData(date, checkIn, checkOut)
}
_listOfAttendanceDates.value = attendanceList
}
}
.addOnFailureListener { exception ->
Log.e("Firestore", "Error getting documents: ", exception)
viewModelScope.launch {
_listOfAttendanceDates.value = emptyList()
}
}
}
}
}
private fun formatTimestamp(timestamp: Any): String {
val sdf = SimpleDateFormat("dd MMMM yyyy hh:mm a", Locale.getDefault())
return if (timestamp is Timestamp) {
sdf.format(timestamp.toDate())
} else {
timestamp.toString()
}
}
}

View File

@@ -1,74 +0,0 @@
package `in`.sminnovations.attendanceapp.screens.home.data
import android.content.Context
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Singleton
private const val DATASTORE_NAME = "check_in_check_out_prefs"
private val Context.dataStore by preferencesDataStore(name = DATASTORE_NAME)
@Singleton
class CheckInCheckOutDataStore @Inject constructor(@ApplicationContext context: Context) {
private val dataStore = context.dataStore
private val CHECK_IN_KEY = stringPreferencesKey("check_in_id")
private val CHECK_OUT_KEY = stringPreferencesKey("check_out_id")
private val CHECK_IN_Date = stringPreferencesKey("check_in_date_id")
private val IS_CHECK_IN = booleanPreferencesKey("is_checked_in_id")
private val IS_CHECK_OUT = booleanPreferencesKey("is_checked_in_id")
val checkInId: Flow<String> = dataStore.data.map { preferences ->
preferences[CHECK_IN_KEY] ?: ""
}
val checkOutId: Flow<String> = dataStore.data.map { preferences ->
preferences[CHECK_OUT_KEY] ?: ""
}
val checkInDate: Flow<String> = dataStore.data.map { preferences ->
preferences[CHECK_IN_Date] ?: ""
}
suspend fun saveLoginData(checkOut: String) {
dataStore.edit { preferences ->
preferences[CHECK_OUT_KEY] = checkOut
}
}
suspend fun saveLoginDate(checkDate: String,checkInTime: String) {
dataStore.edit { preferences ->
preferences[CHECK_IN_Date] = checkDate
preferences[CHECK_IN_KEY] = checkInTime
}
}
suspend fun saveIsLogIn(isCheckIn: Boolean) {
dataStore.edit { preferences ->
preferences[IS_CHECK_IN] = isCheckIn
}
}
suspend fun clearLoginData() {
dataStore.edit { preferences ->
preferences.remove(CHECK_IN_Date)
preferences.remove(IS_CHECK_OUT)
preferences.remove(IS_CHECK_IN)
preferences.remove(CHECK_IN_KEY)
preferences.remove(CHECK_OUT_KEY)
}
}
}

View File

@@ -0,0 +1,14 @@
package `in`.sminnovations.attendanceapp.screens.home.data
data class AttendanceUiState(
val isScanning: Boolean = false,
val qrCodeValue: String? = null,
val userName: String? = null,
val isCheckedIn: Boolean = false,
val isCheckedOut: Boolean = false,
val checkedOutTime: String = "",
val errorMessage: String? = null,
val isLoading: Boolean = false,
val isConnected : Boolean =true,
val isAppDisabled : Boolean = false,
)

View File

@@ -0,0 +1,86 @@
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.isGranted
import `in`.sminnovations.attendanceapp.R
import `in`.sminnovations.attendanceapp.screens.home.data.AttendanceUiState
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.TopComponents
@OptIn(ExperimentalPermissionsApi::class)
@Composable
internal fun AttendanceScreenContent(
uiState: AttendanceUiState,
cameraPermissionState: PermissionState,
onScannerToggle: () -> Unit,
onClickSettings: () -> Unit,
onQrCodeScanned: (String) -> Unit,
modifier: Modifier = Modifier
) {
Box(modifier = modifier.fillMaxSize()) {
IconButton(onClick = onClickSettings) {
Icon(painter = painterResource(id = R.drawable.baseline_settings_24), contentDescription ="settings" )
}
Column {
when {
!cameraPermissionState.status.isGranted -> {
RequestPermissionsContent(cameraPermissionState)
}
uiState.isScanning -> {
ScannerContent(
onQrCodeScanned = onQrCodeScanned, onClose = onScannerToggle
)
}
uiState.errorMessage != null -> {
ErrorContent(
errorMessage = uiState.errorMessage,
userName = uiState.userName,
checkedOutTime = uiState.checkedOutTime
)
}
uiState.isCheckedIn -> {
AttendanceStatusCard(
title = "Checked In as", userName = uiState.userName
)
}
uiState.isCheckedOut -> {
AttendanceStatusCard(
title = "Checked Out as", userName = uiState.userName
)
}
}
}
if (uiState.isLoading) {
LoadingOverlay()
}
if (!uiState.isScanning && uiState.errorMessage == null && !uiState.isCheckedIn && !uiState.isCheckedOut && cameraPermissionState.status.isGranted){
Box(modifier =Modifier.align(Alignment.Center) ){
TopComponents()}
}
if (!uiState.isScanning && uiState.errorMessage == null && !uiState.isCheckedIn && !uiState.isCheckedOut && !cameraPermissionState.status.isGranted){
TopComponents()
}
ScannerToggleButton(
isScanning = uiState.isScanning,
onClick = onScannerToggle,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}

View File

@@ -0,0 +1,44 @@
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
internal fun AttendanceStatusCard(
title: String, userName: String?
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
OutlinedCard {
Text(
text = title,
letterSpacing = 2.sp,
lineHeight = 40.sp,
fontSize = 30.sp,
color = Color.Green,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(10.dp, 5.dp)
)
}
Spacer(modifier = Modifier.height(10.dp))
Text(
text = userName ?: "",
letterSpacing = 2.sp,
lineHeight = 40.sp,
fontSize = 30.sp,
fontWeight = FontWeight.Bold
)
}
}

View File

@@ -1,2 +0,0 @@
package `in`.sminnovations.attendanceapp.screens.home.ui.componets

View File

@@ -0,0 +1,131 @@
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
internal fun ErrorContent(
errorMessage: String,
userName: String?,
checkedOutTime: String
) {
val errorExplanations = mapOf(
// Network and Data Access Errors
"Failed to fetch employee data" to "There was an issue retrieving your employee information. Please check your internet connection and try again.",
"Failed to fetch attendance data" to "Unable to retrieve attendance records. Please verify your internet connection and try again.",
// Employee Validation Errors
"Invalid employee ID" to "The scanned ID isn't recognized. Double-check the ID, and if the issue continues, contact your administrator.",
"Employee ID has expired" to "This ID is no longer active. Please contact HR to renew or reactivate your ID.",
// Attendance Process Errors
"Invalid attendance record" to "An unexpected issue occurred with your attendance record. Please contact support for assistance.",
"Already checked out for today." to "You've already completed your check-out for today. If you need to make corrections, please contact support.",
"Failed to check in" to "There was an issue recording your check-in. Please try again or contact support if the problem persists.",
"Failed to check out" to "There was an issue recording your check-out. Please try again or contact support if the problem persists.",
// Generic Error
"An error occurred" to "An unexpected error occurred. Please try again or contact support if the issue continues.",
//Quick Login Log off
"Wait 5 minutes before logging off" to "You have just Logged in You need to wait 5 minutes before logging in \n This behaviour may be notified to Admin 😊"
)
val detailedExplanation = errorExplanations[errorMessage] ?: "An unexpected error occurred."
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Only show the "Error Message for" text if we have a valid userName
if (!userName.isNullOrEmpty()) {
OutlinedCard(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
) {
Text(
text = buildAnnotatedString {
append("Error Message for ")
withStyle(style = SpanStyle(color = Color.Green)) {
append(userName)
}
},
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(16.dp)
)
}
}
OutlinedCard(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
) {
Text(
text = errorMessage,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(16.dp)
)
}
// Detailed explanation for the error
OutlinedCard(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = if (checkedOutTime.isNotEmpty()) 16.dp else 0.dp)
) {
Text(
letterSpacing = 2.sp,
lineHeight = 40.sp,
text = detailedExplanation,
fontSize = 18.sp,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(16.dp)
)
}
if (checkedOutTime.isNotEmpty()) {
OutlinedCard(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Check Out Time",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = checkedOutTime,
style = MaterialTheme.typography.titleLarge,
color = Color.Green
)
}
}
}
}
}

View File

@@ -1,40 +0,0 @@
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun LargeButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Button(
onClick = onClick,
modifier = modifier
.height(56.dp)
.width(200.dp),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
),
shape = RoundedCornerShape(28.dp)
) {
Text(
text = text,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
}
}

View File

@@ -0,0 +1,30 @@
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
internal fun LoadingOverlay() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.3f)),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.primary, modifier = Modifier.size(48.dp)
)
}
}

View File

@@ -0,0 +1,68 @@
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.subSetting.AboutSettings
@Composable
fun NetworkErrorDialog(
showDialog: Boolean,
onRetry: () -> Unit,
onCheckForUpdate: () -> Unit
) {
if (!showDialog) return
var showSettingDialog by remember { mutableStateOf(false) }
val context = LocalContext.current
AlertDialog(
onDismissRequest = { /* Prevent dismissing by tapping outside */ },
title = {
Text(
text = if (showSettingDialog) "Settings" else "Network Error",
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
},
text = {
if (showSettingDialog) {
AboutSettings(context = context, onCheckForUpdate)
} else {
Text(
text = "Network is not connected. Please check your Wi-Fi or mobile data.\n\nIf Wi-Fi is just connected, wait for a while and then press retry.",
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
modifier = Modifier.padding(vertical = 8.dp)
)
}
},
confirmButton = {
if (!showSettingDialog) {
Button(onClick = onRetry) {
Text("Retry")
}
}
},
dismissButton = {
Button(onClick = { showSettingDialog = !showSettingDialog }) {
Text(text = if (showSettingDialog) "Network" else "Settings")
}
},
modifier = Modifier.padding(16.dp)
)
}

View File

@@ -1,37 +0,0 @@
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
import android.widget.Toast
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.shouldShowRationale
@Composable
fun DefaultScreen(
qrCodeValue: String?,
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
qrCodeValue?.let {
Text(text = "Scanned Value: $it", modifier = Modifier.padding(16.dp))
}
}
}

View File

@@ -10,7 +10,6 @@ import androidx.compose.material3.OutlinedCard
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
@@ -22,7 +21,6 @@ import com.google.mlkit.vision.common.InputImage
@Composable
fun QRCodeScannerScreen(
onQRCodeScanned: (String) -> Unit,
lifecycleOwner: LifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
) {
@@ -61,7 +59,7 @@ fun QRCodeScannerScreen(
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
CameraSelector.DEFAULT_FRONT_CAMERA,
preview,
imageAnalyzer
)
@@ -72,8 +70,9 @@ fun QRCodeScannerScreen(
AndroidView(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
.aspectRatio(1f),
,
factory = { previewView }
)
}

View File

@@ -1,27 +0,0 @@
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.isGranted
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun RequestPermissions(
cameraPermissionState: PermissionState,
locationPermissionState: PermissionState
) {
LaunchedEffect(cameraPermissionState.status.isGranted, locationPermissionState.status.isGranted) {
when {
!cameraPermissionState.status.isGranted -> {
cameraPermissionState.launchPermissionRequest()
}
!locationPermissionState.status.isGranted -> {
locationPermissionState.launchPermissionRequest()
}
}
}
}

View File

@@ -0,0 +1,35 @@
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
internal fun ScannerContent(
onQrCodeScanned: (String) -> Unit, onClose: () -> Unit
) {
Box(modifier = Modifier.fillMaxSize()) {
QRCodeScannerScreen(onQrCodeScanned)
IconButton(
onClick = onClose, modifier = Modifier
.align(Alignment.TopStart)
.padding(16.dp)
) {
Icon(Icons.Default.ArrowBack, contentDescription = "Close Scanner")
}
}
BackHandler { onClose() }
}

View File

@@ -0,0 +1,54 @@
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun ScannerToggleButton(
isScanning: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier
) {
OutlinedCard(
onClick = onClick, colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primary
), modifier = modifier
.fillMaxWidth()
.padding(16.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = if (isScanning) Icons.Default.Close else Icons.Default.KeyboardArrowUp,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp)
)
Text(
text = if (isScanning) "Close Scanner" else "Open Scanner",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
}
}

View File

@@ -0,0 +1,126 @@
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import `in`.sminnovations.attendanceapp.utils.UpdateInfo
@Composable
fun UpdateDialog(
updateInfo: UpdateInfo,
downloadProgress: Float,
onDismiss: () -> Unit,
onInstall: (String) -> Unit,
modifier: Modifier = Modifier
) {
val isinstallPressed = remember { mutableStateOf(false) } //not a smart approach but it works
AlertDialog(
onDismissRequest = {
// Only allow dismissing if not downloading
if (downloadProgress == 0f) onDismiss()
},
modifier = modifier,
title = {
Text(
text = "Update Available",
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
},
text = {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "A new version (${updateInfo.version}) is available. Please update.",
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(vertical = 8.dp)
)
Text(
text = "Release Note:",
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Start,
modifier = Modifier.padding(vertical = 8.dp),
fontWeight = FontWeight.Bold
)
Text(
text = updateInfo.releaseNote,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Start,
modifier = Modifier.padding(vertical = 8.dp),
fontWeight = FontWeight.SemiBold,
fontFamily = FontFamily.Monospace
)
if (downloadProgress > 0f) {
Spacer(modifier = Modifier.height(8.dp))
Row (verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center){
LinearProgressIndicator(progress = downloadProgress , modifier = Modifier.fillMaxWidth(0.8f))
Text(text = " ${floatToProgress(downloadProgress)}%")
}
}
}
},
confirmButton = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
Button(
onClick = {
onInstall(updateInfo.url)
isinstallPressed.value = true },
enabled = downloadProgress == 0f || downloadProgress == 1f
) {
AnimatedVisibility(visible = isinstallPressed.value && downloadProgress ==0f) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.surface)
}
Text(
text = if (downloadProgress > 0f && downloadProgress <1f) "Downloading..." else if (downloadProgress == 1f && isinstallPressed.value) "ReInstall Again" else "Install",
modifier = Modifier.padding(start = if(isinstallPressed.value && downloadProgress ==0f) 10.dp else 0.dp )
)
}
}
},
dismissButton = {
if (downloadProgress == 0f) {
Button(onClick = onDismiss) {
Text("Later")
}
}
}
)
}
fun floatToProgress(float: Float): Int{
return (float * 100f).toInt()
}

View File

@@ -0,0 +1,42 @@
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
@OptIn(ExperimentalPermissionsApi::class)
@Composable
internal fun RequestPermissionsContent(cameraPermissionState: PermissionState) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "Camera Permission Required",
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Request Permission")
}
}
}

View File

@@ -1,217 +1,91 @@
package `in`.sminnovations.attendanceapp.screens.home.ui
import android.Manifest
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import android.content.Context
import android.widget.Toast
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.platform.LocalContext
import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.DefaultScreen
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.LargeButton
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.QRCodeScannerScreen
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.RequestPermissions
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.AttendanceScreenContent
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.NetworkErrorDialog
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.UpdateDialog
import `in`.sminnovations.attendanceapp.screens.home.viewModel.HomeViewModel
import `in`.sminnovations.attendanceapp.screens.settings.ui.SettingsScreenDialogue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class)
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun HomeScreen(viewModel: HomeViewModel = hiltViewModel()) {
val isScanning by viewModel.isScanning.collectAsState()
val qrCodeValue by viewModel.qrCodeValue.collectAsState()
val checkInTime by viewModel.checkedIn.collectAsState()
val checkOutTime by viewModel.checkedOut.collectAsState()
val isCheckedIn by viewModel.isCheckedIn.collectAsState()
val isCheckedOut by viewModel.isCheckedOut.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
val errorMessage by viewModel.errorMessage.collectAsState()
fun HomeScreen(
viewModel: HomeViewModel = hiltViewModel(),
) {
val uiState by viewModel.uiState.collectAsState()
val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
val locationPermissionState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
val allPermissionsGranted = remember {
derivedStateOf {
cameraPermissionState.status.isGranted && locationPermissionState.status.isGranted
}
}
val updateInfo by viewModel.updateInfo.collectAsState()
val isUpdateDialogVisible by viewModel.isUpdateDialogVisible.collectAsState()
val showSettingDialog = remember { mutableStateOf(false) }
val downloadProgress by viewModel.downloadProgress.collectAsState()
val context = LocalContext.current
Box(modifier = Modifier.fillMaxSize()) {
if (!allPermissionsGranted.value) {
RequestPermissions(
fun checkForUpdateToast(context: Context ,value : Boolean){
val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
delay(1000)
}
if (value) {
Toast.makeText(context, "App is Upto Date", Toast.LENGTH_SHORT).show()
}
}
SettingsScreenDialogue(showDialogue = showSettingDialog.value,
onDismissDialogue = { showSettingDialog.value = false },
onCheckForUpdate = {
viewModel.checkForUpdates(context)
checkForUpdateToast(context,!isUpdateDialogVisible)
})
if (uiState.isConnected) {
viewModel.checkForUpdates(context)
if (isUpdateDialogVisible && updateInfo != null) {
UpdateDialog(updateInfo = updateInfo!!,
downloadProgress = downloadProgress,
onDismiss = { viewModel.dismissUpdate() },
onInstall = { url -> viewModel.installUpdate(url) })
}
AttendanceScreenContent(uiState = uiState,
cameraPermissionState = cameraPermissionState,
locationPermissionState = locationPermissionState
)
onScannerToggle = { if (uiState.isScanning) viewModel.stopScanning() else viewModel.startScanning() },
onQrCodeScanned = viewModel::setQRCodeValue,
onClickSettings = { showSettingDialog.value = true })
} else {
DefaultScreen(qrCodeValue = qrCodeValue)
NetworkErrorDialog(showDialog = true, onRetry = {
viewModel.reStartMonitoring()
}, onCheckForUpdate = {
viewModel.checkForUpdates(context)
checkForUpdateToast(context,!isUpdateDialogVisible)
AnimatedVisibility(
visible = isScanning,
enter = expandVertically(expandFrom = Alignment.Bottom) + fadeIn(),
exit = shrinkVertically(shrinkTowards = Alignment.Bottom) + fadeOut()
) {
Box(modifier = Modifier.fillMaxSize()) {
QRCodeScannerScreen(viewModel::setQRCodeValue)
IconButton(
onClick = { viewModel.stopScanning() },
modifier = Modifier
.align(Alignment.TopStart)
.padding(16.dp)
) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
}
}
}
// Loading Indicator
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
// Display Error Message
errorMessage?.let { message ->
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
.padding(16.dp)
) {
Text(
text = message,
color = MaterialTheme.colorScheme.error,
fontWeight = FontWeight.Bold,
modifier = Modifier
.background(Color(0xFFFFCDD2)) // Light red background for error
.padding(8.dp)
.align(Alignment.Center)
)
}
}
OutlinedCard(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(.47f)
.align(Alignment.BottomCenter)
.padding(10.dp),
onClick = { }
) {
Box(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.padding(20.dp)
.align(Alignment.TopCenter)
.fillMaxWidth()
) {
errorMessage?.let { message ->
Text(
text = message,
color = MaterialTheme.colorScheme.error,
fontWeight = FontWeight.Bold,
modifier = Modifier
.background(Color(0xFFFFCDD2)) // Light red background for error
.padding(8.dp)
)
}
AnimatedVisibility(visible = isCheckedIn && !isCheckedOut) {
Text(
text = "You already checked in. Scanning again will Check out.",
letterSpacing = 2.sp,
lineHeight = 40.sp,
fontSize = 30.sp,
color = MaterialTheme.colorScheme.error,
fontWeight = FontWeight.Bold
)
}
AnimatedVisibility(visible = !isCheckedIn && !isCheckedOut) {
Text(
text = "Please Check In",
letterSpacing = 2.sp,
lineHeight = 40.sp,
fontSize = 30.sp,
color = Color.Green,
fontWeight = FontWeight.Bold
)
}
Column(Modifier.padding(15.dp)) {
AnimatedVisibility(visible = isCheckedIn || isCheckedOut) {
Text(text = "Check-in: $checkInTime")
Spacer(modifier = Modifier.height(10.dp))
}
AnimatedVisibility(visible = (isCheckedIn && isCheckedOut) || isCheckedOut ) {
Text(text = "Check-out: $checkOutTime")
Spacer(modifier = Modifier.height(10.dp))
}
}
}
Column(Modifier.align(Alignment.BottomCenter)) {
LargeButton(
text = if (isScanning) "Close" else "Scan",
onClick = if (isScanning) {
{ viewModel.stopScanning() }
} else {
{ viewModel.startScanning() }
},
modifier = Modifier.padding(bottom = 24.dp)
)
if (isScanning) {
BackHandler { viewModel.stopScanning() }
}
AnimatedVisibility(visible = !isCheckedIn) {
LargeButton(
text = "Check In",
onClick = { viewModel.loginNow() },
modifier = Modifier.padding(bottom = 24.dp)
)
}
AnimatedVisibility(visible = isCheckedIn ) {
LargeButton(
text = "Check Out",
onClick = { viewModel.loginNow() },
modifier = Modifier.padding(bottom = 24.dp)
)
}
if (isScanning) {
BackHandler { viewModel.stopScanning() }
}
}
}
})
}
}
}

View File

@@ -1,256 +1,412 @@
package `in`.sminnovations.attendanceapp.screens.home.viewModel
import android.util.Log
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope
import com.google.firebase.Timestamp
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.FirebaseFirestore
import dagger.hilt.android.lifecycle.HiltViewModel
import `in`.sminnovations.attendanceapp.screens.home.data.CheckInCheckOutDataStore
import `in`.sminnovations.attendanceapp.screens.login.data.LoginDataStore
import `in`.sminnovations.attendanceapp.screens.home.data.AttendanceUiState
import `in`.sminnovations.attendanceapp.utils.AppUpdater
import `in`.sminnovations.attendanceapp.utils.NetworkMonitor
import `in`.sminnovations.attendanceapp.utils.UpdateChecker
import `in`.sminnovations.attendanceapp.utils.UpdateInfo
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import java.text.SimpleDateFormat
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import java.util.Date
import java.util.Locale
import javax.inject.Inject
import kotlin.math.abs
const val timeFormat = "dd MMMM yyyy hh:mm:ss a"
@HiltViewModel
class HomeViewModel @Inject constructor(
private val loginDataStore: LoginDataStore,
private val checkInCheckOutDataStore: CheckInCheckOutDataStore,
applicationContext: Context,
private val firebaseFirestore: FirebaseFirestore,
private val networkMonitor: NetworkMonitor
) : ViewModel() {
private val _qrCodeValue = MutableStateFlow<String?>(null)
val qrCodeValue: StateFlow<String?> = _qrCodeValue.asStateFlow()
private val _uiState = MutableStateFlow(AttendanceUiState())
val uiState: StateFlow<AttendanceUiState> = _uiState.asStateFlow()
private val _isScanning = MutableStateFlow(false)
val isScanning: StateFlow<Boolean> = _isScanning.asStateFlow()
private val _updateInfo = MutableStateFlow<UpdateInfo?>(null)
val updateInfo = _updateInfo.asStateFlow()
private val _checkedIn = MutableStateFlow("")
val checkedIn: StateFlow<String> = _checkedIn.asStateFlow()
private val _isUpdateDialogVisible = MutableStateFlow(false)
val isUpdateDialogVisible = _isUpdateDialogVisible.asStateFlow()
private val _checkedOut = MutableStateFlow("N/A")
val checkedOut: StateFlow<String> = _checkedOut.asStateFlow()
private val _isCheckedIn = MutableStateFlow(false)
val isCheckedIn: StateFlow<Boolean> = _isCheckedIn.asStateFlow()
private val _downloadProgress = MutableStateFlow(0f)
val downloadProgress: StateFlow<Float> = _downloadProgress
private val _isCheckedOut = MutableStateFlow(false)
val isCheckedOut: StateFlow<Boolean> = _isCheckedOut.asStateFlow()
private val _errorMessage = MutableStateFlow<String?>(null)
val errorMessage: StateFlow<String?> = _errorMessage.asStateFlow()
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
private fun postErrorMessage(message: String) {
private val appUpdater = AppUpdater(applicationContext)
fun checkForUpdates(context: Context) {
viewModelScope.launch {
_errorMessage.emit(message)
UpdateChecker.checkForUpdate(context).onSuccess { updateInfo ->
if (updateInfo.isNew) {
_updateInfo.value = updateInfo
_isUpdateDialogVisible.value = true
}
}.onFailure { _ ->
// Handle error
}
}
}
fun startScanning() {
_isScanning.value = true
fun installUpdate(url: String) {
// Start the download
appUpdater.downloadAndInstall(url)
viewModelScope.launch {
appUpdater.updateState.collect { state ->
when (state) {
is AppUpdater.UpdaterState.Downloading -> {
_downloadProgress.value = state.progress
}
is AppUpdater.UpdaterState.Success -> {
_downloadProgress.value = 1f
}
is AppUpdater.UpdaterState.Error -> {
_downloadProgress.value = 0f
}
AppUpdater.UpdaterState.Idle -> {
// No op
}
}
}
}
}
fun dismissUpdate() {
_isUpdateDialogVisible.value = false
_updateInfo.value = null
_downloadProgress.value = 0f
}
init {
networkMonitor.startMonitoring()
viewModelScope.launch {
networkMonitor.isConnected.asFlow().collectLatest { isConnected ->
_uiState.update { it.copy(isConnected = isConnected) }
if (isConnected) {
startScanning()
}
}
}
}
fun reStartMonitoring() {
networkMonitor.startMonitoring()
}
fun startScanning() {
viewModelScope.launch {
_uiState.update { _ ->
AttendanceUiState( // Reset to completely fresh state
isScanning = true
)
}
}
}
fun stopScanning() {
_isScanning.value = false
viewModelScope.launch {
_uiState.update { currentState ->
currentState.copy(
isScanning = false, errorMessage = null, qrCodeValue = null
)
}
}
}
fun setQRCodeValue(value: String) {
viewModelScope.launch {
_qrCodeValue.emit(value)
_uiState.update {
it.copy(
qrCodeValue = value,
userName = null, // Reset userName when new QR code is scanned
errorMessage = null
)
}
stopScanning()
processQRCode(value)
}
}
private fun processQRCode(userId: String) {
if (userId.isBlank()) return
viewModelScope.launch {
try {
setLoading(true)
_uiState.update {
it.copy(errorMessage = null, userName = null) // Reset UI state
}
// Fetch Employee Document
val employeeDoc = getEmployeeDocument(userId)
delay(200L)
if (employeeDoc == null) {
handleError("Invalid employee ID")
return@launch
}
if (employeeDoc.getBoolean("EmployeeStatus") != true) {
_uiState.update {
it.copy(
userName = employeeDoc.getString("Name"),
errorMessage = "Employee ID has expired"
)
}
return@launch
}
_uiState.update { it.copy(userName = employeeDoc.getString("Name")) }
// Fetch Attendance Document
val currentDate = getCurrentFormattedDate()
val attendanceDoc = getAttendanceDocument(userId, currentDate)
if (attendanceDoc?.exists() == true) {
handleExistingAttendance(attendanceDoc)
_uiState.update { it.copy(isCheckedOut = true, qrCodeValue = null) }
} else {
handleNewAttendance(userId, currentDate)
}
} catch (e: Exception) {
handleError("An error occurred: ${e.message}")
} finally {
setLoading(false)
}
}
}
private fun handleError(message: String) {
viewModelScope.launch {
_uiState.update {
it.copy(
errorMessage = message,
isLoading = false,
userName = null // Reset userName on error
)
}
}
}
private suspend fun getEmployeeDocument(userId: String): DocumentSnapshot? {
return try {
val querySnapshot =
firebaseFirestore.collection("employees").whereEqualTo("Id", userId).get().await()
if (querySnapshot.isEmpty) null else querySnapshot.documents.first()
} catch (e: Exception) {
throw Exception("Failed to fetch employee data")
}
}
private suspend fun getAttendanceDocument(userId: String, date: String): DocumentSnapshot? {
return try {
firebaseFirestore.collection("attendance_$userId").document(date).get().await()
} catch (e: Exception) {
throw Exception("Failed to fetch attendance data")
}
}
private fun setLoading(loading: Boolean) {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = loading) }
}
}
private fun getCurrentFormattedDate(): String {
return SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).format(Date())
}
private fun formatTimestamp(timestamp: Timestamp): String {
val sdf =
SimpleDateFormat("dd MMMM yyyy hh:mm:ss a", Locale.getDefault()) // Format with AM/PM
return sdf.format(timestamp.toDate())
return SimpleDateFormat(
timeFormat, Locale.getDefault()
).format(timestamp.toDate())
}
init {
viewModelScope.launch {
getCheckInChekout()
// fun isAppDisabled(): Boolean {
// val a = firebaseFirestore.collection("deviceState").document("state")
// println(a)
// return true
// }
private suspend fun handleExistingAttendance(document: DocumentSnapshot) {
val data = document.data ?: return
val checkIns = data.filterKeys { it.startsWith("checkIn") }.size
val checkOuts = data.filterKeys { it.startsWith("checkOut") }.size
delay(200L)
val currentTimestamp = formatTimestamp(Timestamp.now())
val checkIn = document.getString("checkIn$checkIns") ?: run {
handleError("Missing check-in data")
return
}
}
private fun getCheckInChekout() {
viewModelScope.launch {
checkInCheckOutDataStore.checkInId.collect { checkIn ->
_checkedIn.value = checkIn
}
}
viewModelScope.launch {
checkInCheckOutDataStore.checkOutId.collect { checkOut ->
_checkedOut.value = checkOut
}
}
viewModelScope.launch {
checkInCheckOutDataStore.checkInDate.collect {date ->
if (date == LocalDate.now().toString()) {
_isCheckedIn.value = true
if (isTimeDifferenceGreaterThanSec(givenTimeStr = checkIn, currentTimeStr = currentTimestamp, secVal = 5L)) {
if (!isTimeDifferenceGreaterThanMinutes(givenTimeStr = checkIn, currentTimeStr = currentTimestamp, minutesVal = 5L)) {
handleError("Wait 5 minutes before logging off")
return
} else {
if (checkIns > checkOuts) {
processCheckOut(document, checkIns)
} else {
processCheckIn(document, checkIns + 1)
}
}
}else{
return
}
}
private fun getFormattedDate(): String {
val dateFormat = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
return dateFormat.format(Date())
private fun isTimeDifferenceGreaterThanMinutes(
givenTimeStr: String,
currentTimeStr: String = formatTimestamp(Timestamp.now()),
minutesVal: Long
): Boolean {
val formatter = DateTimeFormatter.ofPattern(timeFormat)
val givenTime = LocalDateTime.parse(givenTimeStr, formatter)
val currentTime = LocalDateTime.parse(currentTimeStr, formatter)
val timeDifference = abs(ChronoUnit.MINUTES.between(givenTime, currentTime))
return timeDifference > minutesVal
}
fun loginNow() {
viewModelScope.launch {
_isLoading.value = true
checkInCheckOutDataStore.checkInDate.collect { checkOutDate ->
loginDataStore.loginId.collect { _ ->
firebaseFirestore.collection("qr_key").whereEqualTo("id", getFormattedDate())
.get().addOnSuccessListener { querySnapshot ->
_isLoading.value = false
if (!querySnapshot.isEmpty) {
for (document in querySnapshot) {
if (qrCodeValue.value == document.getString("key")) {
viewModelScope.launch {
if (checkOutDate != LocalDate.now().toString() || checkOutDate == ""
) {
putCheckInAttendance("")
} else {
viewModelScope.launch {
_isCheckedIn.value = true
}
putCheckOutAttendance()
}
}
} else {
postErrorMessage("Invalid QR code scanned")
}
}
} else {
postErrorMessage("No matching QR key found for today's date")
}
}.addOnFailureListener { exception ->
_isLoading.value = false
postErrorMessage("Error fetching QR key: ${exception.message}")
}
}
private fun isTimeDifferenceGreaterThanSec(
givenTimeStr: String,
currentTimeStr: String = formatTimestamp(Timestamp.now()),
secVal: Long
): Boolean {
val formatter = DateTimeFormatter.ofPattern(timeFormat)
val givenTime = LocalDateTime.parse(givenTimeStr, formatter)
val currentTime = LocalDateTime.parse(currentTimeStr, formatter)
val timeDifference = abs(ChronoUnit.SECONDS.between(givenTime, currentTime))
return timeDifference > secVal
}
private suspend fun handleNewAttendance(userId: String, currentDate: String) {
try {
val checkInData = hashMapOf(
"DayCompleted" to false,
"checkIn1" to formatTimestamp(Timestamp.now()),
"totalWork" to "00:00:00"
)
firebaseFirestore.collection("attendance_$userId").document(currentDate)
.set(checkInData).await()
_uiState.update {
it.copy(
isCheckedIn = true, qrCodeValue = null
)
}
} catch (e: Exception) {
handleError("Failed to check in: ${e.message}")
}
}
private fun putCheckInAttendance(location: String) {
viewModelScope.launch {
_isLoading.value = true
loginDataStore.loginId.collect { id ->
val currentDate = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).format(Date())
val attendanceRef =
firebaseFirestore.collection("attendance_$id").document(currentDate)
attendanceRef.get().addOnSuccessListener { document ->
_isLoading.value = false
if (!document.exists()) {
val checkInData = hashMapOf(
"Status" to false,
"checkIn" to formatTimestamp(Timestamp.now()),
"location" to location,
"checkOut" to "NA"
)
attendanceRef.set(checkInData).addOnSuccessListener {
viewModelScope.launch {
checkInCheckOutDataStore.saveLoginDate(
checkDate = LocalDate.now().toString(),
checkInTime = formatTimestamp(Timestamp.now())
)
}
Log.d("Firestore", "Check-in successful")
viewModelScope.launch {
_isCheckedIn.value = true
}
}.addOnFailureListener { e ->
postErrorMessage("Error adding check-in: ${e.message}")
}
} else {
postErrorMessage("Check-in already exists for today. Cannot check in again.")
viewModelScope.launch {
_isCheckedIn.value = true
}
}
}.addOnFailureListener { exception ->
_isLoading.value = false
postErrorMessage("Error fetching document: ${exception.message}")
}
private suspend fun processCheckIn(document: DocumentSnapshot, checkInCount: Int) {
try {
document.reference.update("checkIn$checkInCount", formatTimestamp(Timestamp.now())).await()
_uiState.update {
it.copy(
isCheckedIn = true, qrCodeValue = null
)
}
} catch (e: Exception) {
handleError("Failed to check in: ${e.message}")
}
}
private fun putCheckOutAttendance() {
viewModelScope.launch {
_isLoading.value = true
loginDataStore.loginId.collect { id ->
val currentDate = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).format(Date())
val attendanceRef =
firebaseFirestore.collection("attendance_$id").document(currentDate)
attendanceRef.get().addOnSuccessListener { document ->
_isLoading.value = false
if (document.exists()) {
val checkIn = document.getString("checkIn")
val checkOut = document.getString("checkOut")
if (checkIn != null && checkOut == "NA") {
val checkOutData = hashMapOf(
"checkOut" to formatTimestamp(Timestamp.now()), "Status" to true
) as Map<String, Any>
attendanceRef.update(checkOutData).addOnSuccessListener {
viewModelScope.launch {
checkInCheckOutDataStore.saveLoginData(
checkOut = formatTimestamp(
Timestamp.now()
)
)
checkInCheckOutDataStore.saveIsLogIn(false)
}
Log.d("Firestore", "Check-out successful")
}.addOnFailureListener { e ->
postErrorMessage("Error adding check-out: ${e.message}")
}
} else if (checkOut != "NA") {
postErrorMessage("Already checked out for today.")
viewModelScope.launch {
_isCheckedOut.value = true
}
} else {
postErrorMessage("Cannot check out without checking in.")
}
} else {
postErrorMessage("No attendance found for today. Please check in first.")
}
}.addOnFailureListener { exception ->
_isLoading.value = false
postErrorMessage("Error fetching document: ${exception.message}")
}
private suspend fun processCheckOut(document: DocumentSnapshot, checkInCount: Int) {
try {
val checkOutTime = formatTimestamp(Timestamp.now())
document.reference.update("checkOut$checkInCount", checkOutTime).await()
val updatedDocument = document.reference.get().await()
val totalWork = calculateTotalWork(updatedDocument)
document.reference.update("totalWork", totalWork.first).await()
document.reference.update("DayCompleted", totalWork.second).await()
_uiState.update {
it.copy(
isCheckedOut = true, qrCodeValue = null
)
}
} catch (e: Exception) {
handleError("Failed to check out: ${e.message}")
}
}
private fun calculateTotalWork(document: DocumentSnapshot): Pair<String, Boolean> {
val formatter = DateTimeFormatter.ofPattern(timeFormat, Locale.getDefault())
var totalMinutes = 0L
for (i in 1..20) {
val checkIn = document.getString("checkIn$i") ?: break
val checkOut = document.getString("checkOut$i") ?: continue
val checkInTime = LocalDateTime.parse(checkIn, formatter)
val checkOutTime = LocalDateTime.parse(checkOut, formatter)
totalMinutes += ChronoUnit.MINUTES.between(checkInTime, checkOutTime)
}
val hours = totalMinutes / 60
val minutes = totalMinutes % 60
val isDayCompleted = totalMinutes >= 420L
return Pair(String.format("%02d:%02d:00", hours, minutes), isDayCompleted)
}
override fun onCleared() {
super.onCleared()
networkMonitor.stopMonitoring()
}
}

View File

@@ -1,54 +0,0 @@
package `in`.sminnovations.attendanceapp.screens.login.data
// File: LoginDataStore.kt
import android.content.Context
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
private const val DATASTORE_NAME = "user_prefs"
private val Context.dataStore by preferencesDataStore(name = DATASTORE_NAME)
@Singleton
class LoginDataStore @Inject constructor(@ApplicationContext context: Context) {
private val dataStore = context.dataStore
private val LOGIN_ID_KEY = stringPreferencesKey("login_id")
private val PASSWORD_KEY = stringPreferencesKey("password")
private val IS_LOGGED_IN_KEY = booleanPreferencesKey("is_logged_in")
val loginId: Flow<String> = dataStore.data.map { preferences ->
preferences[LOGIN_ID_KEY] ?: ""
}
val password: Flow<String> = dataStore.data.map { preferences ->
preferences[PASSWORD_KEY] ?: ""
}
val isLoggedIn: Flow<Boolean> = dataStore.data.map { preferences ->
preferences[IS_LOGGED_IN_KEY] ?: false
}
suspend fun saveLoginData(id: String, password: String) {
dataStore.edit { preferences ->
preferences[LOGIN_ID_KEY] = id
preferences[PASSWORD_KEY] = password
preferences[IS_LOGGED_IN_KEY] = true
}
}
suspend fun clearLoginData() {
dataStore.edit { preferences ->
preferences[LOGIN_ID_KEY] = ""
preferences[PASSWORD_KEY] = ""
preferences[IS_LOGGED_IN_KEY] = false
}
}
}

View File

@@ -1,6 +0,0 @@
package `in`.sminnovations.attendanceapp.screens.login.data
data class LoginData(
val id : String ="",
val password: String = ""
)

View File

@@ -1,55 +0,0 @@
package `in`.sminnovations.attendanceapp.screens.login.ui.componets
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun OutlinedTextFieldWithError(
value: String,
onValueChange: (String) -> Unit,
label: String,
isError: Boolean,
errorMessage: String,
modifier: Modifier = Modifier,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
visualTransformation: VisualTransformation = VisualTransformation.None
) {
Column(modifier = modifier) {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
label = { Text(label) },
isError = isError,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
visualTransformation = visualTransformation,
modifier = Modifier.fillMaxWidth(),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
unfocusedBorderColor = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
focusedLabelColor = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
unfocusedLabelColor = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant
)
)
if (isError) {
Text(
text = errorMessage,
color = MaterialTheme.colorScheme.error,
style = TextStyle(fontSize = 12.sp),
modifier = Modifier.padding(start = 16.dp, top = 4.dp)
)
}
}
}

View File

@@ -1,155 +0,0 @@
package `in`.sminnovations.attendanceapp.screens.login.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import `in`.sminnovations.attendanceapp.R
import `in`.sminnovations.attendanceapp.screens.login.ui.componets.OutlinedTextFieldWithError
import `in`.sminnovations.attendanceapp.screens.login.viewModel.LoginViewModel
import kotlinx.coroutines.launch
@Composable
fun LoginScreen(
loginViewModel: LoginViewModel = hiltViewModel(),
onLoginSuccess: () -> Unit
) {
val coroutineScope = rememberCoroutineScope()
val isPasswordError by loginViewModel.isPasswordError.collectAsState()
val isUserIdError by loginViewModel.isIdError.collectAsState()
val isLoggedIn by loginViewModel.isLoggedIn.collectAsState()
var userId by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var passwordVisible by remember { mutableStateOf(false) }
LaunchedEffect(isLoggedIn) {
if (isLoggedIn) onLoginSuccess()
}
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp, vertical = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Attendance App", style = MaterialTheme.typography.headlineLarge)
Spacer(modifier = Modifier.height(40.dp))
Image(
painter = painterResource(id = R.drawable.smi_logo),
contentDescription = "App Logo",
modifier = Modifier
.fillMaxWidth(0.5f)
.aspectRatio(1f)
.padding(8.dp)
)
Spacer(modifier = Modifier.height(40.dp))
Text(text = "Credentials", style = MaterialTheme.typography.headlineMedium)
Spacer(modifier = Modifier.weight(1f))
OutlinedTextFieldWithError(
value = userId,
onValueChange = { userId = it },
label = "User ID",
isError = isUserIdError,
errorMessage = "Please enter a valid User ID",
modifier = Modifier.fillMaxWidth(),
leadingIcon = {
Icon(
imageVector = Icons.Default.Person,
contentDescription = "User Icon"
)
}
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextFieldWithError(
value = password,
onValueChange = { password = it },
label = "Password",
isError = isPasswordError,
errorMessage = "Please enter a valid password",
modifier = Modifier.fillMaxWidth(),
leadingIcon = {
Icon(
imageVector = Icons.Default.Lock,
contentDescription = "Password Icon"
)
},
trailingIcon = {
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(
painter = painterResource(
id = if (passwordVisible) R.drawable.baseline_visibility_24 else R.drawable.baseline_visibility_off_24
),
contentDescription = if (passwordVisible) "Hide Password" else "Show Password"
)
}
},
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation()
)
Spacer(modifier = Modifier.height(24.dp))
Button(
onClick = {
coroutineScope.launch {
loginViewModel.login(userId, password, onLoginSuccess)
}
},
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
) {
Text(
text = "Login",
fontSize = 20.sp
)
}
Spacer(modifier = Modifier.weight(1f))
}
}

View File

@@ -1,126 +0,0 @@
package `in`.sminnovations.attendanceapp.screens.login.viewModel
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.FirebaseFirestore
import dagger.hilt.android.lifecycle.HiltViewModel
import `in`.sminnovations.attendanceapp.data.dataStore.Employee
import `in`.sminnovations.attendanceapp.data.dataStore.EmployeeRepository
import `in`.sminnovations.attendanceapp.screens.login.data.LoginData
import `in`.sminnovations.attendanceapp.screens.login.data.LoginDataStore
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import javax.inject.Inject
@HiltViewModel
class LoginViewModel @Inject constructor(
private val loginDataStore: LoginDataStore,
private val firebaseFirestore: FirebaseFirestore,
private val employeeRepository: EmployeeRepository
) : ViewModel() {
private val _isPasswordError = MutableStateFlow(false)
val isPasswordError = _isPasswordError.asStateFlow()
private val _isIDError = MutableStateFlow(false)
val isIdError = _isIDError.asStateFlow()
private val _isLoggedIn = MutableStateFlow(false)
val isLoggedIn = _isLoggedIn.asStateFlow()
private val _loginData = MutableStateFlow(LoginData("", ""))
val loginData = _loginData.asStateFlow()
init {
viewModelScope.launch {
collectLoginData()
}
}
fun collectLoginData(){
viewModelScope.launch {
loginDataStore.isLoggedIn.collect { loggedIn ->
_isLoggedIn.value = loggedIn
if (loggedIn) {
loginDataStore.loginId.collect { id ->
loginDataStore.password.collect { password ->
_loginData.value = LoginData(id, password)
}
}
}
}
}
}
fun login(userId: String, password: String, onLoginSuccess: () -> Unit) {
viewModelScope.launch {
firebaseFirestore.collection("employees").whereEqualTo("Id", userId).get()
.addOnSuccessListener {
if (!it.isEmpty) {
_isIDError.value = false
for (doc in it) {
if (password == doc.getString("Password")) {
if (true == doc.getBoolean("EmployeeStatus")) {
viewModelScope.launch {
loginDataStore.saveLoginData(userId, password)
_isLoggedIn.value = true
}
viewModelScope.launch {
fetchEmployeeData(userId)
onLoginSuccess()
}
}else{
_isIDError.value = true
}
} else {
_isPasswordError.value = true
}
}
} else {
_isIDError.value = true
}
}.addOnFailureListener {
_isIDError.value = true
}
}
}
private fun DocumentSnapshot.toEmployee(): Employee {
return Employee(
androidID = getString("AndroidID") ?: "",
dateOfJoining = getString("DateOfJoining") ?: "",
dateOfLeaving = getString("DateOfLeaving") ?: "",
designation = getString("Designation") ?: "",
employeeStatus = getBoolean("EmployeeStatus") ?: true,
id = getString("Id") ?: "",
name = getString("Name") ?: "",
password = getString("Password") ?: "",
photo = getString("Photo") ?: "",
dateOfBirth = getString("dateOfBirth") ?: ""
)
}
private suspend fun fetchEmployeeData(employeeId: String) {
val db = firebaseFirestore
val document = db.collection("employees").document(employeeId).get().await()
if (document.exists()) {
val savedata = document.toEmployee()
Log.d("FetchSuccess", "Fetched Employee Data: $savedata")
employeeRepository.saveEmployeeData(savedata)
} else {
Log.d("FetchError", "Document does not exist for employee ID: $employeeId")
}
}
}

View File

@@ -1,2 +0,0 @@
package `in`.sminnovations.attendanceapp.screens.qrcode.ui

View File

@@ -1,7 +0,0 @@
package `in`.sminnovations.attendanceapp.screens.settings.data
class EmployeeDataSettings (
val name : String = "",
val id : String = "",
val designation : String = ""
)

View File

@@ -1,52 +1,41 @@
package `in`.sminnovations.attendanceapp.screens.settings.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import `in`.sminnovations.attendanceapp.screens.settings.data.SettingsData
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.TopComponents
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.subSetting.AboutSettings
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.subSetting.Appearance
@Composable
fun SettingsScreen(
navController: NavController
fun SettingsScreenDialogue(
showDialogue: Boolean,
onDismissDialogue: () -> Unit,
onCheckForUpdate: () -> Unit,
) {
val context = LocalContext.current
val dataStore = SettingsData(context)
val scrollState = rememberScrollState()
val devMode = dataStore.getDevModeToggle.collectAsState(initial = false)
Column(
modifier = Modifier
.verticalScroll(scrollState)
.fillMaxSize(),
) {
Spacer(modifier = Modifier.height(20.dp))
TopComponents(navController = navController)
Spacer(modifier = Modifier.height(20.dp))
OutlinedCard(
modifier = Modifier
.padding(10.dp)
.wrapContentSize()
) {
Appearance(context = context)
AboutSettings(context = context)
}
if (showDialogue) {
val context = LocalContext.current
AlertDialog(
onDismissRequest = { /* Do nothing to prevent dismissal by tapping outside */ },
title = {
Text(text = "Settings")
},
text = {
AboutSettings(context = context,onCheckForUpdate)
},
confirmButton = {},
dismissButton = {
Button(onClick = onDismissDialogue) {
Text("Close")
}
},
modifier = Modifier.padding(16.dp)
)
}
}

View File

@@ -1,45 +1,31 @@
package `in`.sminnovations.attendanceapp.screens.settings.ui.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import `in`.sminnovations.attendanceapp.R
import `in`.sminnovations.attendanceapp.screens.settings.data.EmployeeDataSettings
import `in`.sminnovations.attendanceapp.screens.settings.viewModel.SettingsViewModel
@Composable
fun TopComponents(
navController: NavController,
settingsViewModel: SettingsViewModel = hiltViewModel()
modifier: Modifier = Modifier
) {
val employeeDataSettings by settingsViewModel.employeeDataSettings.collectAsState(
initial = EmployeeDataSettings()
)
Column(
modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Image(
painter = painterResource(id = R.drawable.smi_logo),
@@ -51,38 +37,5 @@ fun TopComponents(
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = employeeDataSettings.name,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "ID: ${employeeDataSettings.id}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 16.sp
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = employeeDataSettings.designation,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary,
fontSize = 16.sp
)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = {
settingsViewModel.logout()
navController.navigate("login")
}) {
Text(text = "Log out")
}
}
}

View File

@@ -66,6 +66,46 @@ fun ClickableCard(
}
@Composable
fun ClickableCardNonBoolean(
title:String,
subtitle:String,
onClick: () -> Unit,
icon: ImageVector
) {
Row(
modifier = Modifier
// .background(MaterialTheme.colorScheme.surfaceContainer)
.clickable{ onClick()}
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier
.weight(1f)
) {
Text(
text = title,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.bodyLarge,
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodyMedium,
)
}
}
}
@OptIn( ExperimentalFoundationApi::class)
@Composable

View File

@@ -1,126 +0,0 @@
package `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import `in`.sminnovations.attendanceapp.R
@Composable
fun RadioDialogue(
showDialog:Boolean,
listOptions:List<String>,
headText:String,
selectedOption:String,
onClose: () -> Unit,
gotOption:(String)->Unit
) {
if (showDialog) {
Dialog(
onDismissRequest = { onClose() },
DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true,
usePlatformDefaultWidth = true
)
) {
OutlinedCard(
modifier = Modifier
.wrapContentSize()
.clickable { onClose() },
) {
BackHandler(onBack = onClose)
Box(
// modifier = Modifier
// .background(MaterialTheme.colorScheme.su)
) {
Column {
Text(
text = headText,
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(20.dp)
)
LazyColumn(
modifier =
if (listOptions.size <=5) {
Modifier
.padding(20.dp, 20.dp, 20.dp, 0.dp)
.wrapContentSize()
}else {
Modifier
.padding(20.dp, 20.dp, 20.dp, 0.dp)
.height(400.dp)
},
)
{
items(listOptions) { option ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
gotOption(option)
onClose()
},
verticalAlignment = Alignment.CenterVertically
) {
RadioButtonComponent(value = selectedOption, onValueChange = {
gotOption(option)
onClose()
}, placeholder = option)
}
Spacer(modifier = Modifier.height(10.dp))
}
}
Row {
Spacer(Modifier.weight(1f))
Text(
text = stringResource(R.string.cancel),
modifier = Modifier
.padding(0.dp, 0.dp, 40.dp, 20.dp)
.clickable { onClose() },
color = MaterialTheme.colorScheme.primary
)
}
}
}
}
}
}
}
@Composable
fun RadioButtonComponent(value: String, onValueChange: (String) -> Unit, placeholder: String) {
RadioButton(
selected = value == placeholder,
onClick = { onValueChange(placeholder) }
)
Text(
text = placeholder,
modifier = Modifier.padding(start = 8.dp)
)
}

View File

@@ -1,40 +0,0 @@
package `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.vector.ImageVector
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents.ClickableCard
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents.RadioDialogue
@Composable
fun RadioCard(
title:String,
subtitle:String,
icon: ImageVector,
radioHeader:String,
gotOption:(String)->Unit,
listOptions:List<String>,
selectedOption:String
) {
var showDialog by remember { mutableStateOf(false) }
ClickableCard(
title = title,
subtitle = subtitle,
onClick = {
showDialog = true
},
icon = icon
)
RadioDialogue(
showDialog = showDialog,
listOptions =listOptions,
headText =radioHeader ,
selectedOption = selectedOption,
onClose = { showDialog=false},
gotOption = gotOption
)
}

View File

@@ -1,55 +1,23 @@
package `in`.sminnovations.attendanceapp.screens.settings.ui.components.subSetting
import android.annotation.SuppressLint
import android.content.Context
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import `in`.sminnovations.attendanceapp.R
import `in`.sminnovations.attendanceapp.screens.settings.data.SettingsData
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents.ClickableCard
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents.ClickableCardNonBoolean
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents.CombinedClickableCard
import kotlinx.coroutines.launch
@SuppressLint("CoroutineCreationDuringComposition")
@Composable
fun AboutSettings(context: Context) {
val scope = rememberCoroutineScope()
val dataStore = SettingsData(context)
var clickCount by remember { mutableIntStateOf(0) }
if (clickCount > 5) {
scope.launch {
dataStore.setDevMode(true)
}
clickCount = 0
Toast.makeText(context, "Developer mode is On : Scroll Down", Toast.LENGTH_SHORT).show()
}
Column(modifier = Modifier.padding(10.dp, 0.dp, 10.dp, 10.dp)) {
Text(
text = "About",
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier
.align(Alignment.Start)
.padding(14.dp),
color = MaterialTheme.colorScheme.primary
)
fun AboutSettings(
context: Context ,
onCheckUpdate : () -> Unit
) {
Column() {
OutlinedCard {
ClickableCard(
title = "Version",
@@ -57,23 +25,27 @@ fun AboutSettings(context: Context) {
context.packageName,
0
).versionName,
onClick = {
clickCount++
},
onClick = {},
icon = ImageVector.vectorResource(id = R.drawable.round_info_24)
)
ClickableCardNonBoolean(
title = "Check For Update",
subtitle = "Check For latest update",
onClick = onCheckUpdate,
icon = ImageVector.vectorResource(id = R.drawable.round_info_24)
)
CombinedClickableCard(title = "Developer",
subtitle = "SM Innovations Pvt Ltd",
subtitle = "ShanMukha Innovations Pvt Ltd",
onClick = {},
onDoubleClick = {
},
icon = ImageVector.vectorResource(id = R.drawable.round_logo_dev_24)
icon = ImageVector.vectorResource(id = R.drawable.baseline_developer_mode_24)
)
}
}
}

View File

@@ -1,90 +0,0 @@
package `in`.sminnovations.attendanceapp.screens.settings.ui.components.subSetting
import android.content.Context
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import `in`.sminnovations.attendanceapp.R
import `in`.sminnovations.attendanceapp.screens.settings.data.SettingsData
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents.RadioCard
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents.SwitchCard
import kotlinx.coroutines.launch
@Composable
fun Appearance (context: Context) {
val scope = rememberCoroutineScope()
val dataStore = SettingsData(context)
val dynamicColorsThemeValue = dataStore.getDynamicColorThemeValue.collectAsState(true)
val blackThemeValue = dataStore.getBlackThemeValue.collectAsState(initial = false)
val theme = dataStore.getThemeMode.collectAsState(initial = "System default")
Column(modifier = Modifier.padding(10.dp, 0.dp, 10.dp, 0.dp)) {
Text(
text = stringResource(R.string.appearance),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier
.align(Alignment.Start)
.padding(14.dp),
color = MaterialTheme.colorScheme.primary
)
OutlinedCard{
RadioCard(
title = stringResource(R.string.theme),
subtitle =theme.value ,
icon = ImageVector.vectorResource(id = R.drawable.round_color_lens_24),
radioHeader = stringResource(R.string.select_theme_mode) ,
gotOption = {
scope.launch {
dataStore.saveThemeMode(it)
}
},
listOptions = listOf("Light", "Dark", "System default"),
selectedOption =theme.value
)
SwitchCard(
checkedValue = dynamicColorsThemeValue.value!!,
title = stringResource(R.string.dynamic_theme),
subtitle = stringResource(R.string.theme_based_on_wallpaper_color),
onSwitchChange =
{
scope.launch {
dataStore.setDynamicColorThemeValue(it)
}
},
icon = ImageVector.vectorResource(id = R.drawable.round_auto_mode_24)
)
SwitchCard(
checkedValue = blackThemeValue.value!!,
title = stringResource(R.string.black_theme),
subtitle = stringResource(R.string.black_theme_description),
onSwitchChange =
{
scope.launch {
dataStore.setBlackThemeValue(it)
}
},
icon = ImageVector.vectorResource(id = R.drawable.round_contrast_24)
)
}
}
}

View File

@@ -1,77 +0,0 @@
package `in`.sminnovations.attendanceapp.screens.settings.ui.components.subSetting
import android.content.Context
import android.os.Build
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import `in`.sminnovations.attendanceapp.R
import `in`.sminnovations.attendanceapp.screens.settings.data.SettingsData
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents.ClickableCard
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents.SwitchCard
import kotlinx.coroutines.launch
@Composable
fun Developer_settings(context: Context) {
val scope = rememberCoroutineScope()
val dataStore = SettingsData(context)
val devMode = dataStore.getDevModeToggle.collectAsState(initial = false)
Column(modifier = Modifier.padding(10.dp,0.dp,10.dp,10.dp)) {
Text(
text = stringResource(R.string.developer_options),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier
.align(Alignment.Start)
.padding(14.dp),
color = MaterialTheme.colorScheme.primary
)
OutlinedCard {
SwitchCard(
checkedValue = devMode.value!!,
title = "Developer mode",
subtitle = "To toggle developer mode",
onSwitchChange = {
scope.launch {
dataStore.setDevMode(it)
}
},
icon = ImageVector.vectorResource(R.drawable.round_developer_mode_24)
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
ClickableCard(
title = "Clear the Database",
subtitle = "you will loose every thing 😨",
onClick = {
},
icon = ImageVector.vectorResource(R.drawable.round_delete_sweep_24)
)
}
ClickableCard(
title = "Change the language",
subtitle = "Current language :English",
onClick = {
Toast.makeText(context, "Not implemented yet", Toast.LENGTH_SHORT).show()
},
icon = ImageVector.vectorResource(R.drawable.round_translate_24)
)
}
}
}

View File

@@ -1,33 +0,0 @@
package `in`.sminnovations.attendanceapp.screens.settings.viewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import `in`.sminnovations.attendanceapp.data.dataStore.EmployeeRepository
import `in`.sminnovations.attendanceapp.screens.home.data.CheckInCheckOutDataStore
import `in`.sminnovations.attendanceapp.screens.login.data.LoginDataStore
import `in`.sminnovations.attendanceapp.screens.settings.data.EmployeeDataSettings
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(
private val loginDataStore: LoginDataStore,
private val employeeRepository: EmployeeRepository,
private val checkInCheckOutDataStore: CheckInCheckOutDataStore,
) : ViewModel() {
val employeeDataSettings : Flow<EmployeeDataSettings> = employeeRepository.getEmployeeDataSettings()
fun logout() {
viewModelScope.launch {
loginDataStore.clearLoginData()
checkInCheckOutDataStore.clearLoginData()
}
}
}

View File

@@ -4,8 +4,9 @@ import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFFF9800)
val PurpleGrey80 = Color(0xFFFFCB7E)
val Pink80 = Color(0xFFEFB8C8)
val Pink80 = Color(0xFF9B6D7B)
val Purple40 = Color(0xFFB86E02)
val Purple40 = Color(0xFF000000)
val PurpleGrey40 = Color(0xFF865307)
val Pink40 = Color(0xFF7D5260)

View File

@@ -0,0 +1,147 @@
package `in`.sminnovations.attendanceapp.utils
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.content.FileProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.ResponseBody
import okio.Buffer
import okio.BufferedSource
import okio.ForwardingSource
import okio.Source
import okio.buffer
import java.io.File
import java.io.IOException
data class UpdateInfo(
val version: String = "",
val url: String = "",
val isNew: Boolean = false,
val releaseNote : String = ""
)
class AppUpdater(private val context: Context) {
sealed class UpdaterState {
data object Idle : UpdaterState()
data class Downloading(val progress: Float) : UpdaterState()
data class Success(val file: File) : UpdaterState()
data class Error(val message: String) : UpdaterState()
}
private val _updateState = MutableStateFlow<UpdaterState>(UpdaterState.Idle)
val updateState = _updateState.asStateFlow()
fun downloadAndInstall(url: String) {
CoroutineScope(Dispatchers.IO).launch {
try {
//Added for future use case
// Resolve the URL (e.g. for Google Drive links)
val resolvedUrl = resolveDownloadUrl(url)
val file = downloadApk(resolvedUrl) { progress ->
// Update progress on Main thread
CoroutineScope(Dispatchers.Main).launch {
_updateState.value = UpdaterState.Downloading(progress)
}
}
_updateState.value = UpdaterState.Success(file)
installApk(file)
} catch (e: Exception) {
_updateState.value = UpdaterState.Error(e.message ?: "Unknown error")
}
}
}
private fun resolveDownloadUrl(url: String): String {
// Optional
// If the URL is from Google Drive, extract file id and convert to direct download URL.
return if (url.contains("drive.google.com")) {
val regex = Regex("(?<=/d/)[^/]+")
val fileId = regex.find(url)?.value ?: Uri.parse(url).getQueryParameter("id")
if (!fileId.isNullOrEmpty())
"https://drive.google.com/uc?export=download&id=$fileId"
else url
} else {
url
}
}
private suspend fun downloadApk(url: String, onProgress: (Float) -> Unit): File =
withContext(Dispatchers.IO) {
val client = OkHttpClient.Builder()
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
val contentLength = originalResponse.body?.contentLength() ?: -1L
originalResponse.newBuilder()
.body(originalResponse.body?.let { body ->
ProgressResponseBody(body) { progress ->
onProgress(progress)
}
})
.build()
}
.build()
val request = Request.Builder().url(url).build()
val file = File(context.cacheDir, "update.apk")
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Failed to download APK")
response.body?.let { body ->
file.outputStream().use { fileOut ->
body.byteStream().use { it.copyTo(fileOut) }
}
} ?: throw IOException("Empty response")
}
file
}
private fun installApk(file: File) {
try {
val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file)
if (file.length() == 0L) throw IOException("Downloaded file is empty")
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "application/vnd.android.package-archive")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
if (intent.resolveActivity(context.packageManager) != null) {
context.startActivity(intent)
} else {
throw IOException("No app found to handle installation")
}
} catch (e: Exception) {
_updateState.value = UpdaterState.Error("Installation failed: ${e.message}")
}
}
}
private class ProgressResponseBody(
private val responseBody: ResponseBody,
private val onProgressUpdate: (Float) -> Unit
) : ResponseBody() {
private val bufferedSource: BufferedSource by lazy {
source(responseBody.source()).buffer()
}
override fun contentType() = responseBody.contentType()
override fun contentLength() = responseBody.contentLength()
override fun source(): BufferedSource = bufferedSource
private fun source(source: Source): Source =
object : ForwardingSource(source) {
var totalBytesRead = 0L
override fun read(sink: Buffer, byteCount: Long): Long {
val bytesRead = super.read(sink, byteCount)
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
val progress = if (contentLength() > 0) totalBytesRead.toFloat() / contentLength() else -1f
onProgressUpdate(progress)
return bytesRead
}
}
}

View File

@@ -0,0 +1,78 @@
package `in`.sminnovations.attendanceapp.utils
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.net.HttpURLConnection
import java.net.URL
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class NetworkMonitor @Inject constructor(
@ApplicationContext private val context: Context
) {
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private val _isConnected = MutableLiveData(false)
val isConnected: LiveData<Boolean> get() = _isConnected
private val networkScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
performPingCheck()
}
override fun onLost(network: Network) {
_isConnected.postValue(false)
}
}
fun startMonitoring() {
val network = connectivityManager.activeNetwork
if (network != null) {
performPingCheck()
}
val networkRequest = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
}
fun stopMonitoring() {
try {
connectivityManager.unregisterNetworkCallback(networkCallback)
} catch (_: IllegalArgumentException) {
}
networkScope.cancel()
}
private fun performPingCheck() {
networkScope.launch {
try {
val url = URL("https://www.google.com")
val connection = url.openConnection() as HttpURLConnection
connection.connectTimeout = 2000
connection.connect()
val isReachable = connection.responseCode == 200
_isConnected.postValue(isReachable)
connection.disconnect()
} catch (e: Exception) {
_isConnected.postValue(false)
}
}
}
}

View File

@@ -0,0 +1,83 @@
package `in`.sminnovations.attendanceapp.utils
import android.content.Context
import com.google.firebase.firestore.ktx.firestore
import com.google.firebase.ktx.Firebase
import kotlinx.coroutines.tasks.await
object UpdateChecker {
private const val COLLECTION_NAME = "app_updates"
private const val DOCUMENT_NAME = "latest_version"
suspend fun checkForUpdate(
context: Context
): Result<UpdateInfo> = runCatching {
val currentVersion = getCurrentVersion(context)
val updateInfo = getLatestVersion()
if (isNewerVersion(currentVersion, updateInfo.version)) {
updateInfo.copy(isNew = true)
} else {
updateInfo.copy(isNew = false)
}
}
private fun getCurrentVersion(context: Context): String {
return try {
context.packageManager.getPackageInfo(context.packageName, 0).versionName
} catch (e: Exception) {
throw UpdateCheckException("Failed to get current app version", e)
}
}
private suspend fun getLatestVersion(): UpdateInfo {
return try {
val document = Firebase.firestore
.collection(COLLECTION_NAME)
.document(DOCUMENT_NAME)
.get()
.await()
if (!document.exists()) {
throw UpdateCheckException("Update document does not exist")
}
UpdateInfo(
version = document.getString("version") ?: throw UpdateCheckException("Version field missing"),
url = document.getString("url") ?: throw UpdateCheckException("URL field missing"),
releaseNote = document.getString("ReleaseNote") ?: throw UpdateCheckException("ReleaseNote field missing")
)
} catch (e: Exception) {
throw UpdateCheckException("Failed to fetch update info", e)
}
}
private fun isNewerVersion(currentVersion: String, latestVersion: String): Boolean {
return try {
val current = currentVersion.split(".").map { it.toIntOrNull() ?: 0 }
val latest = latestVersion.split(".").map { it.toIntOrNull() ?: 0 }
for (i in 0 until maxOf(current.size, latest.size)) {
val currentNum = current.getOrNull(i) ?: 0
val latestNum = latest.getOrNull(i) ?: 0
when {
latestNum > currentNum -> return true
latestNum < currentNum -> return false
}
}
false
} catch (e: Exception) {
false
}
}
}
class UpdateCheckException(
message: String,
cause: Throwable? = null
) : Exception(message, cause){
}

View File

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M7,5h10v2h2L19,3c0,-1.1 -0.9,-1.99 -2,-1.99L7,1c-1.1,0 -2,0.9 -2,2v4h2L7,5zM15.41,16.59L20,12l-4.59,-4.59L14,8.83 17.17,12 14,15.17l1.41,1.42zM10,15.17L6.83,12 10,8.83 8.59,7.41 4,12l4.59,4.59L10,15.17zM17,19L7,19v-2L5,17v4c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2v-4h-2v2z"/>
</vector>

View File

@@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</vector>

View File

@@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
</vector>

View File

@@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,7c2.76,0 5,2.24 5,5 0,0.65 -0.13,1.26 -0.36,1.83l2.92,2.92c1.51,-1.26 2.7,-2.89 3.43,-4.75 -1.73,-4.39 -6,-7.5 -11,-7.5 -1.4,0 -2.74,0.25 -3.98,0.7l2.16,2.16C10.74,7.13 11.35,7 12,7zM2,4.27l2.28,2.28 0.46,0.46C3.08,8.3 1.78,10.02 1,12c1.73,4.39 6,7.5 11,7.5 1.55,0 3.03,-0.3 4.38,-0.84l0.42,0.42L19.73,22 21,20.73 3.27,3 2,4.27zM7.53,9.8l1.55,1.55c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.66 1.34,3 3,3 0.22,0 0.44,-0.03 0.65,-0.08l1.55,1.55c-0.67,0.33 -1.41,0.53 -2.2,0.53 -2.76,0 -5,-2.24 -5,-5 0,-0.79 0.2,-1.53 0.53,-2.2zM11.84,9.02l3.15,3.15 0.02,-0.16c0,-1.66 -1.34,-3 -3,-3l-0.17,0.01z"/>
</vector>

View File

@@ -1,15 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M18.06,2.83c-1.15,-0.77 -2.46,-1.32 -3.86,-1.61C13.58,1.1 13,1.57 13,2.21v0c0,0.46 0.31,0.88 0.76,0.97c1.17,0.23 2.26,0.7 3.21,1.34c0.39,0.26 0.9,0.19 1.23,-0.14l0,0C18.66,3.93 18.59,3.18 18.06,2.83z"/>
<path android:fillColor="@android:color/white" android:pathData="M11,2.21L11,2.21c0,-0.64 -0.58,-1.11 -1.2,-0.99c-1.4,0.29 -2.71,0.84 -3.86,1.61c-0.52,0.35 -0.59,1.1 -0.15,1.54l0,0c0.33,0.33 0.84,0.4 1.23,0.14c0.96,-0.64 2.04,-1.1 3.21,-1.34C10.69,3.09 11,2.67 11,2.21z"/>
<path android:fillColor="@android:color/white" android:pathData="M4.38,5.79L4.38,5.79C3.93,5.34 3.18,5.42 2.84,5.94C2.07,7.09 1.51,8.39 1.23,9.8C1.1,10.42 1.58,11 2.21,11h0c0.46,0 0.88,-0.31 0.97,-0.76c0.23,-1.17 0.7,-2.26 1.34,-3.22C4.77,6.64 4.7,6.12 4.38,5.79z"/>
<path android:fillColor="@android:color/white" android:pathData="M21.79,11L21.79,11c0.63,0 1.11,-0.58 0.98,-1.2c-0.29,-1.4 -0.84,-2.7 -1.61,-3.86c-0.35,-0.52 -1.1,-0.6 -1.54,-0.15l0,0c-0.33,0.33 -0.4,0.84 -0.14,1.23c0.64,0.96 1.1,2.05 1.34,3.22C20.91,10.69 21.33,11 21.79,11z"/>
<path android:fillColor="@android:color/white" android:pathData="M8,12.46l2.44,1.11L11.54,16c0.18,0.39 0.73,0.39 0.91,0l1.11,-2.44L16,12.46c0.39,-0.18 0.39,-0.73 0,-0.91l-2.44,-1.11L12.46,8c-0.18,-0.39 -0.73,-0.39 -0.91,0l-1.11,2.44L8,11.54C7.61,11.72 7.61,12.28 8,12.46z"/>
<path android:fillColor="@android:color/white" android:pathData="M12,21c-3.11,0 -5.85,-1.59 -7.46,-4H6c0.55,0 1,-0.45 1,-1v0c0,-0.55 -0.45,-1 -1,-1H2c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1h0c0.55,0 1,-0.45 1,-1v-1.7c1.99,2.84 5.27,4.7 9,4.7c4.45,0 8.27,-2.64 10,-6.43c0.26,-0.57 -0.08,-1.25 -0.69,-1.39l0,0c-0.45,-0.1 -0.93,0.11 -1.12,0.54C18.77,18.83 15.64,21 12,21z"/>
</vector>

View File

@@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9c0.83,0 1.5,-0.67 1.5,-1.5 0,-0.39 -0.15,-0.74 -0.39,-1.01 -0.23,-0.26 -0.38,-0.61 -0.38,-0.99 0,-0.83 0.67,-1.5 1.5,-1.5L16,16c2.76,0 5,-2.24 5,-5 0,-4.42 -4.03,-8 -9,-8zM6.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S5.67,9 6.5,9 8,9.67 8,10.5 7.33,12 6.5,12zM9.5,8C8.67,8 8,7.33 8,6.5S8.67,5 9.5,5s1.5,0.67 1.5,1.5S10.33,8 9.5,8zM14.5,8c-0.83,0 -1.5,-0.67 -1.5,-1.5S13.67,5 14.5,5s1.5,0.67 1.5,1.5S15.33,8 14.5,8zM17.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S16.67,9 17.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z"/>
</vector>

View File

@@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,22c5.52,0 10,-4.48 10,-10S17.52,2 12,2S2,6.48 2,12S6.48,22 12,22zM13,4.07c3.94,0.49 7,3.85 7,7.93s-3.05,7.44 -7,7.93V4.07z"/>
</vector>

View File

@@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M7,5h10v1c0,0.55 0.45,1 1,1s1,-0.45 1,-1L19,3c0,-1.1 -0.9,-1.99 -2,-1.99L7,1c-1.1,0 -2,0.9 -2,2v3c0,0.55 0.45,1 1,1s1,-0.45 1,-1L7,5zM16.12,15.88l3.17,-3.17c0.39,-0.39 0.39,-1.02 0,-1.41l-3.17,-3.17c-0.39,-0.39 -1.03,-0.39 -1.42,0 -0.39,0.39 -0.39,1.02 0,1.41L17.17,12l-2.47,2.47c-0.39,0.39 -0.39,1.02 0,1.41 0.39,0.39 1.03,0.39 1.42,0zM9.29,14.46L6.83,12l2.46,-2.46c0.39,-0.39 0.39,-1.02 0,-1.41 -0.39,-0.39 -1.03,-0.39 -1.42,0L4.7,11.3c-0.39,0.39 -0.39,1.02 0,1.41l3.17,3.17c0.39,0.39 1.03,0.39 1.42,0 0.4,-0.39 0.39,-1.03 0,-1.42zM17,19L7,19v-1c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v3c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2v-3c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v1z"/>
</vector>

View File

@@ -1,7 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M19,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5C21,3.9 20.1,3 19,3zM7.68,14.98H6V9h1.71c1.28,0 1.71,1.03 1.71,1.71l0,2.56C9.42,13.95 9,14.98 7.68,14.98zM12.38,11.46v1.07h-1.18v1.39h1.93v1.07h-2.25c-0.4,0.01 -0.74,-0.31 -0.75,-0.71V9.75c-0.01,-0.4 0.31,-0.74 0.71,-0.75h2.28l0,1.07h-1.92v1.39H12.38zM16.88,14.23c-0.48,1.11 -1.33,0.89 -1.71,0L13.77,9h1.18l1.07,4.11L17.09,9h1.18L16.88,14.23z"/>
<path android:fillColor="@android:color/white" android:pathData="M7.77,10.12H7.14v3.77h0.63c0.14,0 0.28,-0.05 0.42,-0.16c0.14,-0.1 0.21,-0.26 0.21,-0.47v-2.52c0,-0.21 -0.07,-0.37 -0.21,-0.47C8.05,10.17 7.91,10.12 7.77,10.12z"/>
</vector>

View File

@@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12.65,15.67c0.14,-0.36 0.05,-0.77 -0.23,-1.05l-2.09,-2.06 0.03,-0.03c1.74,-1.94 2.98,-4.17 3.71,-6.53h1.94c0.54,0 0.99,-0.45 0.99,-0.99v-0.02c0,-0.54 -0.45,-0.99 -0.99,-0.99L10,4L10,3c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v1L1.99,4c-0.54,0 -0.99,0.45 -0.99,0.99 0,0.55 0.45,0.99 0.99,0.99h10.18C11.5,7.92 10.44,9.75 9,11.35c-0.81,-0.89 -1.49,-1.86 -2.06,-2.88 -0.16,-0.29 -0.45,-0.47 -0.78,-0.47 -0.69,0 -1.13,0.75 -0.79,1.35 0.63,1.13 1.4,2.21 2.3,3.21L3.3,16.87c-0.4,0.39 -0.4,1.03 0,1.42 0.39,0.39 1.02,0.39 1.42,0L9,14l2.02,2.02c0.51,0.51 1.38,0.32 1.63,-0.35zM17.5,10c-0.6,0 -1.14,0.37 -1.35,0.94l-3.67,9.8c-0.24,0.61 0.22,1.26 0.87,1.26 0.39,0 0.74,-0.24 0.88,-0.61l0.89,-2.39h4.75l0.9,2.39c0.14,0.36 0.49,0.61 0.88,0.61 0.65,0 1.11,-0.65 0.88,-1.26l-3.67,-9.8c-0.22,-0.57 -0.76,-0.94 -1.36,-0.94zM15.88,17l1.62,-4.33L19.12,17h-3.24z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 926 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#EBFFF3</color>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path
name="updates"
path="." />
</paths>

View File

@@ -1,8 +1,14 @@
package `in`.sminnovations.attendanceapp
import com.google.firebase.Timestamp
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.Assert.*
import java.text.SimpleDateFormat
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import java.util.Locale
import kotlin.math.abs
/**
* Example local unit test, which will execute on the development machine (host).
@@ -10,8 +16,48 @@ import org.junit.Assert.*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
/*
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
@Test
fun timeTest(){
val currentTime = "30 January 2025 10:30:55 am"
val givenTime = "30 January 2025 10:31:01 am"
val minutes = 7L
val result = isTimeDifferenceGreaterThanSec(givenTime, currentTime, minutes)
println(result)
assertEquals(false,result)
}
//
// private fun isTimeDifferenceGreaterThanMinutes(givenTimeStr: String, currentTimeStr: String, minutes: Long): Boolean {
// val formatter = DateTimeFormatter.ofPattern("dd MMMM yyyy hh:mm:ss a")
// val givenTime = LocalDateTime.parse(givenTimeStr, formatter)
// val currentTime = LocalDateTime.parse(currentTimeStr, formatter)
// val timeDifference = abs(ChronoUnit.MINUTES.between(givenTime, currentTime))
// return timeDifference > minutes
// }
private fun isTimeDifferenceGreaterThanSec(
givenTimeStr: String,
currentTimeStr: String = formatTimestamp(Timestamp.now()),
sec: Long = 5
): Boolean {
val formatter = DateTimeFormatter.ofPattern("dd MMMM yyyy hh:mm:ss a")
val givenTime = LocalDateTime.parse(givenTimeStr, formatter)
val currentTime = LocalDateTime.parse(currentTimeStr, formatter)
val timeDifference = abs(ChronoUnit.SECONDS.between(givenTime, currentTime))
return timeDifference > sec
}
private fun formatTimestamp(timestamp: Timestamp): String {
return SimpleDateFormat(
"dd MMMM yyyy hh:mm:ss a", Locale.getDefault()
).format(timestamp.toDate())
}
*/
}

View File

@@ -1,5 +1,5 @@
[versions]
agp = "8.4.0"
agp = "8.5.0"
kotlin = "1.9.0"
coreKtx = "1.13.1"
junit = "4.13.2"

View File

@@ -1,6 +1,6 @@
#Fri Oct 04 14:44:43 IST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -16,8 +16,10 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
rootProject.name = "SMI Attendance"
include(":app")