Google sheet — Flutter

RMAG news

Trying to implement and use connect Gsheet.

I’m a noob in the Dart coding language, and have a hard time debugging code I don’t understand. I created a calendar that should save data in a google sheet. If you click on a date, a sliding_up_panel will appear, in that window you can enter your worked hours directly, or use time-picker which calculates your worked hours for you and insert them in the number fields, in the Dropdown menu you select the type of working hours you have worked, if they are regular working hours with your standard salary, or if you have worked overtime that gives you more pay, etc. clicking “SPARA”‘save’ button, it should send the hours you enter to the right column in the google sheet from the dropdown menu. The google sheet will automatically calculate your hours, deduct tax and give you your salary specification the moment your worked hours reach the sheet. You can then use the hamburger menu on the top left, ore swipe from the left and a new side bar will appear and show you your salary and total hours worked.

The dropdown will send the input hours to the respective column in the sheet. ‘Normal working time’ to “D9:D39”, ‘Overtime hours’ to E9:E39, ‘Holiday pay’ to F9:F39.

I have been trying for months now, in every way possible, to create these functions, without success. and I understand that it can be difficult for you to explain in detail all the steps that need to be done to achieve this. So my question is instead! Can someone please reproduce this script with these functions and post a copy of my app, with a working code, so we all noobs can learn how to do this in the future projects?.

import ‘package:http/http.dart’ as http;
import ‘package:flutter/material.dart’;
import ‘package:gsheets/gsheets.dart’;
import ‘package:csv/csv.dart’;

// ——————————————————-
const _apiCreds = r”’
{
“type”: “service_account”,
“project_id”: “timmraport”,
“private_key_id”: “5b160bf14c57354516932aa4d516”,
“private_key”: “—–BEGIN PRIVATE KEY—–n”,
“client_email”: “timr@tiport.iam.gserviceaccount.com”,
“client_id”: “11386613607215”,
“auth_uri”: “https://accounts.google.com/o/oauth2/auth”,
“token_uri”: “https://oauth2.googleapis.com/token”,
“auth_provider_x509_cert_url”: “https://www.googleapis.com/oauth2/v1/certs”,
“client_x509_cert_url”: “https://www.googleapis.com/robot/v1/metadata/x509/timraport%40timmraport.iam.gserviceaccount.com”,
“universe_domain”: “googleapis.com”
}
”’;

const _sheetID = ‘1HkA4j2UnAPIf6OwkHhcXSDplzs6L4FytpTnc1MvK1JA’;
const _worksheetName = ‘Sheet1’;

// ——————————————————-

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: Text(
‘Min kalender & Timlista’,
style: TextStyle(color: Color(0xFF02AF75)),
),
backgroundColor: Color(0xFF17181A),
),
drawer: DrawerContents(),
body: Calendar(),
),
);
}
}

class Calendar extends StatefulWidget {
@override
_CalendarState createState() => _CalendarState();
}

class _CalendarState extends State<Calendar> {
List<List<String>> workingHoursData = [];

@override
void initState() {
super.initState();
fetchWorkingHoursData();
}

Future<void> fetchWorkingHoursData() async {
final response = await http.get(Uri.parse(
‘https://docs.google.com/spreadsheets/d/1HkA4j2UnAPIf6OwkHhcXSDplzs6L4FytpTnc1MvK1JA/export?format=csv&gid=585707959’));

if (response.statusCode == 200) {
final List<List<dynamic>> csvTable =
const CsvToListConverter().convert(response.body);
setState(() {
// Skip the headers (rows 1 to 8) and fetch the desired rows (D9 to F39)
workingHoursData = csvTable.skip(8).take(31).map((row) {
// Select only columns D, E, and F (indices 3, 4, and 5)
return [row[3].toString(), row[4].toString(), row[5].toString()];
}).toList();
});
} else {
throw Exception(‘Failed to load data from Google Spreadsheet’);
}
}

@override
Widget build(BuildContext context) {
return Container(
color: Color(0xFF17181A),
child: Column(
children: <Widget>[
Expanded(
child: Center(
child: Text(
‘April 2024’,
style: TextStyle(
color: Color.fromARGB(255, 2, 175, 117),
fontSize: 30.0,
fontWeight: FontWeight.bold,
),
),
),
),
Expanded(
flex: 8,
child: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 7,
crossAxisSpacing: 1.0,
mainAxisSpacing: 1.0,
childAspectRatio: 0.5,
),
itemCount: 30,
itemBuilder: (BuildContext context, int index) {
bool isToday = DateTime.now().day == index + 1;
String cellData = index < workingHoursData.length
? workingHoursData[index]
.where((element) =>
element.isNotEmpty) // Filter out empty strings
.join(“, “)
: ”;

return GestureDetector(
onTap: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (BuildContext context) {
return Theme(
data: ThemeData.dark(),
child: SingleChildScrollView(
child: Container(
padding: EdgeInsets.only(
bottom:
MediaQuery.of(context).viewInsets.bottom,
),
child: TimeDifferenceCalculator(),
),
),
);
},
);
},
child: Stack(
children: [
Container(
decoration: BoxDecoration(
color: isToday
? Color.fromARGB(255, 0, 119, 255)
: Color.fromARGB(255, 23, 24, 26),
borderRadius: BorderRadius.circular(8.0),
border: Border.all(
color: Color.fromARGB(120, 61, 71, 86),
width: 1.0,
),
),
child: Center(
child: Text(
‘${index + 1}’,
style: TextStyle(
color: Color(0xFFF3F4F5),
fontSize: 25.0,
fontWeight: FontWeight.bold,
),
),
),
),
if (cellData.isNotEmpty) // Show data only if available
Positioned(
bottom: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Color(0x0002AF75),
),
padding: EdgeInsets.all(10),
child: Text(
cellData,
style: TextStyle(
color: Color.fromARGB(255, 2, 175, 117),
),
),
),
),
],
),
);
},
),
),
],
),
);
}
}

