Search code examples
javascriptfluttergoogle-apps-script

Data is not being updated to Google Sheets from the Flutter app


I'm using Google Sheets as the data source, and I've set up a Google Sheets API using Google Apps Script within Google Sheets. When testing with Postman, the data successfully enters into Google Sheets. However, when running the Flutter app, the data isn't being recorded in Google Sheets, and an error mentioning "XMLHTTPRequest" is displayed.

The code in apps script:

// Function to handle GET requests`your text`
function doGet(e) {
  return ContentService.createTextOutput('GET request received. Please use POST method to send data.')
                       .setMimeType(ContentService.MimeType.TEXT);
}
var sheet = SpreadsheetApp.openByUrl('https://docs.google.com/spreadsheets.......').getSheetByName('Sheet1');
 
// Function to handle POST requests
function doPost(e) {
  function doPost(e) {
  var response = {status: 'success', message: 'Data received'};
  return ContentService.createTextOutput(JSON.stringify(response))
                       .setMimeType(ContentService.MimeType.JSON)
                       .setHeader('Access-Control-Allow-Origin', '*')
                       .setHeader('Access-Control-Allow-Methods', 'POST')
                       .setHeader('Access-Control-Allow-Headers', 'Content-Type');
}
 
  if (!e || !e.postData || !e.postData.contents) {
    return ContentService.createTextOutput(JSON.stringify({status: 'error', message: 'Invalid POST request'}))
                         .setMimeType(ContentService.MimeType.JSON);
  }
 
  try {
    // Parse the JSON data from the POST request
    var jsonData = JSON.parse(e.postData.contents);
 
    // Extract data (assuming the JSON object has the correct keys)
    var branch = jsonData.branch || '';
    var shop = jsonData.shop || '';
    var deliveryMode = jsonData.deliveryMode || '';
    var creditDays = jsonData.creditDays || '';
    var discountPercentage = jsonData.discountPercentage || '';
    var remarks = jsonData.remarks || '';
    var items = jsonData.items || [];
    var totalQuantity = jsonData.totalQuantity || '';
    var subtotal = jsonData.subtotal || '';
    var discountAmount = jsonData.discountAmount || '';
    var netAmount = jsonData.netAmount || '';

 
    items.forEach(function(item) {
      var itemName = item.itemName || '';
      var quantity = item.quantity || '';
      var rate = item.rate || '';
      var total = item.total || '';
 
      // Append data to the sheet
      sheet.appendRow([branch, shop, deliveryMode, creditDays, discountPercentage, remarks, itemName, quantity, rate, total,totalQuantity,subtotal,discountAmount,netAmount]);
    });
 
    // Return a success response
    return ContentService.createTextOutput(JSON.stringify({status: 'success'}))
                         .setMimeType(ContentService.MimeType.JSON);
  } catch (error) {
    return ContentService.createTextOutput(JSON.stringify({status: 'error', message: error.toString()}))
                         .setMimeType(ContentService.MimeType.JSON);
  }
}

The code in main.dart in visual code:

import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SOB',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        primaryColor: Colors.blue,
        colorScheme: ColorScheme.fromSwatch().copyWith(secondary: Colors.orange),
        fontFamily: 'Roboto',
      ),
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home Page'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => SalesOrderForm()),
            );
          },
          child: Text('Go to Sales Order Form'),
        ),
      ),
    );
  }
}

class SalesOrderForm extends StatefulWidget {
  @override
  _SalesOrderFormState createState() => _SalesOrderFormState();
}

class _SalesOrderFormState extends State<SalesOrderForm> {
  String? _selectedBranch;
  String? _selectedShop;
  String? _selectedDeliveryMode;
  List<String> _branches = ['Branch A', 'Branch B', 'Branch C'];
  List<String> _shops = ['Shop 1', 'Shop 2', 'Shop 3'];
  List<String> _deliveryModes = ['Standard', 'Express', 'Next Day'];
  final TextEditingController _creditDaysController = TextEditingController();
  final TextEditingController _discountPercentageController = TextEditingController();
  final TextEditingController _remarksController = TextEditingController();
  List<Item> _items = [];
  final _formKey = GlobalKey<FormState>();

  double get subtotal {
    return _items.fold(0.0, (sum, item) => sum + item.total);
  }

  double get discountAmount {
    double discountPercentage = double.tryParse(_discountPercentageController.text) ?? 0.0;
    return subtotal * (discountPercentage / 100);
  }

