├── App Android - React Native webView ├── AndroidManifest.xml ├── App.js ├── README.md ├── gradle.properties └── styles.xml ├── LICENSE ├── README.md ├── Versi 01 ├── Kode.gs ├── absensi.xlsx └── index.html ├── Versi 02 ├── Absensi.xlsx ├── Kode.gs ├── Pegawai.xlsx └── index.html ├── Versi 03 ├── firebase │ └── index.html └── google apps script │ ├── Data Absensi │ └── 2021-06.xlsx │ ├── Data Pegawai.xlsx │ ├── index.html │ ├── kode.gs │ └── logo.png └── logo.png /App Android - React Native webView/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /App Android - React Native webView/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { WebView } from 'react-native-webview'; 3 | 4 | class App extends Component { 5 | render() { 6 | return ( 7 | 13 | 14 | ); 15 | } 16 | } 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /App Android - React Native webView/README.md: -------------------------------------------------------------------------------- 1 | # Aplikasi android webView - React Native 2 | 3 | Aplikasi lanjutan untuk absensi dengan Google Apps Script. 4 | Membuat aplikasi android webView dengan react native. 5 | - Akses lokasi (geolocation) 6 | - Menghapus banner google 7 | 8 | ## Video tutorial 9 | [![DEMO](http://img.youtube.com/vi/jl2anXE-ixY/0.jpg)](https://youtu.be/jl2anXE-ixY) 10 | 11 | ## Donasi 12 | Dukung saya 13 | - [Traktir kopi](https://sociabuzz.com/fahroniganteng/tribe) 14 | - [atau Es krim](https://trakteer.id/fahroniganteng/tip) 15 | -------------------------------------------------------------------------------- /App Android - React Native webView/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Version of flipper SDK to use with React Native 28 | FLIPPER_VERSION=0.93.0 29 | -------------------------------------------------------------------------------- /App Android - React Native webView/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 fahroniganteng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Absensi-google-script 2 | Web absensi pada google script dan spreadsheets dengan login dan menyimpan lokasi 3 | 4 |
5 | 6 | ## VERSI 03 7 | 8 | #### Fitur Web Absensi Versi 03 9 | 1. Fitur versi 01 dan versi 02 masih digunakan. 10 | 2. Metode absensi : 11 | - Absen masuk tidak boleh lebih dari sekali dalam sehari. 12 | - Absen pulang bisa diulang berkali2 (data absen pulang yang lama akan ditimpa) 13 | Jika salah absen pulang pada waktu berangkat, masih bisa absen pulang lagi di sore hari. 14 | - Bisa absen pulang tanpa absen masuk 15 | Ini untuk jika lupa absen masuk, maka masih bisa absen pulang (dihitung potongan tidak absen) 16 | 3. Perbaikan data rekap absensi : 17 | - Recording absensi dalam file spreadsheet per bulan ⇒ mempermudah untuk rekap bulanan (1 file spreadsheet tiap 1 bulan) 18 | - Penggabungan record absen masuk & pulang ⇒ mengurangi jumlah record 19 | 4. Perhitungan potongan terlambat, pulang lebih awal, tidak absen masuk/pulang. 20 | 5. Setting timezone (di setting pada config/variable code) 21 | 22 | #### Aplikasi yang digunakan 23 | - Aplikasi web menggunakan google script 24 | - Penyimpanan data pada google drive 25 | - Recording pada google spreadsheet 26 | - Sub domain pada google firebase (cek pada video) 27 | - Report menggunakan google data studio (cek pada video) 28 | 29 | #### Video 30 | - Video 01. Demo & fitur aplikasi 31 | https://youtu.be/jJtDMGuq6dQ 32 | - Video 02. Instalasi aplikasi pada google apps script 33 | https://youtu.be/FUkhfHXj8jo 34 | - Video 03. Menghilangkan banner google dan sub domain google firebase 35 | https://youtu.be/neY8G5oCjFM 36 | - Video 04. Membuat report spreadsheet dan google data studio 37 | https://youtu.be/pz6Ld-8P8i4 38 | 39 | #### Testing & demo aplikasi 40 | > user : github 41 | > pass : github 42 | - Link sub domain (tidak ada top banner google) 43 | https://demoabsensi.web.app/ 44 | - Link google apps script 45 | https://script.google.com/macros/s/AKfycbx2vOrvFQ4ZyQEtefQR5I2At105yEeMR6HoxQ0RkcheBp7AgG0B_0xrs26y4z45OSc/exec 46 | 47 | #### Testing & demo reporting (google data studio) 48 | - Link google studio 49 | https://datastudio.google.com/s/ldMer2ZUTiY 50 | 51 | 52 | ### NOTE : 53 | Ditemukan bug pada penggunaan ID Pegawai dengan nol di depan, 54 | `misal : 0054321` 55 | pada saat submit absensi, ID tersebut di convert menjadi angka oleh spreadsheet, sehingga yang terekam pada data absensi adalah : 56 | `54321` 57 | pada aplikasi `0054321 != 54321`, sehingga dianggap user yang berbeda dan masih bisa melakukan absensi di hari yang sama. 58 | Solusi Sementara 59 | Tambahkan huruf pada ID, misal: 60 | `ABC0054321` 61 | 62 | 63 | 64 |
65 |
66 |
67 |
68 |
69 | 70 | 71 | 72 |
73 | 74 | ## VERSI 02 75 | Tambahan fitur dari versi 01: 76 | - Membatasi absensi WFO (Work From Office) / WFH (Work From Home) berdasarkankan jarak. 77 | - Link ke google map untuk melihat jarak dan rute lokasi absen menuju kantor. 78 | - Simpan jarak absensi dengan kantor 79 | - Kegiatan harian 80 | #### Video demo & instalasi 81 | [![DEMO](http://img.youtube.com/vi/Sf83RYbiwo0/0.jpg)](https://youtu.be/Sf83RYbiwo0) 82 | 83 | #### Testing & demo aplikasi 84 | https://script.google.com/macros/s/AKfycbzXCF2kUJl72pl42FGQ81FMzTg1axb_UFpVC7HRzhTtME_LbgMyrgGIkqZaTiuAIFnPfg/exec 85 | - user : github 86 | - pass : github 87 | 88 | > NOTE : 89 | > Jika menggunakan browser chrome di android, jika link diatas tidak bisa dibuka coba logout dari akun google yang di chrome. 90 |
91 |
92 |
93 |
94 |
95 | 96 |
97 | 98 | ## VERSI 01 99 | Fitur : 100 | - Login dengan password 101 | - Simpan waktu, koordinat dan lokasi absensi 102 | #### Video demo & instalasi 103 | [![DEMO](http://img.youtube.com/vi/l8oBqwMrlaE/0.jpg)](https://youtu.be/l8oBqwMrlaE) 104 | 105 | #### Testing & demo aplikasi 106 | https://script.google.com/macros/s/AKfycbzyp4ubIEPShU69QxBH_i-Yek9LLISezFKAS89DOXs0eZWAC4XlE6opgVT_Y3cDPIKS/exec 107 | - user : github 108 | - pass : github 109 | 110 | > NOTE : 111 | > Jika menggunakan browser chrome di android, jika link diatas tidak bisa dibuka coba logout dari akun google yang di chrome. 112 | 113 |
114 | 115 | ## License and credits 116 | Lisensi kode saya adalah MIT, untuk libraries yang lain mengikuti lisensi masing-masing. 117 | - Jquery.js 118 | - Bootstrap 119 | - Google script 120 | - Google spreadsheet 121 | - etc... 122 | 123 |
124 | 125 | ## Donasi 126 | Dukung saya 127 | - [Traktir kopi](https://sociabuzz.com/fahroniganteng/tribe) 128 | - [atau Es krim](https://trakteer.id/fahroniganteng/tip) 129 | 130 | -------------------------------------------------------------------------------- /Versi 01/Kode.gs: -------------------------------------------------------------------------------- 1 | /* 2 | * ABSENSI GOOGLE SCRIPT 3 | * *********************************************************************************** 4 | * Code by : fahroni|ganteng 5 | * contact me : fahroniganteng@gmail.com 6 | * Date : Mar 2021 7 | * License : MIT 8 | * 9 | */ 10 | 11 | //spreadsheet url 12 | var database = "https://docs.google.com/spreadsheets/d/[id-dokumen]/edit#gid=0"; 13 | 14 | function doGet(){ 15 | return HtmlService 16 | .createTemplateFromFile('index') 17 | .evaluate() 18 | .setSandboxMode(HtmlService.SandboxMode.NATIVE) 19 | .addMetaTag('viewport', 'width=device-width, initial-scale=1'); 20 | } 21 | 22 | function submitAbsensi(data){ 23 | // Buka Spreadsheet 24 | let ss = SpreadsheetApp.openByUrl(database); 25 | let ws = ss.getSheetByName('pegawai'); 26 | let list = ws.getRange(2,1,ws.getRange("A2").getDataRegion().getLastRow() - 1, 5).getValues(); 27 | let userId = list.map(function(r){ return r[1].toString(); }); 28 | let pass = list.map(function(r){ return r[3].toString(); }); 29 | 30 | // Verifikasi data yang dikirim 31 | data.idPegawai = data['idPegawai'] !== undefined?data.idPegawai.toString():''; 32 | data.password = data['password'] !== undefined?data.password.toString():''; 33 | data.position = data['position'] !== undefined?data.position:[0,0]; 34 | 35 | // Verifikasi user dan password 36 | let indexData = userId.indexOf(data.idPegawai); 37 | if(indexData > -1 && pass[indexData] == data.password){ 38 | let nama = list.map(function(r){ return r[2]}); 39 | 40 | // Get Alamat dari koordinat 41 | let koordinat = data.position[0] +', '+ data.position[1]; 42 | let response = Maps.newGeocoder().reverseGeocode(data.position[0], data.position[1]); 43 | let lokasi = response.results[0].formatted_address; 44 | 45 | //buka sheet absensi dan simpan data 46 | ws = ss.getSheetByName("absensi"); 47 | ws.appendRow([data.idPegawai, nama[indexData], new Date(),koordinat, lokasi]); 48 | return nama[indexData]; 49 | } 50 | else 51 | return false; 52 | } 53 | -------------------------------------------------------------------------------- /Versi 01/absensi.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fahroniganteng/Absensi-google-script/86f49f64c77f592ad4ccec2abb582fbcb4ad52c1/Versi 01/absensi.xlsx -------------------------------------------------------------------------------- /Versi 01/index.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | 20 |