class TimeDifferenceCalculator extends StatefulWidget {
@override
_TimeDifferenceCalculatorState createState() =>
_TimeDifferenceCalculatorState();
}

class _TimeDifferenceCalculatorState extends State<TimeDifferenceCalculator> {
TimeOfDay? _startTime = TimeOfDay.now();
TimeOfDay? _endTime = TimeOfDay.now();
bool _hasBreak = false;
String _selectedCategory = ‘Normal working time’;
TextEditingController _hoursController = TextEditingController();
TextEditingController _minutesController = TextEditingController();

double _textSize = 16.0;

void _calculateDifference() {
if (_startTime != null && _endTime != null) {
final int startMinutes = _startTime!.hour * 60 + _startTime!.minute;
final int endMinutes = _endTime!.hour * 60 + _endTime!.minute;
int difference = endMinutes – startMinutes;

if (_hasBreak) {
difference -= 30;
}

int hours = difference ~/ 60;
int minutes = difference % 60;

showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
‘Dina arbetade timmar …’,
style: TextStyle(
color: Color(0xFF02AF75),
fontSize: _textSize,
),
),
content: Text(
‘$hours tim, $minutes min.’,
style: TextStyle(
color: Color(0xFF02AF75),
fontSize: 30,
),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
‘Lägg till’,
style: TextStyle(
color: Color(0xFF02AF75),
fontSize: _textSize,
),
),
),
],
);
},
);

_hoursController.text = hours.toString();
_minutesController.text = minutes.toString();
}
}