  double get netAmount {
    return subtotal - discountAmount;
  }

  int get totalQuantity {
    return _items.fold(0, (sum, item) => sum + item.quantity);
  }

  @override
  void dispose() {
    _creditDaysController.dispose();
    _discountPercentageController.dispose();
    _remarksController.dispose();
    super.dispose();
  }

  void _updateUI() {
    setState(() {});
  }

  Future<void> _submitForm() async {
    if (_formKey.currentState!.validate()) {
      try {
        final response = await http.post(
          Uri.parse('https://script.google.com/macros/s/...../exec'),
          headers: <String, String>{
            'Content-Type': 'application/json; charset=UTF-8',
          },
          body: jsonEncode(<String, dynamic>{
            'branch': _selectedBranch,
            'shop': _selectedShop,
            'deliveryMode': _selectedDeliveryMode,
            'creditDays': _creditDaysController.text,
            'discountPercentage': _discountPercentageController.text,
            'remarks': _remarksController.text,
            'items': _items.map((item) => item.toMap()).toList(),
            'totalQuantity': totalQuantity.toString(),
            'subtotal': subtotal.toString(),
            'discountAmount': discountAmount.toString(),
            'netAmount': netAmount.toString(),
          }),
        );

        if (response.statusCode == 200) {
          final result = json.decode(response.body);
          if (result['status'] == 'success') {
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Data updated successfully')));
          } else {
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to update data: ${result['message']}')));
          }
        } else {
          ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to update data')));
        }
      } catch (e) {
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: ${e.toString()}')));
      }
    }
  }