Fahroni Ganteng co, ltd

21 |
22 |
23 |
24 |

Formulir Absensi

25 |
26 | 27 | 28 |
29 |
30 | 31 | 32 |
33 |
34 | 35 | 39 | 43 | 44 |
45 |
46 |
47 |

© 2021 Fahroni Ganteng co, ltd

48 |
49 |
50 | 51 | 52 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /Versi 02/Absensi.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fahroniganteng/Absensi-google-script/86f49f64c77f592ad4ccec2abb582fbcb4ad52c1/Versi 02/Absensi.xlsx -------------------------------------------------------------------------------- /Versi 02/Kode.gs: -------------------------------------------------------------------------------- 1 | /* 2 | * ABSENSI GOOGLE SCRIPT 3 | * *********************************************************************************** 4 | * Code by : fahroni|ganteng 5 | * contact me : fahroniganteng@gmail.com 6 | * Date : Apr 2021 7 | * License : MIT 8 | * 9 | */ 10 | 11 | var idGambarLogo = '-------------------id file-------------------'; // id file logo di google drive 12 | var idDataPegawai = '------------------id file-------------------'; // id file spreadsheet 13 | var idDataAbsensi = '------------------id file-------------------'; // id file spreadsheet 14 | var namaPerusahaan = 'Fahroni Ganteng co, ltd'; 15 | var lokasiPerusahaan = [-6.088664, 106.996309]; // koordinat [lat,lon] 16 | var jarakMaxWFO = 1; // max jarak (dari koordinat perusahaan) yang diperbolehkan absensi WFO (dalam km) 17 | var jarakMaxWFH = 50; // max jarak WFH 18 | 19 | function doGet() { 20 | let templateIndex = HtmlService.createTemplateFromFile('index'); 21 | templateIndex.dt = { 22 | logo: getImage(idGambarLogo), 23 | perusahaan: namaPerusahaan 24 | }; 25 | return templateIndex 26 | .evaluate() 27 | .setSandboxMode(HtmlService.SandboxMode.IFRAME) 28 | .addMetaTag('viewport', 'width=device-width, initial-scale=1'); 29 | } 30 | 31 | class absensi { 32 | constructor(dt) { 33 | this.dt = dt; 34 | // validasi data 35 | this.dt.idPegawai = this.dt['idPegawai'] !== undefined ? this.dt.idPegawai.toString() : ''; 36 | this.dt.password = this.dt['password'] !== undefined ? this.dt.password.toString() : ''; 37 | this.dt.kegiatan = this.dt['kegiatan'] !== undefined ? this.dt.kegiatan.toString() : ''; 38 | this.dt.absensi = this.dt['absensi'] !== undefined ? this.dt.absensi.toString() : ''; 39 | this.dt.workFrom = this.dt['workFrom'] !== undefined ? this.dt.workFrom.toString() : ''; 40 | this.dt.position = this.dt['position'] !== undefined ? this.dt.position : [0, 0]; 41 | 42 | // get list pegawai --> from spreadsheet 43 | this.indexUser = -1; 44 | let user = SpreadsheetApp.openById(idDataPegawai); 45 | let userSheet = user.getSheetByName('Pegawai');//sheet 46 | this.user = userSheet.getRange(2, 1, userSheet.getRange("A1").getDataRegion().getLastRow() - 1, 10).getValues(); 47 | } 48 | saveAbsensi() { 49 | let abs = SpreadsheetApp.openById(idDataAbsensi); 50 | let absensi = abs.getSheetByName('Absensi');//sheet 51 | let location = Maps.newGeocoder().reverseGeocode(this.dt.position[0], this.dt.position[1]); // get alamat dari koordinat 52 | let user = this.user[this.indexUser]; 53 | absensi.appendRow([ 54 | new Date(), 55 | user[1], 56 | user[2], 57 | user[3], 58 | this.dt['absensi'], 59 | this.dt['workFrom'], 60 | this.dt['kegiatan'], 61 | this.distance, 62 | this.dt.position[0] + ',' + this.dt.position[1], 63 | location.results[0].formatted_address 64 | ]); 65 | return user[2]; 66 | } 67 | validUser() {//login 68 | let id = this.user.map(function (r) { return r[1].toString().toUpperCase(); }); 69 | let pass = this.user.map(function (r) { return r[4].toString(); }); 70 | this.indexUser = id.indexOf(this.dt.idPegawai.toUpperCase()); 71 | return (this.indexUser >= 0 && pass[this.indexUser] == this.dt.password) ? true : false; 72 | } 73 | validDistance() {// jarak dari perusahaan 74 | if (this.dt.position[0] == 0 && this.dt.position[1] == 0) { 75 | return 'Lokasi anda tidak terekam...'; 76 | } 77 | else { 78 | this.distance = this.getDistanceBetween(lokasiPerusahaan[0], lokasiPerusahaan[1], this.dt.position[0], this.dt.position[1]); 79 | if (this.distance > jarakMaxWFO && this.dt.workFrom == 'WFO') 80 | return 'Lokasi anda terlalu jauh
' + this.distance + 'km dari perusahaan
Maksimal jarak untuk WFO adalah ' + jarakMaxWFO + 'km.'; 81 | else if (this.distance > jarakMaxWFH && this.dt.workFrom == 'WFH') 82 | return 'Lokasi anda terlalu jauh.
' + this.distance + 'km dari perusahaan
Maksimal jarak untuk WFH adalah ' + jarakMaxWFH + 'km.'; 83 | else return 'confirm'; 84 | } 85 | } 86 | getDistanceBetween(originLat, originLong, destLat, destLong) { 87 | var directions, route; 88 | 89 | // Create a new Direction finder using Maps API available on Google Apps Script 90 | directions = Maps.newDirectionFinder() 91 | .setOrigin(originLat, originLong) 92 | .setDestination(destLat, destLong) 93 | .setMode(Maps.DirectionFinder.Mode.WALKING) 94 | .getDirections(); // Direction Object 95 | 96 | // The path may have some segments so it sum it all 97 | if (directions.routes[0]) { 98 | route = directions.routes[0].legs.reduce(function (acc, currentRow) { 99 | acc.dist += currentRow.distance.value / 1000; 100 | acc.dur += currentRow.duration.value / 60; 101 | return acc; 102 | }, { dist: 0, dur: 0 }); 103 | return route.dist; 104 | } else { 105 | return 0; 106 | } 107 | } 108 | } 109 | 110 | function submitAbsensi(dt) { 111 | let a = new absensi(dt); 112 | let ret = { 113 | success: false, 114 | msg: '' 115 | } 116 | 117 | if (!a.validUser()) ret.msg = 'Anda tidak terdaftar'; 118 | else { 119 | let cekJarak = a.validDistance(); 120 | if (cekJarak != 'confirm') ret.msg = cekJarak; 121 | else { 122 | ret.msg = a.saveAbsensi(); 123 | ret.success = true; 124 | } 125 | } 126 | return ret; 127 | } 128 | function getImage(fileId) { 129 | var img = DriveApp.getFileById(fileId).getBlob().getBytes(); 130 | return Utilities.base64Encode(img); 131 | } 132 | -------------------------------------------------------------------------------- /Versi 02/Pegawai.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fahroniganteng/Absensi-google-script/86f49f64c77f592ad4ccec2abb582fbcb4ad52c1/Versi 02/Pegawai.xlsx -------------------------------------------------------------------------------- /Versi 02/index.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 | 22 |

