diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml new file mode 100644 index 0000000..5e001b8 --- /dev/null +++ b/.github/workflows/flutter.yml @@ -0,0 +1,25 @@ +name: Flutter CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Flutter + uses: subosito/flutter-action@v1 + with: + flutter-version: '3.x' + - name: Install dependencies + run: flutter pub get + - name: Run tests + run: flutter test + - name: Build the app for release + run: flutter build apk --release diff --git a/create_invoice_screen.dart b/create_invoice_screen.dart new file mode 100644 index 0000000..75e4738 --- /dev/null +++ b/create_invoice_screen.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'customer_provider.dart'; +import 'product_provider.dart'; +import 'invoice_provider.dart'; + +class CreateInvoiceScreen extends StatefulWidget { + @override + _CreateInvoiceScreenState createState() => _CreateInvoiceScreenState(); +} + +class _CreateInvoiceScreenState extends State { + final TextEditingController _quantityController = TextEditingController(); + + int? _selectedCustomerId; + int? _selectedProductId; + + @override + Widget build(BuildContext context) { + final customerProvider = Provider.of(context); + final productProvider = Provider.of(context); + final invoiceProvider = Provider.of(context); + + return Scaffold( + appBar: AppBar( + title: Text('請求書作成'), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + DropdownButtonFormField( + value: _selectedCustomerId, + items: customerProvider.customers.map((customer) { + return DropdownMenuItem( + value: customer.id, + child: Text(customer.name), + ); + }).toList(), + onChanged: (value) { + setState(() { + _selectedCustomerId = value; + }); + }, + decoration: InputDecoration(labelText: '顧客選択'), + ), + SizedBox(height: 20), + DropdownButtonFormField( + value: _selectedProductId, + items: productProvider.products.map((product) { + return DropdownMenuItem( + value: product.id, + child: Text(product.name), + ); + }).toList(), + onChanged: (value) { + setState(() { + _selectedProductId = value; + }); + }, + decoration: InputDecoration(labelText: '商品選択'), + ), + SizedBox(height: 20), + TextField( + controller: _quantityController, + keyboardType: TextInputType.number, + decoration: InputDecoration(labelText: '数量'), + ), + SizedBox(height: 20), + ElevatedButton( + onPressed: () async { + if (_selectedCustomerId != null && + _selectedProductId != null && + _quantityController.text.isNotEmpty) { + final quantity = int.parse(_quantityController.text); + final product = productProvider.products.firstWhere((p) => p.id == _selectedProductId); + + final invoiceItem = InvoiceItem( + invoiceId: 0, // 後で生成される ID + productId: _selectedProductId!, + quantity: quantity, + unitPrice: product.unitPrice, + discount: product.discount, + ); + + final total = (product.unitPrice * quantity) - (product.unitPrice * quantity * product.discount); + final tax = total * 0.1; // 簡易的な税率 + final discountTotal = product.unitPrice * quantity * product.discount; + + final invoice = Invoice( + customerId: _selectedCustomerId!, + date: DateTime.now().toIso8601String(), + total: total, + tax: tax, + discountTotal: discountTotal, + ); + + await invoiceProvider.addInvoice(invoice); + // 追加の処理(PDF生成など) + + Navigator.pop(context); + } + }, + child: Text('PDF生成'), + ), + ], + ), + ), + ); + } +} diff --git a/customer.dart b/customer.dart new file mode 100644 index 0000000..62276d7 --- /dev/null +++ b/customer.dart @@ -0,0 +1,35 @@ +class Customer { + final int? id; + final String name; + final String phone; + final String address; + final String email; + + Customer({ + this.id, + required this.name, + required this.phone, + required this.address, + required this.email, + }); + + Map toMap() { + return { + 'id': id, + 'name': name, + 'phone': phone, + 'address': address, + 'email': email, + }; + } + + factory Customer.fromMap(Map map) { + return Customer( + id: map['id'] as int?, + name: map['name'] as String, + phone: map['phone'] as String, + address: map['address'] as String, + email: map['email'] as String, + ); + } +} diff --git a/customer_list_screen.dart b/customer_list_screen.dart new file mode 100644 index 0000000..c635a0e --- /dev/null +++ b/customer_list_screen.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'customer_provider.dart'; + +class CustomerListScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + final customerProvider = Provider.of(context); + + return Scaffold( + appBar: AppBar( + title: Text('顧客一覧'), + ), + body: FutureBuilder( + future: customerProvider.fetchCustomers(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } else { + return ListView.builder( + itemCount: customerProvider.customers.length, + itemBuilder: (context, index) { + final customer = customerProvider.customers[index]; + return ListTile( + title: Text(customer.name), + subtitle: Text(customer.phone), + ); + }, + ); + } + }, + ), + ); + } +} diff --git a/customer_provider.dart b/customer_provider.dart new file mode 100644 index 0000000..ef53920 --- /dev/null +++ b/customer_provider.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:sqflite/sqflite.dart'; +import 'db_helper.dart'; +import 'customer.dart'; + +class CustomerProvider with ChangeNotifier { + List _customers = []; + + List get customers => _customers; + + Future fetchCustomers() async { + final db = await DbHelper().database; + final List> maps = await db.query('customers'); + _customers = List.generate(maps.length, (i) { + return Customer.fromMap(maps[i]); + }); + notifyListeners(); + } + + Future addCustomer(Customer customer) async { + final db = await DbHelper().database; + await db.insert('customers', customer.toMap()); + fetchCustomers(); + } + + // 追加の CRUD メソッドを実装 +} diff --git a/customer_test.dart b/customer_test.dart new file mode 100644 index 0000000..2d9e695 --- /dev/null +++ b/customer_test.dart @@ -0,0 +1,41 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:path_provider/path_provider.dart'; +import 'db_helper.dart'; +import 'customer.dart'; + +void main() { + group('Customer Tests', () { + late Database db; + + setUp(() async { + final io.Directory documentsDirectory = await getApplicationDocumentsDirectory(); + String path = '${documentsDirectory.path}/test.db'; + db = await openDatabase(path, version: 1, onCreate: DbHelper()._onCreate); + }); + + tearDown(() async { + await db.close(); + final io.Directory documentsDirectory = await getApplicationDocumentsDirectory(); + String path = '${documentsDirectory.path}/test.db'; + await io.File(path).delete(); + }); + + test('Add and fetch customer', () async { + final customer = Customer( + name: 'Test Customer', + phone: '1234567890', + address: 'Test Address', + email: 'test@example.com', + ); + + await db.insert('customers', customer.toMap()); + + final List> maps = await db.query('customers'); + expect(maps.length, 1); + + final fetchedCustomer = Customer.fromMap(maps.first); + expect(fetchedCustomer.name, 'Test Customer'); + }); + }); +} diff --git a/db_helper.dart b/db_helper.dart new file mode 100644 index 0000000..2daa9b7 --- /dev/null +++ b/db_helper.dart @@ -0,0 +1,63 @@ +import 'package:sqflite/sqflite.dart'; +import 'package:path_provider/path_provider.dart'; +import 'dart:io' as io; + +class DbHelper { + static Database? _database; + + Future get database async { + if (_database != null) return _database!; + _database = await initDb(); + return _database!; + } + + initDb() async { + io.Directory documentsDirectory = await getApplicationDocumentsDirectory(); + String path = '${documentsDirectory.path}/invoice.db'; + var db = await openDatabase(path, version: 1, onCreate: _onCreate); + return db; + } + + void _onCreate(Database db, int version) async { + await db.execute(''' + CREATE TABLE customers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, + phone TEXT, + address TEXT, + email TEXT + ) + '''); + + await db.execute(''' + CREATE TABLE products ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, + unit_price REAL, + discount REAL + ) + '''); + + await db.execute(''' + CREATE TABLE invoices ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + customer_id INTEGER, + date TEXT, + total REAL, + tax REAL, + discount_total REAL + ) + '''); + + await db.execute(''' + CREATE TABLE invoice_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + invoice_id INTEGER, + product_id INTEGER, + quantity INTEGER, + unit_price REAL, + discount REAL + ) + '''); + } +} diff --git a/invoice.dart b/invoice.dart new file mode 100644 index 0000000..82e15a1 --- /dev/null +++ b/invoice.dart @@ -0,0 +1,39 @@ +class Invoice { + final int? id; + final int customerId; + final String date; + final double total; + final double tax; + final double discountTotal; + + Invoice({ + this.id, + required this.customerId, + required this.date, + required this.total, + required this.tax, + required this.discountTotal, + }); + + Map toMap() { + return { + 'id': id, + 'customer_id': customerId, + 'date': date, + 'total': total, + 'tax': tax, + 'discount_total': discountTotal, + }; + } + + factory Invoice.fromMap(Map map) { + return Invoice( + id: map['id'] as int?, + customerId: map['customer_id'] as int, + date: map['date'] as String, + total: map['total'] as double, + tax: map['tax'] as double, + discountTotal: map['discount_total'] as double, + ); + } +} diff --git a/invoice_item.dart b/invoice_item.dart new file mode 100644 index 0000000..1cc019f --- /dev/null +++ b/invoice_item.dart @@ -0,0 +1,39 @@ +class InvoiceItem { + final int? id; + final int invoiceId; + final int productId; + final int quantity; + final double unitPrice; + final double discount; + + InvoiceItem({ + this.id, + required this.invoiceId, + required this.productId, + required this.quantity, + required this.unitPrice, + required this.discount, + }); + + Map toMap() { + return { + 'id': id, + 'invoice_id': invoiceId, + 'product_id': productId, + 'quantity': quantity, + 'unit_price': unitPrice, + 'discount': discount, + }; + } + + factory InvoiceItem.fromMap(Map map) { + return InvoiceItem( + id: map['id'] as int?, + invoiceId: map['invoice_id'] as int, + productId: map['product_id'] as int, + quantity: map['quantity'] as int, + unitPrice: map['unit_price'] as double, + discount: map['discount'] as double, + ); + } +} diff --git a/invoice_provider.dart b/invoice_provider.dart new file mode 100644 index 0000000..9e4eb80 --- /dev/null +++ b/invoice_provider.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:sqflite/sqflite.dart'; +import 'db_helper.dart'; +import 'invoice.dart'; + +class InvoiceProvider with ChangeNotifier { + List _invoices = []; + + List get invoices => _invoices; + + Future fetchInvoices() async { + final db = await DbHelper().database; + final List> maps = await db.query('invoices'); + _invoices = List.generate(maps.length, (i) { + return Invoice.fromMap(maps[i]); + }); + notifyListeners(); + } + + Future addInvoice(Invoice invoice) async { + final db = await DbHelper().database; + await db.insert('invoices', invoice.toMap()); + fetchInvoices(); + } + + // 追加の CRUD メソッドを実装 +} diff --git a/invoice_test.dart b/invoice_test.dart new file mode 100644 index 0000000..53d7010 --- /dev/null +++ b/invoice_test.dart @@ -0,0 +1,42 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:path_provider/path_provider.dart'; +import 'db_helper.dart'; +import 'invoice.dart'; + +void main() { + group('Invoice Tests', () { + late Database db; + + setUp(() async { + final io.Directory documentsDirectory = await getApplicationDocumentsDirectory(); + String path = '${documentsDirectory.path}/test.db'; + db = await openDatabase(path, version: 1, onCreate: DbHelper()._onCreate); + }); + + tearDown(() async { + await db.close(); + final io.Directory documentsDirectory = await getApplicationDocumentsDirectory(); + String path = '${documentsDirectory.path}/test.db'; + await io.File(path).delete(); + }); + + test('Add and fetch invoice', () async { + final invoice = Invoice( + customerId: 1, + date: DateTime.now().toIso8601String(), + total: 90.0, + tax: 9.0, + discountTotal: 10.0, + ); + + await db.insert('invoices', invoice.toMap()); + + final List> maps = await db.query('invoices'); + expect(maps.length, 1); + + final fetchedInvoice = Invoice.fromMap(maps.first); + expect(fetchedInvoice.customerId, 1); + }); + }); +} diff --git a/main.dart b/main.dart new file mode 100644 index 0000000..e129bf4 --- /dev/null +++ b/main.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'customer_provider.dart'; +import 'product_provider.dart'; +import 'invoice_provider.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => CustomerProvider()), + ChangeNotifierProvider(create: (_) => ProductProvider()), + ChangeNotifierProvider(create: (_) => InvoiceProvider()), + ], + child: MaterialApp( + title: 'Invoice App', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: HomeScreen(), + ), + ); + } +} + +class HomeScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Invoice App'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + Navigator.pushNamed(context, '/create_invoice'); + }, + child: Text('請求書作成'), + ), + SizedBox(height: 20), + ElevatedButton( + onPressed: () { + Navigator.pushNamed(context, '/customer_list'); + }, + child: Text('顧客一覧'), + ), + SizedBox(height: 20), + ElevatedButton( + onPressed: () { + Navigator.pushNamed(context, '/product_list'); + }, + child: Text('商品一覧'), + ), + ], + ), + ), + ); + } +} diff --git a/path/to/filename.js b/path/to/filename.js new file mode 100644 index 0000000..7435937 --- /dev/null +++ b/path/to/filename.js @@ -0,0 +1,2 @@ +// entire file content ... +// ... goes in between diff --git a/pdf_generator.dart b/pdf_generator.dart new file mode 100644 index 0000000..54ea4af --- /dev/null +++ b/pdf_generator.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:pdf/pdf.dart'; +import 'package:printing/printing.dart'; + +class PdfGenerator { + static Future generatePdf(Invoice invoice, List items) async { + final pdf = pw.Document(); + + pdf.addPage( + pw.Page( + build: (pw.Context context) => pw.Column( + children: [ + pw.Text('請求書'), + pw.SizedBox(height: 20), + pw.Text('顧客名: ${invoice.customerId}'), // 後で顧客名に変更 + pw.Text('日付: ${invoice.date}'), + pw.SizedBox(height: 20), + pw.Table( + border: pw.TableBorder.all(), + children: [ + pw.TableRow(children: [ + pw.Text('商品名'), + pw.Text('数量'), + pw.Text('単価'), + pw.Text('値引き'), + pw.Text('小計'), + ]), + for (var item in items) + pw.TableRow(children: [ + pw.Text(item.productId.toString()), // 後で商品名に変更 + pw.Text(item.quantity.toString()), + pw.Text(item.unitPrice.toStringAsFixed(2)), + pw.Text((item.discount * 100).toStringAsFixed(2) + '%'), + pw.Text((item.unitPrice * item.quantity - (item.unitPrice * item.quantity * item.discount)).toStringAsFixed(2)), + ]), + ], + ), + pw.SizedBox(height: 20), + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Text('合計'), + pw.Text(invoice.total.toStringAsFixed(2)), + ], + ), + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Text('税額'), + pw.Text(invoice.tax.toStringAsFixed(2)), + ], + ), + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Text('値引き合計'), + pw.Text(invoice.discountTotal.toStringAsFixed(2)), + ], + ), + ], + ), + ), + ); + + await Printing.sharePdf(bytes: pdf.save(), filename: 'invoice.pdf'); + } +} diff --git a/product.dart b/product.dart new file mode 100644 index 0000000..88eef1e --- /dev/null +++ b/product.dart @@ -0,0 +1,31 @@ +class Product { + final int? id; + final String name; + final double unitPrice; + final double discount; + + Product({ + this.id, + required this.name, + required this.unitPrice, + required this.discount, + }); + + Map toMap() { + return { + 'id': id, + 'name': name, + 'unit_price': unitPrice, + 'discount': discount, + }; + } + + factory Product.fromMap(Map map) { + return Product( + id: map['id'] as int?, + name: map['name'] as String, + unitPrice: map['unit_price'] as double, + discount: map['discount'] as double, + ); + } +} diff --git a/product_list_screen.dart b/product_list_screen.dart new file mode 100644 index 0000000..12e53d0 --- /dev/null +++ b/product_list_screen.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'product_provider.dart'; + +class ProductListScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + final productProvider = Provider.of(context); + + return Scaffold( + appBar: AppBar( + title: Text('商品一覧'), + ), + body: FutureBuilder( + future: productProvider.fetchProducts(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } else { + return ListView.builder( + itemCount: productProvider.products.length, + itemBuilder: (context, index) { + final product = productProvider.products[index]; + return ListTile( + title: Text(product.name), + subtitle: Text('単価: ${product.unitPrice}, 値引き: ${product.discount}'), + ); + }, + ); + } + }, + ), + ); + } +} diff --git a/product_provider.dart b/product_provider.dart new file mode 100644 index 0000000..d727ae2 --- /dev/null +++ b/product_provider.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:sqflite/sqflite.dart'; +import 'db_helper.dart'; +import 'product.dart'; + +class ProductProvider with ChangeNotifier { + List _products = []; + + List get products => _products; + + Future fetchProducts() async { + final db = await DbHelper().database; + final List> maps = await db.query('products'); + _products = List.generate(maps.length, (i) { + return Product.fromMap(maps[i]); + }); + notifyListeners(); + } + + Future addProduct(Product product) async { + final db = await DbHelper().database; + await db.insert('products', product.toMap()); + fetchProducts(); + } + + // 追加の CRUD メソッドを実装 +} diff --git a/product_test.dart b/product_test.dart new file mode 100644 index 0000000..dac90ca --- /dev/null +++ b/product_test.dart @@ -0,0 +1,40 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:path_provider/path_provider.dart'; +import 'db_helper.dart'; +import 'product.dart'; + +void main() { + group('Product Tests', () { + late Database db; + + setUp(() async { + final io.Directory documentsDirectory = await getApplicationDocumentsDirectory(); + String path = '${documentsDirectory.path}/test.db'; + db = await openDatabase(path, version: 1, onCreate: DbHelper()._onCreate); + }); + + tearDown(() async { + await db.close(); + final io.Directory documentsDirectory = await getApplicationDocumentsDirectory(); + String path = '${documentsDirectory.path}/test.db'; + await io.File(path).delete(); + }); + + test('Add and fetch product', () async { + final product = Product( + name: 'Test Product', + unitPrice: 100.0, + discount: 0.1, + ); + + await db.insert('products', product.toMap()); + + final List> maps = await db.query('products'); + expect(maps.length, 1); + + final fetchedProduct = Product.fromMap(maps.first); + expect(fetchedProduct.name, 'Test Product'); + }); + }); +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..25b8186 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,22 @@ +name: invoice_app +description: A new Flutter project. + +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +version: 1.0.0+1 + +environment: + sdk: ">=3.0.0 <4.0.0" + +dependencies: + flutter: + sdk: flutter + sqflite: ^2.0.0+3 + path_provider: ^2.0.11 + pdf: ^3.6.0 + printing: ^5.9.3 + provider: ^6.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter