feat: add lock flags and refresh theme
This commit is contained in:
parent
5426751978
commit
145f0d7cad
9 changed files with 217 additions and 164 deletions
|
|
@ -22,9 +22,43 @@ class MyApp extends StatelessWidget {
|
|||
return MaterialApp(
|
||||
title: '販売アシスト1号',
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blueGrey,
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo.shade700).copyWith(
|
||||
primary: Colors.indigo.shade700,
|
||||
secondary: Colors.deepOrange.shade400,
|
||||
surface: Colors.grey.shade50,
|
||||
onSurface: Colors.blueGrey.shade900,
|
||||
),
|
||||
scaffoldBackgroundColor: Colors.grey.shade50,
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: Colors.indigo.shade700,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
textStyle: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
side: BorderSide(color: Colors.indigo.shade700),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
textStyle: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.indigo.shade700, width: 1.4),
|
||||
),
|
||||
),
|
||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||
useMaterial3: true,
|
||||
fontFamily: 'IPAexGothic',
|
||||
),
|
||||
home: const InvoiceHistoryScreen(),
|
||||
);
|
||||
|
|
@ -54,54 +88,20 @@ class _InvoiceFlowScreenState extends State<InvoiceFlowScreen> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: const BackButton(),
|
||||
title: const Text("販売アシスト1号 V1.5.02"),
|
||||
backgroundColor: Colors.blueGrey,
|
||||
),
|
||||
drawer: Drawer(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
const DrawerHeader(
|
||||
decoration: BoxDecoration(color: Colors.blueGrey),
|
||||
child: Text("メニュー", style: TextStyle(color: Colors.white, fontSize: 24)),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.add_task),
|
||||
title: const Text("新規伝票作成"),
|
||||
onTap: () => Navigator.pop(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.history),
|
||||
title: const Text("伝票履歴"),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const InvoiceHistoryScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 入力フォームを表示
|
||||
body: InvoiceInputForm(
|
||||
onInvoiceGenerated: (invoice, path) async {
|
||||
// GPSの記録を試みる
|
||||
final locationService = LocationService();
|
||||
final position = await locationService.getCurrentLocation();
|
||||
if (position != null) {
|
||||
final customerRepo = CustomerRepository();
|
||||
await customerRepo.addGpsHistory(invoice.customer.id, position.latitude, position.longitude);
|
||||
debugPrint("GPS recorded for customer ${invoice.customer.id}");
|
||||
}
|
||||
_handleInvoiceGenerated(invoice, path);
|
||||
if (widget.onComplete != null) widget.onComplete!();
|
||||
},
|
||||
),
|
||||
// 入力フォーム自身が Scaffold を持つため、ここではそのまま返す
|
||||
return InvoiceInputForm(
|
||||
onInvoiceGenerated: (invoice, path) async {
|
||||
// GPSの記録を試みる
|
||||
final locationService = LocationService();
|
||||
final position = await locationService.getCurrentLocation();
|
||||
if (position != null) {
|
||||
final customerRepo = CustomerRepository();
|
||||
await customerRepo.addGpsHistory(invoice.customer.id, position.latitude, position.longitude);
|
||||
debugPrint("GPS recorded for customer ${invoice.customer.id}");
|
||||
}
|
||||
_handleInvoiceGenerated(invoice, path);
|
||||
if (widget.onComplete != null) widget.onComplete!();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class Customer {
|
|||
final String? odooId; // Odoo側のID
|
||||
final bool isSynced; // 同期フラグ
|
||||
final DateTime updatedAt; // 最終更新日時
|
||||
final bool isLocked; // ロック
|
||||
|
||||
Customer({
|
||||
required this.id,
|
||||
|
|
@ -22,6 +23,7 @@ class Customer {
|
|||
this.odooId,
|
||||
this.isSynced = false,
|
||||
DateTime? updatedAt,
|
||||
this.isLocked = false,
|
||||
}) : updatedAt = updatedAt ?? DateTime.now();
|
||||
|
||||
String get invoiceName {
|
||||
|
|
@ -42,6 +44,7 @@ class Customer {
|
|||
'address': address,
|
||||
'tel': tel,
|
||||
'odoo_id': odooId,
|
||||
'is_locked': isLocked ? 1 : 0,
|
||||
'is_synced': isSynced ? 1 : 0,
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
};
|
||||
|
|
@ -57,6 +60,7 @@ class Customer {
|
|||
address: map['address'],
|
||||
tel: map['tel'],
|
||||
odooId: map['odoo_id'],
|
||||
isLocked: (map['is_locked'] ?? 0) == 1,
|
||||
isSynced: map['is_synced'] == 1,
|
||||
updatedAt: DateTime.parse(map['updated_at']),
|
||||
);
|
||||
|
|
@ -73,6 +77,7 @@ class Customer {
|
|||
String? odooId,
|
||||
bool? isSynced,
|
||||
DateTime? updatedAt,
|
||||
bool? isLocked,
|
||||
}) {
|
||||
return Customer(
|
||||
id: id ?? this.id,
|
||||
|
|
@ -85,6 +90,7 @@ class Customer {
|
|||
odooId: odooId ?? this.odooId,
|
||||
isSynced: isSynced ?? this.isSynced,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
isLocked: isLocked ?? this.isLocked,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ class Invoice {
|
|||
final String terminalId; // 追加: 端末識別子
|
||||
final bool isDraft; // 追加: 下書きフラグ
|
||||
final String? subject; // 追加: 案件名
|
||||
final bool isLocked; // 追加: ロック
|
||||
|
||||
Invoice({
|
||||
String? id,
|
||||
|
|
@ -102,6 +103,7 @@ class Invoice {
|
|||
String? terminalId, // 追加
|
||||
this.isDraft = false, // 追加: デフォルトは通常
|
||||
this.subject, // 追加: 案件
|
||||
this.isLocked = false,
|
||||
}) : id = id ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
terminalId = terminalId ?? "T1", // デフォルト端末ID
|
||||
updatedAt = updatedAt ?? DateTime.now();
|
||||
|
|
@ -160,6 +162,7 @@ class Invoice {
|
|||
'content_hash': contentHash, // 追加
|
||||
'is_draft': isDraft ? 1 : 0, // 追加
|
||||
'subject': subject, // 追加
|
||||
'is_locked': isLocked ? 1 : 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -181,6 +184,7 @@ class Invoice {
|
|||
String? terminalId,
|
||||
bool? isDraft,
|
||||
String? subject,
|
||||
bool? isLocked,
|
||||
}) {
|
||||
return Invoice(
|
||||
id: id ?? this.id,
|
||||
|
|
@ -200,6 +204,7 @@ class Invoice {
|
|||
terminalId: terminalId ?? this.terminalId,
|
||||
isDraft: isDraft ?? this.isDraft,
|
||||
subject: subject ?? this.subject,
|
||||
isLocked: isLocked ?? this.isLocked,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ class Product {
|
|||
final String? category;
|
||||
final int stockQuantity; // 追加
|
||||
final String? odooId;
|
||||
final bool isLocked; // ロック
|
||||
|
||||
Product({
|
||||
required this.id,
|
||||
|
|
@ -15,6 +16,7 @@ class Product {
|
|||
this.category,
|
||||
this.stockQuantity = 0, // 追加
|
||||
this.odooId,
|
||||
this.isLocked = false,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
|
|
@ -25,6 +27,7 @@ class Product {
|
|||
'barcode': barcode,
|
||||
'category': category,
|
||||
'stock_quantity': stockQuantity, // 追加
|
||||
'is_locked': isLocked ? 1 : 0,
|
||||
'odoo_id': odooId,
|
||||
};
|
||||
}
|
||||
|
|
@ -37,6 +40,7 @@ class Product {
|
|||
barcode: map['barcode'],
|
||||
category: map['category'],
|
||||
stockQuantity: map['stock_quantity'] ?? 0, // 追加
|
||||
isLocked: (map['is_locked'] ?? 0) == 1,
|
||||
odooId: map['odoo_id'],
|
||||
);
|
||||
}
|
||||
|
|
@ -47,12 +51,14 @@ class Product {
|
|||
int? defaultUnitPrice,
|
||||
String? barcode,
|
||||
String? odooId,
|
||||
bool? isLocked,
|
||||
}) {
|
||||
return Product(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
defaultUnitPrice: defaultUnitPrice ?? this.defaultUnitPrice,
|
||||
odooId: odooId ?? this.odooId,
|
||||
isLocked: isLocked ?? this.isLocked,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -327,6 +327,7 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
|||
label: const Text("新規伝票作成"),
|
||||
icon: const Icon(Icons.add),
|
||||
backgroundColor: Colors.indigo,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,24 +129,31 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
);
|
||||
|
||||
setState(() => _isSaving = true);
|
||||
|
||||
// PDF生成有無に関わらず、まずは保存
|
||||
if (generatePdf) {
|
||||
setState(() => _status = "PDFを生成中...");
|
||||
final path = await generateInvoicePdf(invoice);
|
||||
if (path != null) {
|
||||
final updatedInvoice = invoice.copyWith(filePath: path);
|
||||
await _repository.saveInvoice(updatedInvoice);
|
||||
if (mounted) widget.onInvoiceGenerated(updatedInvoice, path);
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("伝票を保存し、PDFを生成しました")));
|
||||
try {
|
||||
// PDF生成有無に関わらず、まずは保存
|
||||
if (generatePdf) {
|
||||
setState(() => _status = "PDFを生成中...");
|
||||
final path = await generateInvoicePdf(invoice);
|
||||
if (path != null) {
|
||||
final updatedInvoice = invoice.copyWith(filePath: path);
|
||||
await _repository.saveInvoice(updatedInvoice);
|
||||
if (mounted) widget.onInvoiceGenerated(updatedInvoice, path);
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("伝票を保存し、PDFを生成しました")));
|
||||
} else {
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("PDF生成に失敗しました")));
|
||||
}
|
||||
} else {
|
||||
await _repository.saveInvoice(invoice);
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("伝票を保存しました(PDF未生成)")));
|
||||
if (mounted) Navigator.pop(context);
|
||||
}
|
||||
} else {
|
||||
await _repository.saveInvoice(invoice);
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("伝票を保存しました(PDF未生成)")));
|
||||
if (mounted) Navigator.pop(context);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('保存に失敗しました: $e')));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isSaving = false);
|
||||
}
|
||||
|
||||
if (mounted) setState(() => _isSaving = false);
|
||||
}
|
||||
|
||||
void _showPreview() {
|
||||
|
|
@ -202,7 +209,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
backgroundColor: themeColor,
|
||||
appBar: AppBar(
|
||||
leading: const BackButton(),
|
||||
title: Text(_isDraft ? "伝票作成 (下書き)" : "販売アシスト1号 V1.5.05"),
|
||||
title: Text(_isDraft ? "伝票作成 (下書き)" : "販売アシスト1号 V1.5.06"),
|
||||
backgroundColor: _isDraft ? Colors.black87 : Colors.blueGrey,
|
||||
),
|
||||
body: Stack(
|
||||
|
|
@ -374,102 +381,98 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
child: Center(child: Text("商品が追加されていません", style: TextStyle(color: Colors.grey))),
|
||||
)
|
||||
else
|
||||
..._items.asMap().entries.map((entry) {
|
||||
final idx = entry.key;
|
||||
final item = entry.value;
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: ListTile(
|
||||
title: Text(item.description),
|
||||
subtitle: Text("¥${fmt.format(item.unitPrice)} x ${item.quantity}"),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("¥${fmt.format(item.unitPrice * item.quantity)}", style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(width: 8),
|
||||
if (idx > 0)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_upward, size: 20),
|
||||
onPressed: () => setState(() {
|
||||
final temp = _items[idx];
|
||||
_items[idx] = _items[idx - 1];
|
||||
_items[idx - 1] = temp;
|
||||
}),
|
||||
tooltip: "上へ",
|
||||
),
|
||||
if (idx < _items.length - 1)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_downward, size: 20),
|
||||
onPressed: () => setState(() {
|
||||
final temp = _items[idx];
|
||||
_items[idx] = _items[idx + 1];
|
||||
_items[idx + 1] = temp;
|
||||
}),
|
||||
tooltip: "下へ",
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove_circle_outline, color: Colors.redAccent),
|
||||
onPressed: () => setState(() => _items.removeAt(idx)),
|
||||
tooltip: "削除",
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
// 簡易編集ダイアログ
|
||||
final descCtrl = TextEditingController(text: item.description);
|
||||
final qtyCtrl = TextEditingController(text: item.quantity.toString());
|
||||
final priceCtrl = TextEditingController(text: item.unitPrice.toString());
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text("明細の編集"),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(controller: descCtrl, decoration: const InputDecoration(labelText: "品名 / 項目")),
|
||||
TextField(controller: qtyCtrl, decoration: const InputDecoration(labelText: "数量"), keyboardType: TextInputType.number),
|
||||
TextField(controller: priceCtrl, decoration: const InputDecoration(labelText: "単価"), keyboardType: TextInputType.number),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.search, size: 18),
|
||||
label: const Text("マスター参照"),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => ProductPickerModal(
|
||||
onItemSelected: (selected) {
|
||||
descCtrl.text = selected.description;
|
||||
priceCtrl.text = selected.unitPrice.toString();
|
||||
Navigator.pop(context); // close picker
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_items[idx] = item.copyWith(
|
||||
description: descCtrl.text,
|
||||
quantity: int.tryParse(qtyCtrl.text) ?? item.quantity,
|
||||
unitPrice: int.tryParse(priceCtrl.text) ?? item.unitPrice,
|
||||
);
|
||||
});
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text("更新"),
|
||||
ReorderableListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: _items.length,
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
setState(() {
|
||||
if (newIndex > oldIndex) newIndex -= 1;
|
||||
final item = _items.removeAt(oldIndex);
|
||||
_items.insert(newIndex, item);
|
||||
});
|
||||
},
|
||||
buildDefaultDragHandles: false,
|
||||
itemBuilder: (context, idx) {
|
||||
final item = _items[idx];
|
||||
return ReorderableDelayedDragStartListener(
|
||||
key: ValueKey('item_${idx}_${item.description}'),
|
||||
index: idx,
|
||||
child: Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: ListTile(
|
||||
title: Text(item.description),
|
||||
subtitle: Text("¥${fmt.format(item.unitPrice)} x ${item.quantity}"),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("¥${fmt.format(item.unitPrice * item.quantity)}", style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove_circle_outline, color: Colors.redAccent),
|
||||
onPressed: () => setState(() => _items.removeAt(idx)),
|
||||
tooltip: "削除",
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
onTap: () {
|
||||
// 簡易編集ダイアログ
|
||||
final descCtrl = TextEditingController(text: item.description);
|
||||
final qtyCtrl = TextEditingController(text: item.quantity.toString());
|
||||
final priceCtrl = TextEditingController(text: item.unitPrice.toString());
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text("明細の編集"),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(controller: descCtrl, decoration: const InputDecoration(labelText: "品名 / 項目")),
|
||||
TextField(controller: qtyCtrl, decoration: const InputDecoration(labelText: "数量"), keyboardType: TextInputType.number),
|
||||
TextField(controller: priceCtrl, decoration: const InputDecoration(labelText: "単価"), keyboardType: TextInputType.number),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.search, size: 18),
|
||||
label: const Text("マスター参照"),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => ProductPickerModal(
|
||||
onItemSelected: (selected) {
|
||||
descCtrl.text = selected.description;
|
||||
priceCtrl.text = selected.unitPrice.toString();
|
||||
Navigator.pop(context); // close picker
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_items[idx] = item.copyWith(
|
||||
description: descCtrl.text,
|
||||
quantity: int.tryParse(qtyCtrl.text) ?? item.quantity,
|
||||
unitPrice: int.tryParse(priceCtrl.text) ?? item.unitPrice,
|
||||
);
|
||||
});
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text("更新"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import 'package:sqflite/sqflite.dart';
|
|||
import 'package:path/path.dart';
|
||||
|
||||
class DatabaseHelper {
|
||||
static const _databaseVersion = 13;
|
||||
static const _databaseVersion = 15;
|
||||
static final DatabaseHelper _instance = DatabaseHelper._internal();
|
||||
static Database? _database;
|
||||
|
||||
|
|
@ -97,6 +97,14 @@ class DatabaseHelper {
|
|||
if (oldVersion < 13) {
|
||||
await db.execute('ALTER TABLE company_info ADD COLUMN registration_number TEXT');
|
||||
}
|
||||
if (oldVersion < 14) {
|
||||
await _safeAddColumn(db, 'invoices', 'subject TEXT');
|
||||
}
|
||||
if (oldVersion < 15) {
|
||||
await _safeAddColumn(db, 'invoices', 'is_locked INTEGER DEFAULT 0');
|
||||
await _safeAddColumn(db, 'customers', 'is_locked INTEGER DEFAULT 0');
|
||||
await _safeAddColumn(db, 'products', 'is_locked INTEGER DEFAULT 0');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onCreate(Database db, int version) async {
|
||||
|
|
@ -110,6 +118,7 @@ class DatabaseHelper {
|
|||
address TEXT,
|
||||
tel TEXT,
|
||||
odoo_id TEXT,
|
||||
is_locked INTEGER DEFAULT 0,
|
||||
is_synced INTEGER DEFAULT 0,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
|
|
@ -135,6 +144,7 @@ class DatabaseHelper {
|
|||
barcode TEXT,
|
||||
category TEXT,
|
||||
stock_quantity INTEGER DEFAULT 0,
|
||||
is_locked INTEGER DEFAULT 0,
|
||||
odoo_id TEXT
|
||||
)
|
||||
''');
|
||||
|
|
@ -148,6 +158,7 @@ class DatabaseHelper {
|
|||
customer_id TEXT NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
notes TEXT,
|
||||
subject TEXT,
|
||||
file_path TEXT,
|
||||
total_amount INTEGER,
|
||||
tax_rate REAL DEFAULT 0.10,
|
||||
|
|
@ -161,6 +172,7 @@ class DatabaseHelper {
|
|||
terminal_id TEXT DEFAULT "T1",
|
||||
content_hash TEXT,
|
||||
is_draft INTEGER DEFAULT 0,
|
||||
is_locked INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (customer_id) REFERENCES customers (id)
|
||||
)
|
||||
''');
|
||||
|
|
@ -212,4 +224,12 @@ class DatabaseHelper {
|
|||
)
|
||||
''');
|
||||
}
|
||||
|
||||
Future<void> _safeAddColumn(Database db, String table, String columnDef) async {
|
||||
try {
|
||||
await db.execute('ALTER TABLE ' + table + ' ADD COLUMN ' + columnDef);
|
||||
} catch (_) {
|
||||
// Ignore if the column already exists.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ class InvoiceRepository {
|
|||
Future<void> saveInvoice(Invoice invoice) async {
|
||||
final db = await _dbHelper.database;
|
||||
|
||||
// 正式発行(下書きでない)場合はロックを掛ける
|
||||
final Invoice toSave = invoice.isDraft ? invoice : invoice.copyWith(isLocked: true);
|
||||
|
||||
await db.transaction((txn) async {
|
||||
// 在庫の調整(更新の場合、以前の数量を戻してから新しい数量を引く)
|
||||
final List<Map<String, dynamic>> oldItems = await txn.query(
|
||||
|
|
@ -34,7 +37,7 @@ class InvoiceRepository {
|
|||
// 伝票ヘッダーの保存
|
||||
await txn.insert(
|
||||
'invoices',
|
||||
invoice.toMap(),
|
||||
toSave.toMap(),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
|
||||
|
|
@ -53,8 +56,16 @@ class InvoiceRepository {
|
|||
'UPDATE products SET stock_quantity = stock_quantity - ? WHERE id = ?',
|
||||
[item.quantity, item.productId],
|
||||
);
|
||||
if (!invoice.isDraft) {
|
||||
await txn.execute('UPDATE products SET is_locked = 1 WHERE id = ?', [item.productId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 顧客をロック
|
||||
if (!invoice.isDraft) {
|
||||
await txn.execute('UPDATE customers SET is_locked = 1 WHERE id = ?', [invoice.customer.id]);
|
||||
}
|
||||
});
|
||||
|
||||
await _logRepo.logAction(
|
||||
|
|
@ -114,6 +125,7 @@ class InvoiceRepository {
|
|||
terminalId: iMap['terminal_id'] ?? "T1",
|
||||
isDraft: (iMap['is_draft'] ?? 0) == 1,
|
||||
subject: iMap['subject'],
|
||||
isLocked: (iMap['is_locked'] ?? 0) == 1,
|
||||
));
|
||||
}
|
||||
return invoices;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class SlideToUnlock extends StatefulWidget {
|
|||
|
||||
class _SlideToUnlockState extends State<SlideToUnlock> {
|
||||
double _position = 0.0;
|
||||
final double _thumbSize = 50.0;
|
||||
final double _thumbSize = 56.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -27,7 +27,7 @@ class _SlideToUnlockState extends State<SlideToUnlock> {
|
|||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final double maxWidth = constraints.maxWidth;
|
||||
final double trackWidth = maxWidth - _thumbSize - 8; // 余白を考慮
|
||||
final double trackWidth = (maxWidth - _thumbSize - 12).clamp(0, maxWidth);
|
||||
|
||||
return Container(
|
||||
height: 64,
|
||||
|
|
@ -81,7 +81,7 @@ class _SlideToUnlockState extends State<SlideToUnlock> {
|
|||
}
|
||||
},
|
||||
child: Container(
|
||||
width: maxWidth * 0.25, // 少し横長に
|
||||
width: _thumbSize,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
|
|
|
|||
Loading…
Reference in a new issue