23 | 24 |

25 |
26 |
27 |
28 |

Formulir Absensi

29 |
30 | 31 | 32 |
33 |
34 | 35 | 36 |
37 |
38 | 39 |
40 | 41 | 42 |
43 |
44 | 45 | 46 |
47 |
48 |
49 | 50 |
51 | 52 | 53 |
54 |
55 | 56 | 57 |
58 |
59 |
60 | 61 | 62 |
63 |
64 | 65 | 69 | 70 |
71 |
72 |
73 |

© 2021 74 | 75 |

76 |
77 |
78 | 79 | 80 | 81 | 138 | 139 | -------------------------------------------------------------------------------- /Versi 03/firebase/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Absensi | fahroni ganteng 8 | 9 | 10 | 16 | 17 | -------------------------------------------------------------------------------- /Versi 03/google apps script/Data Absensi/2021-06.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fahroniganteng/Absensi-google-script/86f49f64c77f592ad4ccec2abb582fbcb4ad52c1/Versi 03/google apps script/Data Absensi/2021-06.xlsx -------------------------------------------------------------------------------- /Versi 03/google apps script/Data Pegawai.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fahroniganteng/Absensi-google-script/86f49f64c77f592ad4ccec2abb582fbcb4ad52c1/Versi 03/google apps script/Data Pegawai.xlsx -------------------------------------------------------------------------------- /Versi 03/google apps script/index.html: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 | 25 |