  void _addItem() {
    setState(() {
      _items.add(Item());
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Sales Order Form'),
        centerTitle: true,
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: SingleChildScrollView(
          child: Form(
            key: _formKey,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: <Widget>[
                Text(
                  'Branch Name',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
                DropdownButtonFormField<String>(
                  decoration: InputDecoration(
                    filled: true,
                    fillColor: Colors.white24,
                    border: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(10.0),
                    ),
                  ),
                  value: _selectedBranch,
                  onChanged: (newValue) {
                    setState(() {
                      _selectedBranch = newValue;
                    });
                  },
                  items: _branches.map((branch) {
                    return DropdownMenuItem<String>(
                      value: branch,
                      child: Text(branch),
                    );
                  }).toList(),
                  validator: (value) => value == null ? 'Please select a branch' : null,
                ),
                SizedBox(height: 20),
                Text(
                  'Shop Name',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
                DropdownButtonFormField<String>(
                  decoration: InputDecoration(
                    filled: true,
                    fillColor: Colors.white24,
                    border: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(10.0),
                    ),
                  ),
                  value: _selectedShop,
                  onChanged: (newValue) {
                    setState(() {
                      _selectedShop = newValue;
                    });
                  },
                  items: _shops.map((shop) {
                    return DropdownMenuItem<String>(
                      value: shop,
                      child: Text(shop),
                    );
                  }).toList(),
                  validator: (value) => value == null ? 'Please select a shop' : null,
                ),
                SizedBox(height: 20),
                Text(
                  'Delivery Mode',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
                DropdownButtonFormField<String>(
                  decoration: InputDecoration(
                    filled: true,
                    fillColor: Colors.white24,
                    border: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(10.0),
                    ),
                  ),
                  value: _selectedDeliveryMode,
                  onChanged: (newValue) {
                    setState(() {
                      _selectedDeliveryMode = newValue;
                    });
                  },
                  items: _deliveryModes.map((mode) {
                    return DropdownMenuItem<String>(
                      value: mode,
                      child: Text(mode),
                    );
                  }).toList(),
                  validator: (value) => value == null ? 'Please select a delivery mode' : null,
                ),
                SizedBox(height: 20),
                Text(
                  'Credit Days',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
                TextFormField(
                  controller: _creditDaysController,
                  decoration: InputDecoration(
                    filled: true,
                    fillColor: Colors.white24,
                    border: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(10.0),
                    ),
                  ),
                  keyboardType: TextInputType.number,
                  validator: (value) {
                    if (value == null || value.isEmpty) {
                      return 'Please enter credit days';
                    }
                    if (int.tryParse(value) == null) {
                      return 'Please enter a valid number';
                    }
                    return null;
                  },
                ),
                SizedBox(height: 20),
                Text(
                  'Discount Percentage',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
                TextFormField(
                  controller: _discountPercentageController,
                  decoration: InputDecoration(
                    filled: true,
                    fillColor: Colors.white24,
                    border: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(10.0),
                    ),
                  ),
                  keyboardType: TextInputType.number,
                  validator: (value) {
                    if (value == null || value.isEmpty) {
                      return 'Please enter discount percentage';
                    }
                    if (double.tryParse(value) == null) {
                      return 'Please enter a valid number';
                    }
                    return null;
                  },
                ),
                SizedBox(height: 20),
                Text(
                  'Remarks',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
                TextFormField(
                  controller: _remarksController,
                  decoration: InputDecoration(
                    filled: true,
                    fillColor: Colors.white24,
                    border: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(10.0),
                    ),
                  ),
                  keyboardType: TextInputType.multiline,
                  maxLines: 3,
                ),
                SizedBox(height: 20),
                Text(
                  'Items',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
                ListView.builder(
                  shrinkWrap: true,
                  itemCount: _items.length,
                  itemBuilder: (context, index) {
                    return ItemWidget(
                      item: _items[index],
                      onDelete: () {
                        setState(() {
                          _items.removeAt(index);
                        });
                      },
                      onUpdate: _updateUI,
                    );
                  },
                ),
                TextButton.icon(
                  onPressed: _addItem,
                  icon: Icon(Icons.add),
                  label: Text('Add Item'),
                ),
                SizedBox(height: 20),
                Text(
                  'Total Quantity: $totalQuantity',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
                Text(
                  'Subtotal: \$${subtotal.toStringAsFixed(2)}',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
                Text(
                  'Discount Amount: \$${discountAmount.toStringAsFixed(2)}',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
                Text(
                  'Net Amount: \$${netAmount.toStringAsFixed(2)}',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
                SizedBox(height: 20),
                ElevatedButton(
                  onPressed: _submitForm,
                  child: Text('Submit'),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class Item {
  String itemName;
  int quantity;
  double rate;

  Item({this.itemName = '', this.quantity = 0, this.rate = 0.0});

  double get total => quantity * rate;

  Map<String, dynamic> toMap() {
    return {
      'itemName': itemName,
      'quantity': quantity,
      'rate': rate,
      'total': total,
    };
  }
}

class ItemWidget extends StatelessWidget {
  final Item item;
  final VoidCallback onDelete;
  final VoidCallback onUpdate;

  const ItemWidget({
    required this.item,
    required this.onDelete,
    required this.onUpdate,
  });

  @override
  Widget build(BuildContext context) {
    final TextEditingController itemNameController = TextEditingController(text: item.itemName);
    final TextEditingController quantityController = TextEditingController(text: item.quantity.toString());
    final TextEditingController rateController = TextEditingController(text: item.rate.toString());

    return Card(
      margin: EdgeInsets.symmetric(vertical: 8.0),
      child: Padding(
        padding: EdgeInsets.all(8.0),
        child: Column(
          children: [
            TextFormField(
              controller: itemNameController,
              decoration: InputDecoration(labelText: 'Item Name'),
              onChanged: (value) {
                item.itemName = value;
                onUpdate();
              },
            ),
            TextFormField(
              controller: quantityController,
              decoration: InputDecoration(labelText: 'Quantity'),
              keyboardType: TextInputType.number,
              onChanged: (value) {
                item.quantity = int.tryParse(value) ?? 0;
                onUpdate();
              },
            ),
            TextFormField(
              controller: rateController,
              decoration: InputDecoration(labelText: 'Rate'),
              keyboardType: TextInputType.number,
              onChanged: (value) {
                item.rate = double.tryParse(value) ?? 0.0;
                onUpdate();
              },
            ),
            SizedBox(height: 8.0),
            Text('Total: \$${item.total.toStringAsFixed(2)}'),
            SizedBox(height: 8.0),
            TextButton.icon(
              onPressed: onDelete,
              icon: Icon(Icons.delete),
              label: Text('Delete'),
              style: TextButton.styleFrom(foregroundColor: Colors.red),
            ),
          ],
        ),
      ),
    );
  }
}

The screenshot of the sales order form we are updating is: The output in googlesheet format:


Solution

  • The function doPost declares an inner function named doPost too, but this inner doPost function is not called.

    I also think there should not be calls to Google Apps Script methods that require authorization at the top level / global scope, as this might cause authorization problems.

    There might be other problems, but you should start by fixing these two.