@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Color(0xFF202123),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.0),
topRight: Radius.circular(20.0),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
‘Typ av arbetstid:’,
style: TextStyle(
color: Color(0xFF02AF75),
fontSize: 20,
),
),
DropdownButton<String>(
value: _selectedCategory,
onChanged: (String? newValue) {
setState(() {
_selectedCategory = newValue!;
});
},
items: <String>[
‘Normal working time’,
‘Overtime hours’,
‘Holiday pay’
].map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
],
),
SizedBox(height: 16.0),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 60.0,
child: TextFormField(
controller: _hoursController,
keyboardType: TextInputType.number,
maxLength: 2,
textAlign: TextAlign.center,
style: TextStyle(
color: Color.fromARGB(255, 255, 255, 255),
fontSize: 25,
),
),
),
Text(
‘,’,
style: TextStyle(
color: Color.fromARGB(255, 255, 255, 255),
fontSize: 20,
),
),
SizedBox(
width: 60.0,
child: TextFormField(
controller: _minutesController,
keyboardType: TextInputType.number,
maxLength: 2,
textAlign: TextAlign.center,
style: TextStyle(
color: Color.fromARGB(255, 255, 255, 255),
fontSize: 25,
),
),
),
SizedBox(
width: 20.0,
),
ElevatedButton(
onPressed: _calculateDifference,
child: Text(
‘SPARA’,
style: TextStyle(
color: Color(0xFF02AF75),
fontSize: 20,
),
),
),
],
),
SizedBox(height: 16.0),
Container(
height: 1,
color: Color.fromARGB(255, 50, 50, 50),
margin: EdgeInsets.symmetric(vertical: 16.0),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
‘Starttid’,
style: TextStyle(
color: Color(0xFF02AF75),
fontSize: 20,
),
),
TextButton(
onPressed: () async {
TimeOfDay? selectedTime = await showTimePicker(
context: context,
initialTime: _startTime!,
);
if (selectedTime != null) {
setState(() {
_startTime = selectedTime;
});
}
},
child: Text(
‘${_startTime!.hour}:${_startTime!.minute.toString().padLeft(2, ‘0’)}’,
style: TextStyle(
color: Color(0xFF02AF75),
fontSize: 30,
),
),
),
],
),
SizedBox(height: 16.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
‘Sluttid’,
style: TextStyle(
color: Color(0xFF02AF75),
fontSize: 20,
),
),
TextButton(
onPressed: () async {
TimeOfDay? selectedTime = await showTimePicker(
context: context,
initialTime: _endTime!,
);
if (selectedTime != null) {
setState(() {
_endTime = selectedTime;
});
}
},
child: Text(
‘${_endTime!.hour}:${_endTime!.minute.toString().padLeft(2, ‘0’)}’,
style: TextStyle(
color: Color(0xFF02AF75),
fontSize: 30,
),
),
),
],
),
SizedBox(height: 16.0),
Row(
children: [
Checkbox(
value: _hasBreak,
onChanged: (bool? value) {
setState(() {
_hasBreak = value!;
});
},
),
Text(
‘Hade du rast? (30 min)’,
style: TextStyle(
color: Color(0xFF02AF75),
fontSize: _textSize,
),
),
],
),
SizedBox(height: 16.0),
ElevatedButton(
onPressed: _calculateDifference,
child: Text(
‘Räkna ut’,
style: TextStyle(
color: Color(0xFF02AF75),
fontSize: _textSize,
),
),
),
],
),
);
}
}

class DrawerContents extends StatelessWidget {
Future<String> fetchCellData(String url, String cell) async {
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final List<List<dynamic>> csvTable =
const CsvToListConverter().convert(response.body);
String cellData = csvTable[int.parse(cell.substring(1)) – 1]
[cell.codeUnitAt(0) – 65]
.toString();
return cellData;
} else {
throw Exception(‘Failed to load data from Google Spreadsheet’);
}
}

@override
Widget build(BuildContext context) {
return Container(
color: Color(0xFF202123),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 50.0),
child: Text(
‘Specification’,
style: TextStyle(
fontSize: 25.0,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 2, 175, 117),
),
),
),
Divider(
color: Color(0xFF02AF75),
height: 1,
thickness: 1,
),
ListTile(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 90.0, vertical: 30.0),
child: Text(
‘Mina timmar:’,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
color: Color(0xFF02AF75),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 86.0, vertical: 1.0),
child: FutureBuilder(
future: fetchCellData(
‘https://docs.google.com/spreadsheets/d/1HkA4j2UnAPIf6OwkHhcXSDplzs6L4FytpTnc1MvK1JA/export?format=csv&gid=585707959’,
‘H41’,
),
builder:
(BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text(‘Error: ${snapshot.error}’);
} else {
return Text(
‘${snapshot.data ?? “”}’,
style: TextStyle(
fontSize: 40.0,
color: Color.fromARGB(255, 2, 175, 117),
),
);
}
},
),
),
],
),
),
Divider(
color: Color.fromARGB(255, 114, 119, 129),
height: 30,
thickness: 1,
),
ListTile(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 120.0, vertical: 30.0),
child: Text(
‘Min lön:’,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 2, 175, 117),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 90.0, vertical: 1.0),
child: FutureBuilder(
future: fetchCellData(
‘https://docs.google.com/spreadsheets/d/1HkA4j2UnAPIf6OwkHhcXSDplzs6L4FytpTnc1MvK1JA/export?format=csv&gid=585707959’,
‘I43’,
),
builder:
(BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text(‘Error: ${snapshot.error}’);
} else {
return Text(
‘${snapshot.data ?? “”}’,
style: TextStyle(
fontSize: 40.0,
color: Color.fromARGB(255, 2, 175, 117),
),
);
}
},
),
),
],
),
),
],
),
);
}
}`

Leave a Reply

Your email address will not be published. Required fields are marked *