26 | 27 |

28 | 29 | 30 | 31 |

32 | Demo Aplikasi Web Absensi v.03
33 | 34 | YouTube Channel 35 | | 36 | 37 | Fork me at Github 38 | 39 |

40 | 41 | 42 | 43 |
44 |
45 |
46 |

Formulir Absensi

47 |
48 | 49 | 50 |
51 |
52 | 53 | 54 |
55 |
56 | 57 |
58 | 59 | 60 |
61 |
62 | 63 | 64 |
65 |
66 |
67 | 68 | 71 |
72 | 73 | 74 |
75 |
76 | 77 | 78 |
79 |
80 | 81 | 82 |
83 |
84 | 85 | 86 |
87 |
88 | 89 | 90 |
91 |
92 |
93 | 94 | 95 |
96 |
97 | 98 | 102 | 103 |
104 |
105 |
106 |

107 | Copyright © 2021 . All rights reserved 108 |

109 |
110 |
111 | 112 | 113 | 114 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /Versi 03/google apps script/kode.gs: -------------------------------------------------------------------------------- 1 | /* 2 | * ABSENSI - GOOGLE SCRIPT 3 | * *********************************************************************************** 4 | * Code by : fahroni|ganteng 5 | * Contact me : fahroniganteng@gmail.com 6 | * YouTube : https://www.youtube.com/c/FahroniGanteng 7 | * Github : https://github.com/fahroniganteng 8 | * Date : Jul 2021 9 | * License : MIT 10 | */ 11 | 12 | 13 | // SET VARIABLE 14 | // Silakan diganti variable dibawah ini menyesuaikan kebutuhan 15 | // -------------------------------------------------------------------------------------------------------------------- 16 | var idGambarLogo = '13ClVA2xO5SDEmruMMsxoOU2-YGY-Sh0q'; // id file logo di google drive 17 | var idDataPegawai = '1oFM55l2X4dkF6NnYc0BHwR7ZZmZZ9shXWQ9uKag5n18'; // id file spreadsheet 18 | var idFolderAbsensi = '1IOqpyDTKT12mOafF8sd1lz4ZF6jfU-84'; // id folder absensi 19 | var namaPerusahaan = 'Fahroni Ganteng co, ltd'; 20 | 21 | // koordinat [lat,lon] --> koordinat bisa diambil dari google map (cek video di channel youtube saya) 22 | var lokasiPerusahaan = [-6.186079, 106.978706]; 23 | 24 | // max jarak (dari koordinat perusahaan) yang diperbolehkan absensi dari kantor (dalam km) 25 | // jika perlu dalam meter isikan dalam koma, misal 100m isikan 0.1 26 | var jarakMaxWFO = 0.5; 27 | 28 | // max jarak absen dari rumah dalam km (bisa menggunakan koma juga) 29 | var jarakMaxWFH = 50; 30 | 31 | // Perhitungan potongan terlambat dan pulang awal 32 | var jamMasuk = '07:30:00'; 33 | var jamPulang = '16:00:00'; 34 | 35 | // potongan jika tidak absen masuk atau pulang (dalam jam) 36 | // dan digunakan sebagai maximal jumlah potongan 37 | // misal : 38 | // tidakAbsen = 4; 39 | // jamMasuk = '07:30:00'; 40 | // kemudian pegawai absen masuk jam 14:30:00 41 | // maka, keterlambatan sebenarnya = 7 jam, namun untuk potongan keterlambatan akan dihitung 4 jam 42 | var tidakAbsen = 4; 43 | 44 | 45 | /* 46 | * GET TIME ZONE 47 | * manual : https://developers.google.com/apps-script/reference/utilities/utilities#formatDate(Date,String,String) 48 | * manual format : https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html 49 | * valid code : 50 | * var timeZone = 'GMT+07:00'; 51 | * var timeZone = 'GMT+07'; 52 | * var timeZone = 'GMT+7'; 53 | * */ 54 | var timeZone = 'GMT+07:00'; 55 | 56 | /* 57 | * GET TIME ZONE CITY 58 | * manual : https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet#setSpreadsheetTimeZone(String) 59 | * list city : http://joda-time.sourceforge.net/timezones.html 60 | * valid code : 61 | * var timeZoneCity = 'Asia/Jakarta'; 62 | * */ 63 | var timeZoneCity = 'Asia/Jakarta'; 64 | 65 | /* 66 | * GET LOCALE 67 | * manual : https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet#setspreadsheetlocalelocale 68 | * list locale : https://wpastra.com/docs/complete-list-wordpress-locale-codes/ 69 | * valid code : 70 | * var localeCode = 'id_ID'; 71 | * var localeCode = 'id'; 72 | * */ 73 | var localeCode = 'id'; 74 | 75 | 76 | // CLI firebase deploy manual: 77 | // https://firebase.google.com/docs/hosting/?authuser=0#implementation_path 78 | 79 | // END SET VARIABLE 80 | // -------------------------------------------------------------------------------------------------------------------- 81 | 82 | 83 | 84 | 85 | 86 | 87 | // Class untuk proses absensi 88 | // -------------------------------------------------------------------------------------------------------------------- 89 | class absensi { 90 | constructor(dt) { // Fungsi yang di run pertama kali pada saat class dipanggil 91 | this.dt = dt; 92 | 93 | // validasi data dari http web (http request) 94 | this.dt.idPegawai = this.dt['idPegawai'] !== undefined ? this.dt.idPegawai.toString() : ''; 95 | this.dt.password = this.dt['password'] !== undefined ? this.dt.password.toString() : ''; 96 | this.dt.kegiatan = this.dt['kegiatan'] !== undefined ? this.dt.kegiatan.toString() : ''; 97 | this.dt.absensi = this.dt['absensi'] !== undefined ? this.dt.absensi.toString() : ''; 98 | this.dt.workFrom = this.dt['workFrom'] !== undefined ? this.dt.workFrom.toString() : ''; 99 | this.dt.position = this.dt['position'] !== undefined ? this.dt.position : [0, 0]; 100 | this.dt.kegiatan = this.dt['kegiatan'] !== undefined ? this.dt.kegiatan.toString() : ''; 101 | 102 | // get list data pegawai --> from spreadsheet 103 | this.indexUser = -1; 104 | let user = SpreadsheetApp.openById(idDataPegawai); 105 | let userSheet = user.getSheetByName('Pegawai');//sheet 106 | this.user = userSheet.getRange(2, 1, userSheet.getRange("A1").getDataRegion().getLastRow() - 1, 10).getValues(); 107 | } 108 | 109 | // Cek file spreadsheet (file : Data Absensi/yyyy-mm) di bulan terkait sudah ada atau belum 110 | getSpreatsheetDataAbsensi(now){ 111 | let spreadsheetName = Utilities.formatDate(now, timeZone, "yyyy-MM"); 112 | let sheetName = 'Absensi Pegawai'; 113 | let dir = DriveApp.getFolderById(idFolderAbsensi); 114 | let fileName = dir.getFilesByName(spreadsheetName); 115 | let dataAbsensi; // variable buat file spreadsheet 116 | let shDataAbsensi; // variable buat sheet 117 | 118 | // buat file spreadsheet jika belum ada 119 | if(!fileName.hasNext()){ 120 | dataAbsensi = SpreadsheetApp.create(spreadsheetName); // create file spreadsheet 121 | dataAbsensi.setSpreadsheetTimeZone(timeZoneCity); // set timezone spreadsheet 122 | dataAbsensi.setSpreadsheetLocale(localeCode); // set locale (lokasi) spreadsheet 123 | dataAbsensi.renameActiveSheet(sheetName); // rename sheet --> 'Absensi Pegawai' 124 | DriveApp.getFileById(dataAbsensi.getId()).moveTo(dir); // move to folder (create file spreadsheet berada di root google drive) 125 | shDataAbsensi = dataAbsensi.getSheetByName(sheetName); // open sheet 'Absensi Pegawai' 126 | 127 | // Bikin header table di baris pertama & format text nya 128 | shDataAbsensi.getRange('A1:O1') 129 | .setValues([[ 'TGL','ID','NAMA','BAGIAN','JAM MASUK','POTONGAN MASUK','STATUS MASUK','JARAK MASUK', 130 | 'LOKASI MASUK','JAM PULANG','POTONGAN PULANG','STATUS PULANG','JARAK PULANG', 131 | 'LOKASI PULANG','KEGIATAN']]) 132 | .setHorizontalAlignment('center') 133 | .setVerticalAlignment('middle') 134 | .setWrap(true) 135 | .setBackground('#000000') 136 | .setFontColor('#FFFFFF') 137 | .setFontFamily('Comfortaa') 138 | ; 139 | shDataAbsensi.getRange('E1:I1').setBackground('#274E13'); 140 | shDataAbsensi.getRange('J1:N1').setBackground('#660000'); 141 | shDataAbsensi.getRange('O:O').setWrap(true); 142 | // new row not formatted --> pindah tiap input data baru 143 | // shDataAbsensi.getRange('E:E').setNumberFormat('H:mm:ss'); 144 | // shDataAbsensi.getRange('J:J').setNumberFormat('H:mm:ss'); 145 | 146 | //set lebar kolom 147 | shDataAbsensi.setColumnWidth(9,150); 148 | shDataAbsensi.setColumnWidth(14,150); 149 | shDataAbsensi.setColumnWidth(15,300); 150 | } 151 | 152 | // buka file spreadsheet jika sudah ada 153 | else{ 154 | dataAbsensi = SpreadsheetApp.open(fileName.next()); 155 | shDataAbsensi = dataAbsensi.getSheetByName(sheetName); 156 | } 157 | 158 | return shDataAbsensi; 159 | } 160 | 161 | // Cek data absensi, apakah hari ini user sudah absen? 162 | getUserAbsen(shDataAbsensi,dateNow){ 163 | let user = this.user[this.indexUser]; 164 | let jmlDataAbsensi = shDataAbsensi.getRange("A1").getDataRegion().getLastRow() - 1; 165 | let indexAbsensi = -1;//belum ada data absensi di hari yang sama = -1 166 | if(jmlDataAbsensi>0){ 167 | let dataAbsensi = shDataAbsensi.getRange(2, 1, jmlDataAbsensi, 2).getValues(); 168 | for (let i=0; i telat = 0 187 | telat = telat>tidakAbsen?tidakAbsen:telat; // jika melebiji nilai tidakAbsen 188 | return telat; 189 | } 190 | 191 | // hitung potongan pulang lebih awal, dalam jam 192 | potonganPulang(now){ 193 | let dateNow = Utilities.formatDate(now, timeZone, 'yyyy-MM-dd'); 194 | let jamPulangToday = new Date(dateNow+'T'+jamPulang); 195 | let pulangAwal = jamPulangToday.getTime() - now.getTime(); // dalam mili second 196 | pulangAwal = Math.ceil(pulangAwal/(1000*60*60)); // convert ke jam (pembulatan keatas) 197 | pulangAwal = pulangAwal<0?0:pulangAwal; 198 | pulangAwal = pulangAwal>tidakAbsen?tidakAbsen:pulangAwal; 199 | return pulangAwal; 200 | } 201 | 202 | saveAbsensi() { 203 | let now = new Date(); 204 | let dateNow = Utilities.formatDate(now, timeZone, 'yyyy-MM-dd'); 205 | //let timeNow = Utilities.formatDate(now, timeZone, 'HH:mm:ss'); --> format jam tanpa tanggal, di read dari script berubah timezone 206 | let timeNow = Utilities.formatDate(now, timeZone, 'yyyy-MM-dd HH:mm:ss'); 207 | 208 | // buka atau bikin spreadsheet absensi bulanan --> tak taruh di fungsi sendiri biar mudah dimengerti & dibaca 209 | let shDataAbsensi = this.getSpreatsheetDataAbsensi(now); 210 | 211 | // cek user sudah absen atau belum --> tak taruh di fungsi sendiri biar mudah dimengerti & dibaca 212 | let indexAbsensi = this.getUserAbsen(shDataAbsensi,dateNow); 213 | 214 | let ret = {success : false, msg : ''}; // format untuk return 215 | let user = this.user[this.indexUser]; // get data user yang absen (fungsi di constructor) 216 | let masuk = this.dt.absensi=='Masuk'; // masuk = true; pulang = false 217 | let koordinat = this.dt.position[0] + ',' + this.dt.position[1]; 218 | let potonganMasuk = this.potonganMasuk(now); // get potongan terlambat (tak taruh di fungsi sendiri juga) 219 | let potonganPulang = this.potonganPulang(now); // get potongan pulang lebih awal (tak taruh di fungsi sendiri juga) 220 | 221 | // // get alamat dari koordinat --> gak jadi pakai, text terlalu panjang di laporan (spreadsheet) 222 | // let location = Maps.newGeocoder().reverseGeocode(this.dt.position[0], this.dt.position[1]); 223 | 224 | // user belum ada data absensi di hari yang sama (indexAbsensi= -1) --> sisipkan baris baru (paling bawah) 225 | if(indexAbsensi<0){ 226 | shDataAbsensi.appendRow([ 227 | dateNow, // 1. 'TGL' 228 | user[1], // 2. 'ID' 229 | user[2], // 3. 'NAMA' 230 | user[3], // 4. 'BAGIAN' 231 | masuk?timeNow:null, // 5. 'JAM MASUK' 232 | masuk?potonganMasuk:tidakAbsen, // 6. 'POTONGAN MASUK' 233 | masuk?this.dt['workFrom']:null, // 7. 'STATUS MASUK' 234 | masuk?this.distance:null, // 8. 'JARAK MASUK' 235 | masuk?koordinat:null, // 9. 'LOKASI MASUK' 236 | !masuk?timeNow:null, // 10. 'JAM PULANG' 237 | !masuk?potonganPulang:tidakAbsen, // 11. 'POTONGAN PULANG' 238 | !masuk?this.dt['workFrom']:null, // 12.'STATUS PULANG' 239 | !masuk?this.distance:null, // 13.'JARAK PULANG' 240 | !masuk?koordinat:null, // 14.'LOKASI PULANG' 241 | this.dt['kegiatan'], // 15.'KEGIATAN' 242 | ]); 243 | 244 | 245 | ret.success = true; 246 | ret.msg = user[2]; 247 | } 248 | 249 | // user sudah ada data absensi di hari yang sama --> update data 250 | else{ 251 | // index dimulai dari 0 dan baris dimulai dari 1 --> baris 1 (paling atas) dipakai header table 252 | // jadi data absensi index 0 = baris 2 253 | indexAbsensi = indexAbsensi + 2; 254 | 255 | let absenMasuk = shDataAbsensi.getRange(indexAbsensi,5).getValue(); 256 | 257 | // larangan absen masuk lebih dari sekali dalam sehari 258 | if(masuk && absenMasuk!='') 259 | ret.msg = 'Anda sudah melakukan absensi masuk jam '+Utilities.formatDate(absenMasuk, timeZone, 'HH:mm:ss'); 260 | 261 | // absen masuk lagi (jika salah absen pulang duluan) 262 | else if(masuk){ 263 | shDataAbsensi.getRange(indexAbsensi,5,1,5).setValues([[ 264 | timeNow, // 5. 'JAM MASUK' 265 | potonganMasuk, // 6. 'POTONGAN MASUK' 266 | this.dt['workFrom'], // 7. 'STATUS MASUK' 267 | this.distance, // 8. 'JARAK MASUK' 268 | koordinat, // 9. 'LOKASI MASUK' 269 | ]]); 270 | ret.success = true; 271 | ret.msg = user[2]; 272 | } 273 | 274 | // absen pulang 275 | else{ 276 | shDataAbsensi.getRange(indexAbsensi,10,1,6).setValues([[ 277 | timeNow, // 10. 'JAM PULANG' 278 | potonganPulang, // 11. 'POTONGAN PULANG' 279 | this.dt['workFrom'], // 12. 'STATUS PULANG' 280 | this.distance, // 13. 'JARAK PULANG' 281 | koordinat, // 14. 'LOKASI PULANG' 282 | this.dt['kegiatan'], // 15. 'KEGIATAN' 283 | ]]); 284 | ret.success = true; 285 | ret.msg = user[2]; 286 | } 287 | } 288 | 289 | // format jam masuk & pulang 290 | // (jika cell kosong pidah format otomatis, jadi tiap ada data baru harus format lagi) 291 | shDataAbsensi.getRange('E:E').setNumberFormat('H:mm:ss'); 292 | shDataAbsensi.getRange('J:J').setNumberFormat('H:mm:ss'); 293 | 294 | return ret; 295 | } 296 | 297 | // cek login user ID & password 298 | validUser() { 299 | let id = this.user.map(function (r) { return r[1].toString().toUpperCase(); }); 300 | let pass = this.user.map(function (r) { return r[4].toString(); }); 301 | this.indexUser = id.indexOf(this.dt.idPegawai.toUpperCase()); 302 | return (this.indexUser >= 0 && pass[this.indexUser] == this.dt.password) ? true : false; 303 | } 304 | 305 | // cek jarak dari perusahaan 306 | validDistance() { 307 | // sesuaikan dengan list Lokasi & status absensi di index.html 308 | let bolehAbsenDariRumah = ['WFH','Ijin']; 309 | 310 | if (this.dt.position[0] == 0 && this.dt.position[1] == 0) 311 | return 'Lokasi anda tidak terekam...'; 312 | else { 313 | // get jarak kantor dengan lokasi absen 314 | this.distance = this.getDistanceBetween(lokasiPerusahaan[0], lokasiPerusahaan[1], this.dt.position[0], this.dt.position[1]); 315 | 316 | // pilih lokasi absen kantor dan jarak > jarakMaxWFO 317 | if (this.distance > jarakMaxWFO && !bolehAbsenDariRumah.includes(this.dt.workFrom)) 318 | return 'Lokasi anda terlalu jauh
' + 323 | this.distance + 'km dari perusahaan
' + 324 | 'Maksimal jarak untuk '+ this.dt.workFrom +' adalah ' + jarakMaxWFO + 'km.'; 325 | 326 | // pilih lokasi rumah dan jarak > jarakMaxWFH 327 | else if (this.distance > jarakMaxWFH && bolehAbsenDariRumah.includes(this.dt.workFrom)) 328 | return 'Lokasi anda terlalu jauh
' + 333 | this.distance + 'km dari perusahaan
' + 334 | 'Maksimal jarak untuk '+ this.dt.workFrom +' adalah ' + jarakMaxWFH + 'km.'; 335 | 336 | // jarak valid 337 | else return 'confirm'; 338 | } 339 | } 340 | getDistanceBetween(originLat, originLong, destLat, destLong) { 341 | var directions, route; 342 | 343 | // Create a new Direction finder using Maps API available on Google Apps Script 344 | directions = Maps.newDirectionFinder() 345 | .setOrigin(originLat, originLong) 346 | .setDestination(destLat, destLong) 347 | .setMode(Maps.DirectionFinder.Mode.WALKING) 348 | .getDirections(); // Direction Object 349 | 350 | // The path may have some segments so it sum it all 351 | if (directions.routes[0]) { 352 | route = directions.routes[0].legs.reduce(function (acc, currentRow) { 353 | acc.dist += currentRow.distance.value / 1000; 354 | acc.dur += currentRow.duration.value / 60; 355 | return acc; 356 | }, { dist: 0, dur: 0 }); 357 | return route.dist; 358 | } else { 359 | return 0; 360 | } 361 | } 362 | } 363 | // END Class proses absensi 364 | // -------------------------------------------------------------------------------------------------------------------- 365 | 366 | 367 | 368 | // jika web diakses/dibuka, yang pertama kali dijalankan adalah fungsi ini 369 | // -------------------------------------------------------------------------------------------------------------------- 370 | function doGet() { 371 | let templateIndex = HtmlService.createTemplateFromFile('index'); // baca index.html 372 | templateIndex.dt = { // kirim variable ke index.html 373 | logo: getImage(idGambarLogo), 374 | perusahaan: namaPerusahaan 375 | }; 376 | return templateIndex 377 | .evaluate() 378 | .setSandboxMode(HtmlService.SandboxMode.IFRAME) 379 | .addMetaTag('viewport', 'width=device-width, initial-scale=1') // biar auto scale jika dibuka via HP 380 | .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL); // allow embed --> iframe (buat ilangin banner google) 381 | } 382 | 383 | // Fungsi buat handle request dari index.html --> klik simpan absensi di web 384 | // -------------------------------------------------------------------------------------------------------------------- 385 | function submitAbsensi(dt) { 386 | let a = new absensi(dt); // panggil class absensi 387 | let ret = { success: false, msg: '' }; // format return (web request) 388 | 389 | if (!a.validUser()) ret.msg = 'Anda tidak terdaftar'; // cek user & password ada di list daftar pegawai 390 | else{ 391 | let cekJarak = a.validDistance(); // get jarak lokasi absen & kantor 392 | if (cekJarak != 'confirm') ret.msg = cekJarak; // jika tidak valid kembalikan link google map direction lokasi ke kantor 393 | else ret = a.saveAbsensi(); // jika valid 394 | } 395 | return ret; 396 | } 397 | 398 | // Fungsi buat buka gambar (dipake buat buka logo.png doang) 399 | // -------------------------------------------------------------------------------------------------------------------- 400 | function getImage(fileId) { 401 | var img = DriveApp.getFileById(fileId).getBlob().getBytes(); // get image dari drive berdasarkan id file 402 | return Utilities.base64Encode(img); // convert image ke base64Encode 403 | } 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | // Fungsi dibawah ini hanya untuk test input data absen saja -------------------------------------- 412 | function testInput(){ 413 | let dt= { 414 | idPegawai : '20210001MT', 415 | password : '20210001', 416 | kegiatan : '', 417 | absensi : 'Masuk', 418 | workFrom : 'WFH', 419 | position : [-6.089501, 106.997263], 420 | kegiatan : 'test', 421 | } 422 | let a = new absensi(dt); 423 | if (!a.validUser()) Logger.log('Anda tidak terdaftar'); 424 | else { 425 | let cekJarak = a.validDistance(); 426 | if (cekJarak != 'confirm') Logger.log(cekJarak); 427 | else { 428 | Logger.log(a.saveAbsensi()); 429 | } 430 | } 431 | } 432 | 433 | 434 | -------------------------------------------------------------------------------- /Versi 03/google apps script/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fahroniganteng/Absensi-google-script/86f49f64c77f592ad4ccec2abb582fbcb4ad52c1/Versi 03/google apps script/logo.png -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fahroniganteng/Absensi-google-script/86f49f64c77f592ad4ccec2abb582fbcb4ad52c1/logo.png --------------------------------------------------------------------------------