feat(ui): add Android navigation shell, Web POS workstation, and bilingual Taglish/English localization

This commit is contained in:
ssbg04 committed 2026-10-20 17:50:35 +08:00
1 parent e8623577cd
commit 960b9a512e
10 files changed
+1308 -85

No files matched your search

+8 -2
View File
@@ -7,7 +7,10 @@
"type": "dart",
"deviceId": "all",
"cwd": "${workspaceFolder}/frontend",
"program": "lib/main.dart"
"program": "lib/main.dart",
"env": {
"GRADLE_USER_HOME": "F:\\.gradle"
}
},
{
"name": "TindaGrid: Android (ADB Device)",
@@ -15,7 +18,10 @@
"type": "dart",
"deviceId": "android",
"cwd": "${workspaceFolder}/frontend",
"program": "lib/main.dart"
"program": "lib/main.dart",
"env": {
"GRADLE_USER_HOME": "F:\\.gradle"
}
},
{
"name": "TindaGrid: Chrome (Web PWA)",
+5
View File
@@ -1,5 +1,10 @@
{
"version": "2.0.0",
"options": {
"env": {
"GRADLE_USER_HOME": "F:\\.gradle"
}
},
"tasks": [
{
"label": "Flutter Run: All Devices",
+2
View File
@@ -4,3 +4,5 @@ android.useAndroidX=true
android.newDsl=false
# This builtInKotlin flag was added by the Flutter template
android.builtInKotlin=false
kotlin.incremental=false
@@ -0,0 +1,124 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
enum AppLanguage {
taglish,
english,
}
class AppStrings {
final AppLanguage language;
const AppStrings(this.language);
bool get isTaglish => language == AppLanguage.taglish;
// Global & Status
String get appName => 'TindaGrid';
String get offlineSafe => isTaglish ? 'Ligtas (Offline)' : 'Safe (Offline)';
String get onlineSynced => isTaglish ? 'Naka-sync' : 'Cloud Synced';
String get switchLang => isTaglish ? 'English' : 'Taglish';
// Navigation
String get navPos => isTaglish ? 'POS' : 'POS';
String get navInventory => isTaglish ? 'Paninda' : 'Inventory';
String get navDebts => isTaglish ? 'Utang' : 'Debts';
String get navGcash => isTaglish ? 'GCash' : 'GCash';
String get navSettings => isTaglish ? 'Settings' : 'Settings';
// POS Screen
String get searchPlaceholder =>
isTaglish ? 'I-type ang paninda o barcode...' : 'Search product or barcode...';
String get scanButton => 'SCAN';
String get quickTingiTitle =>
isTaglish ? 'MABILISANG TINGI (Walang Barcode — Pindutin Agad):' : 'QUICK TINGI (No Barcode — Quick Tap):';
String get recentCartTitle =>
isTaglish ? 'HULING NAIDAGDAG SA KART:' : 'RECENTLY ADDED TO CART:';
String get cartLabel => isTaglish ? 'KART' : 'CART';
String get itemsCountLabel => isTaglish ? 'Bagay' : 'Items';
String get totalLabel => isTaglish ? 'KABUUAN' : 'TOTAL';
String get checkoutButton => isTaglish ? 'BAYARAN / MANINGIL' : 'COLLECT / PAY';
String get eachPrice => isTaglish ? 'bawat isa' : 'each';
String get liveTicketTitle =>
isTaglish ? 'KASALUKUYANG KART (Live Ticket)' : 'CURRENT CART (Live Ticket)';
String get emptyCartPrompt =>
isTaglish ? 'Walang laman ang kart. Pumili sa paninda.' : 'Cart is empty. Select products.';
String get estProfit => isTaglish ? 'Tinatayang Tubo (Kita):' : 'Estimated Profit:';
// Checkout Modal
String get checkoutModalTitle => isTaglish ? 'Bayaran at Sukli' : 'Payment & Change';
String get totalPayable => isTaglish ? 'KABUUANG BABAYARAN:' : 'TOTAL AMOUNT DUE:';
String get cashTendered =>
isTaglish ? 'PERANG INABOT NG MAMIMILI:' : 'CASH RECEIVED FROM CUSTOMER:';
String get exactBill => isTaglish ? 'Eksakto' : 'Exact';
String get customerChange =>
isTaglish ? '🟢 SUKLI SA MAMIMILI:' : '🟢 CHANGE DUE TO CUSTOMER:';
String get returnToCustomer => isTaglish ? 'Ibalik sa Tao' : 'Return to Customer';
String get insufficientCash =>
isTaglish ? '⚠️ KULANG PA NG PERA:' : '⚠️ INSUFFICIENT CASH:';
String get completeSale =>
isTaglish ? 'TAPOS NA ANG BENTA' : 'CONFIRM & COMPLETE SALE';
String get saleRecorded =>
isTaglish ? '✅ Benta naitala! Ligtas sa lokal database.' : '✅ Sale recorded! Saved to local database.';
// Inventory Screen
String get inventoryTitle => isTaglish ? 'Talaan ng mga Paninda' : 'Product Inventory';
String get totalProducts => isTaglish ? 'Kabuuang Paninda' : 'Total Products';
String get lowStockWarning => isTaglish ? 'Kulang sa Stock' : 'Low Stock Alert';
String get addProduct => isTaglish ? 'Magdagdag ng Paninda' : 'Add Product';
String get inStock => isTaglish ? 'piraso natitira' : 'items left';
String get costPrice => isTaglish ? 'Puhunan' : 'Cost';
String get sellingPrice => isTaglish ? 'Presyo' : 'Price';
// Debts Screen
String get debtsTitle => isTaglish ? 'Kwaderno ng Pautang' : 'Customer Debt Ledger';
String get totalCollectibles => isTaglish ? 'Kabuuang Sisingilin' : 'Total Collectibles';
String get activeDebtors => isTaglish ? 'Mga May Utang' : 'Active Borrowers';
String get recordNewDebt => isTaglish ? 'Maglista ng Bagong Utang' : 'Record New Debt';
String get dueDate => isTaglish ? 'Petsa ng Bayaran' : 'Due Date';
String get settleDebt => isTaglish ? 'Maningil / Magbayad' : 'Settle Payment';
// GCash Screen
String get gcashTitle => isTaglish ? 'GCash Float & Transaksyon' : 'GCash Float & Ledger';
String get ewalletFloat => isTaglish ? 'GCash E-Wallet Balance' : 'GCash E-Wallet Balance';
String get cashBoxDrawer => isTaglish ? 'Pera sa Kahon (Cash Box)' : 'Cash Drawer (Physical Cash)';
String get cashInAction => isTaglish ? 'Cash-In (Pasa Pera)' : 'Cash-In (Deposit/Send)';
String get cashOutAction => isTaglish ? 'Cash-Out (Pera Palit)' : 'Cash-Out (Withdraw)';
String get feeSchedule => isTaglish ? 'Talaan ng Patong (Fee Brackets)' : 'Fee Schedule';
// Settings Screen
String get settingsTitle => isTaglish ? 'Pag-aayos ng Tindahan' : 'Store Settings';
String get languageSection => isTaglish ? 'Piliin ang Wika' : 'Display Language';
String get cloudBackupTitle => isTaglish ? 'Cloud Backup & Sync' : 'Cloud Backup & Sync';
String get cloudBackupSubtitle => isTaglish
? 'Awtomatikong backup sa Neon Cloud tuwing may internet'
: 'Automatic backup to Neon Cloud when online';
String get syncNowButton => isTaglish ? 'I-sync Ngayon' : 'Sync Now';
String get securitySection => isTaglish ? 'Seguridad at PIN' : 'Security & Store PIN';
String get storeProfile => isTaglish ? 'Detalye ng Tindahan' : 'Store Profile';
String get aboutTindaGrid =>
isTaglish ? 'Tungkol sa TindaGrid' : 'About TindaGrid';
String get systemStatusSubtitle => isTaglish
? '100% Offline-First | Walang Bayad Kada Buwan (\$0/mo)'
: '100% Offline-First | \$0/mo Free Cloud Architecture';
}
class LanguageNotifier extends Notifier<AppLanguage> {
@override
AppLanguage build() => AppLanguage.taglish;
void toggleLanguage() {
state = state == AppLanguage.taglish ? AppLanguage.english : AppLanguage.taglish;
}
void setLanguage(AppLanguage language) {
state = language;
}
}
final languageProvider = NotifierProvider<LanguageNotifier, AppLanguage>(() {
return LanguageNotifier();
});
final stringsProvider = Provider<AppStrings>((ref) {
final lang = ref.watch(languageProvider);
return AppStrings(lang);
});
@@ -0,0 +1,201 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/constants/app_colors.dart';
import '../../core/localization/language_provider.dart';
class DebtsScreen extends ConsumerWidget {
const DebtsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final tr = ref.watch(stringsProvider);
final isLight = Theme.of(context).brightness == Brightness.light;
final sampleDebts = [
{'name': 'Aling Marites (Kapitbahay)', 'phone': '0917-123-4567', 'amount': 385.00, 'date': 'Oct 18, 2026', 'items': 'Bear Brand, Bigas 2kg, Itlog'},
{'name': 'Mang Boy Tricycle Driver', 'phone': '0928-888-9999', 'amount': 150.00, 'date': 'Oct 19, 2026', 'items': 'Marlboro Red, Cobra Energy'},
{'name': 'Kuya Jun (Tubero)', 'phone': '0939-555-1234', 'amount': 520.00, 'date': 'Oct 15, 2026', 'items': 'San Miguel Pale, Yosi, Chicharon'},
{'name': 'Ate Gina (Sari-Sari sukli)', 'phone': '0945-333-7777', 'amount': 45.00, 'date': 'Oct 20, 2026', 'items': 'Kopiko Blanca, SkyFlakes'},
];
final double totalCollectibles = sampleDebts.fold(0.0, (sum, d) => sum + (d['amount'] as double));
return Scaffold(
appBar: AppBar(
title: Text(
tr.debtsTitle,
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
actions: [
IconButton(
tooltip: tr.recordNewDebt,
icon: const Icon(Icons.person_add_alt_1_rounded, color: AppColors.primary, size: 26),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(tr.isTaglish ? 'Magbubukas ang form para sa bagong pautang...' : 'Opening new debt recording form...'),
),
);
},
),
const SizedBox(width: 8),
],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
// Total Collectibles Banner
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.warning, AppColors.warning.withAlpha(200)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: AppColors.warning.withAlpha(80),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
tr.totalCollectibles.toUpperCase(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
letterSpacing: 1.1,
),
),
const Icon(Icons.menu_book_rounded, color: Colors.white, size: 24),
],
),
const SizedBox(height: 8),
Text(
'₱ ${totalCollectibles.toStringAsFixed(2)}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 34,
),
),
const SizedBox(height: 6),
Text(
'${sampleDebts.length} ${tr.activeDebtors}',
style: const TextStyle(color: Colors.white70, fontSize: 13, fontWeight: FontWeight.w500),
),
],
),
),
const SizedBox(height: 20),
// Search bar
TextField(
decoration: InputDecoration(
prefixIcon: const Icon(Icons.search, color: AppColors.primary),
hintText: tr.isTaglish ? 'Hanapin ang pangalan ng may utang...' : 'Search customer name...',
filled: true,
fillColor: isLight ? AppColors.surfaceLight : AppColors.surfaceDark,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
),
),
const SizedBox(height: 16),
// Debt Customer Cards
...sampleDebts.map((debt) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
debt['name'] as String,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
Text(
'₱ ${(debt['amount'] as double).toStringAsFixed(2)}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
color: AppColors.alert,
),
),
],
),
const SizedBox(height: 4),
Text(
'📞 ${debt['phone']} • ${debt['date']}',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.withAlpha(20),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Paninda: ${debt['items']}',
style: const TextStyle(fontSize: 12, fontStyle: FontStyle.italic),
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
OutlinedButton.icon(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(tr.isTaglish ? 'Sisingilin si ${debt['name']}...' : 'Sending SMS reminder to ${debt['name']}...'),
),
);
},
icon: const Icon(Icons.send_rounded, size: 16),
label: Text(tr.isTaglish ? 'I-text Paalala' : 'Send Reminder'),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(tr.isTaglish ? 'Naitalang bayad ang utang!' : 'Payment marked as settled!'),
backgroundColor: AppColors.primary,
),
);
},
icon: const Icon(Icons.check, size: 16),
label: Text(tr.settleDebt),
),
],
),
],
),
),
);
}),
],
),
);
}
}
@@ -0,0 +1,300 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/constants/app_colors.dart';
import '../../core/localization/language_provider.dart';
class GcashScreen extends ConsumerWidget {
const GcashScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final tr = ref.watch(stringsProvider);
final isLight = Theme.of(context).brightness == Brightness.light;
final feeBrackets = [
{'range': '₱ 1 - ₱ 500', 'fee': '₱ 10.00'},
{'range': '₱ 501 - ₱ 1,000', 'fee': '₱ 15.00'},
{'range': '₱ 1,001 - ₱ 1,500', 'fee': '₱ 20.00'},
{'range': '₱ 1,501 - ₱ 2,000', 'fee': '₱ 25.00'},
{'range': '₱ 2,001 - ₱ 3,000', 'fee': '₱ 35.00'},
{'range': '₱ 3,001 - ₱ 5,000', 'fee': '₱ 50.00'},
];
return Scaffold(
appBar: AppBar(
title: Text(
tr.gcashTitle,
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
// Float balances (Dual Balances: GCash vs Cash Box)
Row(
children: [
Expanded(
child: _buildBalanceCard(
context,
title: tr.ewalletFloat,
amount: '₱ 8,450.00',
icon: Icons.account_balance_wallet_rounded,
color: Colors.blue.shade700,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildBalanceCard(
context,
title: tr.cashBoxDrawer,
amount: '₱ 4,120.00',
icon: Icons.payments_rounded,
color: AppColors.primary,
),
),
],
),
const SizedBox(height: 20),
// Quick Action Buttons
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () {
_showGcashActionModal(context, tr, isCashIn: true);
},
icon: const Icon(Icons.arrow_upward_rounded),
label: Text(tr.cashInAction),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade700,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () {
_showGcashActionModal(context, tr, isCashIn: false);
},
icon: const Icon(Icons.arrow_downward_rounded),
label: Text(tr.cashOutAction),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
),
),
],
),
const SizedBox(height: 24),
// Fee Scheduler Table Card
Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
tr.feeSchedule,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const Icon(Icons.receipt_long_rounded, color: Colors.blue, size: 22),
],
),
const SizedBox(height: 12),
const Divider(),
Table(
children: [
TableRow(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
tr.isTaglish ? 'Halaga ng Transaksyon' : 'Transaction Range',
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.grey),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
tr.isTaglish ? 'Patong / Tubo' : 'Fee / Profit',
textAlign: TextAlign.end,
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.grey),
),
),
],
),
...feeBrackets.map((f) {
return TableRow(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
f['range']!,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
f['fee']!,
textAlign: TextAlign.end,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: AppColors.primary,
),
),
),
],
);
}),
],
),
],
),
),
),
],
),
);
}
void _showGcashActionModal(BuildContext context, AppStrings tr, {required bool isCashIn}) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) {
final isLight = Theme.of(ctx).brightness == Brightness.light;
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: isLight ? Colors.white : AppColors.surfaceDark,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
isCashIn ? tr.cashInAction : tr.cashOutAction,
style: Theme.of(ctx).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
TextField(
keyboardType: TextInputType.phone,
decoration: InputDecoration(
labelText: tr.isTaglish ? 'Numero ng GCash (09xx...)' : 'GCash Phone Number',
prefixIcon: const Icon(Icons.phone_android_rounded),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
),
),
const SizedBox(height: 12),
TextField(
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: InputDecoration(
labelText: tr.isTaglish ? 'Halaga (Halimbawa: 500)' : 'Amount (e.g. 500)',
prefixIcon: const Icon(Icons.attach_money_rounded),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.primaryLight,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
tr.isTaglish ? 'Awtomatikong Patong (Fee):' : 'Calculated Convenience Fee:',
style: const TextStyle(fontWeight: FontWeight.bold, color: AppColors.primary),
),
const Text(
'₱ 10.00',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: AppColors.primary),
),
],
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.pop(ctx);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(tr.isTaglish ? '✅ GCash transaksyon naitala!' : '✅ GCash transaction recorded!'),
backgroundColor: AppColors.primary,
),
);
},
child: Text(tr.isTaglish ? 'I-REHISTRO ANG TRANSAKSYON' : 'RECORD TRANSACTION'),
),
],
),
),
);
},
);
}
Widget _buildBalanceCard(
BuildContext context, {
required String title,
required String amount,
required IconData icon,
required Color color,
}) {
final isLight = Theme.of(context).brightness == Brightness.light;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isLight ? Colors.white : AppColors.surfaceDark,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: color.withAlpha(60)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
backgroundColor: color.withAlpha(30),
radius: 18,
child: Icon(icon, color: color, size: 20),
),
const SizedBox(width: 8),
Expanded(
child: Text(
title,
style: const TextStyle(fontSize: 11, color: Colors.grey, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 12),
Text(
amount,
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: color),
),
],
),
);
}
}
@@ -0,0 +1,198 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/constants/app_colors.dart';
import '../../core/localization/language_provider.dart';
class InventoryScreen extends ConsumerWidget {
const InventoryScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final tr = ref.watch(stringsProvider);
final isLight = Theme.of(context).brightness == Brightness.light;
// Sample inventory items
final sampleProducts = [
{'name': 'Coca-Cola Mismo 300ml', 'barcode': '4800016644021', 'stock': 24, 'cost': 15.0, 'price': 20.0},
{'name': 'SkyFlakes Crackers Single', 'barcode': '4800016644038', 'stock': 48, 'cost': 6.5, 'price': 9.0},
{'name': 'Kopiko Blanca 30g Sachet', 'barcode': '8996001414002', 'stock': 32, 'cost': 10.5, 'price': 14.0},
{'name': 'Bear Brand Fortified 33g', 'barcode': '4800361280011', 'stock': 4, 'cost': 12.0, 'price': 16.0},
{'name': 'Lucky Me! Pancit Canton Kalamansi', 'barcode': '4800361300023', 'stock': 18, 'cost': 13.0, 'price': 17.0},
{'name': 'Safeguard Pure White 60g', 'barcode': '4902430730030', 'stock': 6, 'cost': 22.0, 'price': 28.0},
{'name': 'Silver Swan Sukang Puti 200ml', 'barcode': '4800123000045', 'stock': 12, 'cost': 9.0, 'price': 12.0},
{'name': 'Datu Puti Toyo 200ml', 'barcode': '4800123000052', 'stock': 15, 'cost': 10.0, 'price': 13.0},
];
return Scaffold(
appBar: AppBar(
title: Text(
tr.inventoryTitle,
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
actions: [
IconButton(
tooltip: tr.addProduct,
icon: const Icon(Icons.add_circle, color: AppColors.primary, size: 28),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(tr.isTaglish ? 'Bubuksan ang Barcode Scanner para mag-add...' : 'Opening Barcode Scanner to add item...'),
),
);
},
),
const SizedBox(width: 8),
],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
// Inventory Summary Cards
Row(
children: [
Expanded(
child: _buildSummaryCard(
context,
title: tr.totalProducts,
value: '${sampleProducts.length}',
icon: Icons.inventory_2_rounded,
color: AppColors.primary,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildSummaryCard(
context,
title: tr.lowStockWarning,
value: '2',
icon: Icons.warning_amber_rounded,
color: AppColors.alert,
),
),
],
),
const SizedBox(height: 16),
// Search bar
TextField(
decoration: InputDecoration(
prefixIcon: const Icon(Icons.search, color: AppColors.primary),
hintText: tr.searchPlaceholder,
filled: true,
fillColor: isLight ? AppColors.surfaceLight : AppColors.surfaceDark,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
),
),
const SizedBox(height: 16),
// Product List
...sampleProducts.map((p) {
final int stock = p['stock'] as int;
final isLow = stock <= 6;
return Card(
margin: const EdgeInsets.only(bottom: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: CircleAvatar(
backgroundColor: isLow ? AppColors.alertLight : AppColors.primaryLight,
child: Icon(
Icons.shopping_bag_outlined,
color: isLow ? AppColors.alert : AppColors.primary,
),
),
title: Text(
p['name'] as String,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
),
subtitle: Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'${p['barcode']} • ${tr.costPrice}: ₱${(p['cost'] as double).toStringAsFixed(2)}',
style: const TextStyle(fontSize: 12),
),
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'₱${(p['price'] as double).toStringAsFixed(2)}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
const SizedBox(height: 2),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: isLow ? AppColors.alertLight : Colors.grey.withAlpha(40),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'$stock ${tr.inStock}',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: isLow ? AppColors.alert : null,
),
),
),
],
),
),
);
}),
],
),
);
}
Widget _buildSummaryCard(
BuildContext context, {
required String title,
required String value,
required IconData icon,
required Color color,
}) {
final isLight = Theme.of(context).brightness == Brightness.light;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isLight ? Colors.white : AppColors.surfaceDark,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: color.withAlpha(60)),
),
child: Row(
children: [
CircleAvatar(
backgroundColor: color.withAlpha(30),
radius: 20,
child: Icon(icon, color: color, size: 22),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(fontSize: 11, color: Colors.grey, fontWeight: FontWeight.bold),
),
Text(
value,
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: color),
),
],
),
),
],
),
);
}
}
@@ -0,0 +1,222 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/constants/app_colors.dart';
import '../../core/localization/language_provider.dart';
class SettingsScreen extends ConsumerWidget {
const SettingsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final tr = ref.watch(stringsProvider);
final currentLang = ref.watch(languageProvider);
final isLight = Theme.of(context).brightness == Brightness.light;
return Scaffold(
appBar: AppBar(
title: Text(
tr.settingsTitle,
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
// Language Switcher Section (Taglish / English)
Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.language_rounded, color: AppColors.primary, size: 24),
const SizedBox(width: 8),
Text(
tr.languageSection,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: OutlinedButton(
style: OutlinedButton.styleFrom(
backgroundColor: currentLang == AppLanguage.taglish
? AppColors.primary
: Colors.transparent,
foregroundColor: currentLang == AppLanguage.taglish
? Colors.white
: (isLight ? Colors.black87 : Colors.white70),
side: BorderSide(
color: currentLang == AppLanguage.taglish
? AppColors.primary
: Colors.grey.withAlpha(80),
),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () {
ref.read(languageProvider.notifier).setLanguage(AppLanguage.taglish);
},
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('🇵🇭 ', style: TextStyle(fontSize: 16)),
Text('Taglish', style: TextStyle(fontWeight: FontWeight.bold)),
],
),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton(
style: OutlinedButton.styleFrom(
backgroundColor: currentLang == AppLanguage.english
? AppColors.primary
: Colors.transparent,
foregroundColor: currentLang == AppLanguage.english
? Colors.white
: (isLight ? Colors.black87 : Colors.white70),
side: BorderSide(
color: currentLang == AppLanguage.english
? AppColors.primary
: Colors.grey.withAlpha(80),
),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () {
ref.read(languageProvider.notifier).setLanguage(AppLanguage.english);
},
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('🇺🇸 ', style: TextStyle(fontSize: 16)),
Text('English', style: TextStyle(fontWeight: FontWeight.bold)),
],
),
),
),
],
),
],
),
),
),
const SizedBox(height: 16),
// Cloud Sync Card
Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.cloud_sync_rounded, color: AppColors.primary, size: 24),
const SizedBox(width: 8),
Text(
tr.cloudBackupTitle,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
],
),
const SizedBox(height: 6),
Text(
tr.cloudBackupSubtitle,
style: const TextStyle(fontSize: 13, color: Colors.grey),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(tr.isTaglish ? '☁️ Matagumpay na nai-sync sa Neon Cloud!' : '☁️ Successfully synced to Neon Cloud!'),
backgroundColor: AppColors.primary,
),
);
},
icon: const Icon(Icons.sync_rounded),
label: Text(tr.syncNowButton),
),
),
],
),
),
),
const SizedBox(height: 16),
// Security & PIN Card
Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: const CircleAvatar(
backgroundColor: AppColors.primaryLight,
child: Icon(Icons.lock_outline_rounded, color: AppColors.primary),
),
title: Text(
tr.securitySection,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
tr.isTaglish ? '4-6 digit Store PIN & Fingerprint' : '4-6 digit Store PIN & Biometrics',
),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(tr.isTaglish ? 'Ligtas ang iyong PIN gamit ang Flutter Secure Storage' : 'PIN safely protected by Flutter Secure Storage'),
),
);
},
),
),
const SizedBox(height: 12),
// System Status & Architecture Card
Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.info_outline_rounded, color: Colors.grey, size: 22),
const SizedBox(width: 8),
Text(
tr.aboutTindaGrid,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
),
],
),
const SizedBox(height: 8),
Text(
tr.systemStatusSubtitle,
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
const SizedBox(height: 4),
const Text(
'SQLite Drift (Local) + Neon PostgreSQL (Cloud) + Cloudflare R2',
style: TextStyle(fontSize: 11, color: Colors.grey),
),
],
),
),
),
],
),
);
}
}
+193 -80
View File
@@ -1,9 +1,15 @@
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'core/constants/app_colors.dart';
import 'core/localization/language_provider.dart';
import 'core/theme/app_theme.dart';
import 'core/theme/responsive_layout.dart';
import 'core/utils/web_storage_helper.dart';
import 'features/debts/debts_screen.dart';
import 'features/gcash/gcash_screen.dart';
import 'features/inventory/inventory_screen.dart';
import 'features/settings/settings_screen.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -29,17 +35,88 @@ class TindaGridApp extends StatelessWidget {
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.system,
home: const PosDashboardScreen(),
// Web is strictly POS counter terminal; Android/Mobile includes full navigation
home: kIsWeb ? const PosDashboardScreen() : const MainNavigationShell(),
);
}
}
// ---------------------------------------------------------------------------
// POS Demo Dashboard (All-Ages Touch-Driven Interface)
// Main Navigation Shell for Android / Mobile
// ---------------------------------------------------------------------------
class MainNavigationShell extends ConsumerStatefulWidget {
const MainNavigationShell({super.key});
@override
ConsumerState<MainNavigationShell> createState() => _MainNavigationShellState();
}
class _MainNavigationShellState extends ConsumerState<MainNavigationShell> {
int _currentIndex = 0;
final List<Widget> _screens = const [
PosDashboardScreen(isEmbeddedInNav: true),
InventoryScreen(),
DebtsScreen(),
GcashScreen(),
SettingsScreen(),
];
@override
Widget build(BuildContext context) {
final tr = ref.watch(stringsProvider);
return Scaffold(
body: _screens[_currentIndex],
bottomNavigationBar: NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: (index) {
setState(() => _currentIndex = index);
},
destinations: [
NavigationDestination(
icon: const Icon(Icons.point_of_sale_outlined),
selectedIcon: const Icon(Icons.point_of_sale_rounded, color: AppColors.primary),
label: tr.navPos,
),
NavigationDestination(
icon: const Icon(Icons.inventory_2_outlined),
selectedIcon: const Icon(Icons.inventory_2_rounded, color: AppColors.primary),
label: tr.navInventory,
),
NavigationDestination(
icon: const Icon(Icons.menu_book_outlined),
selectedIcon: const Icon(Icons.menu_book_rounded, color: AppColors.primary),
label: tr.navDebts,
),
NavigationDestination(
icon: const Icon(Icons.payments_outlined),
selectedIcon: const Icon(Icons.payments_rounded, color: AppColors.primary),
label: tr.navGcash,
),
NavigationDestination(
icon: const Icon(Icons.settings_outlined),
selectedIcon: const Icon(Icons.settings_rounded, color: AppColors.primary),
label: tr.navSettings,
),
],
),
);
}
}
// ---------------------------------------------------------------------------
// POS Dashboard Screen (All-Ages Touch-Driven Counter Interface)
// ---------------------------------------------------------------------------
class PosDashboardScreen extends ConsumerStatefulWidget {
const PosDashboardScreen({super.key});
final bool isEmbeddedInNav;
const PosDashboardScreen({
super.key,
this.isEmbeddedInNav = false,
});
@override
ConsumerState<PosDashboardScreen> createState() => _PosDashboardScreenState();
@@ -69,7 +146,7 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
});
}
void _showCheckoutDialog() {
void _showCheckoutDialog(AppStrings tr) {
double tendered = 100.0;
showModalBottomSheet(
context: context,
@@ -96,7 +173,7 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Bayaran at Sukli',
tr.checkoutModalTitle,
style: Theme.of(context).textTheme.titleLarge,
),
IconButton(
@@ -107,7 +184,7 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
),
const SizedBox(height: 16),
Text(
'KABUUANG BABAYARAN:',
tr.totalPayable,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
),
@@ -120,7 +197,7 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
),
const SizedBox(height: 20),
Text(
'PERANG INABOT NG MAMIMILI:',
tr.cashTendered,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
),
@@ -130,7 +207,7 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
spacing: 8,
runSpacing: 8,
children: [
_buildQuickBillChip('Eksakto', _totalPrice, tendered, (val) {
_buildQuickBillChip(tr.exactBill, _totalPrice, tendered, (val) {
setModalState(() => tendered = val);
}),
_buildQuickBillChip('₱50', 50.0, tendered, (val) {
@@ -169,8 +246,8 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
children: [
Text(
sukli >= 0
? '🟢 SUKLI SA MAMIMILI:'
: '⚠️ KULANG PA NG PERA:',
? tr.customerChange
: tr.insufficientCash,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
@@ -201,13 +278,13 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
setState(() => _cart.clear());
Navigator.pop(ctx);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('✅ Benta naitala! Ligtas sa lokal database.'),
SnackBar(
content: Text(tr.saleRecorded),
backgroundColor: AppColors.primary,
),
);
},
child: const Text('TAPOS NA ANG BENTA'),
child: Text(tr.completeSale),
),
],
),
@@ -239,38 +316,67 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
@override
Widget build(BuildContext context) {
final tr = ref.watch(stringsProvider);
final currentLang = ref.watch(languageProvider);
return Scaffold(
appBar: AppBar(
title: Row(
children: [
const Icon(Icons.storefront, color: AppColors.primary, size: 28),
const SizedBox(width: 8),
Text(
'TindaGrid',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
titleSpacing: 0,
leading: const Icon(Icons.storefront, color: AppColors.primary, size: 24),
title: Text(
tr.appName,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
actions: [
// 1-Tap Language Switcher Button
InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
ref.read(languageProvider.notifier).toggleLanguage();
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColors.primaryLight,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(currentLang == AppLanguage.taglish ? '🇵🇭' : '🇺🇸', style: const TextStyle(fontSize: 12)),
const SizedBox(width: 4),
Text(
currentLang == AppLanguage.taglish ? 'Taglish' : 'English',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 11,
color: AppColors.primary,
),
),
],
),
),
),
const SizedBox(width: 6),
// Offline Status Badge
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColors.primaryLight,
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(12),
),
child: const Row(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.check_circle, color: AppColors.primary, size: 16),
SizedBox(width: 4),
const Icon(Icons.check_circle, color: AppColors.primary, size: 14),
const SizedBox(width: 4),
Text(
'Ligtas (Offline)',
style: TextStyle(
tr.offlineSafe,
style: const TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.bold,
fontSize: 12,
fontSize: 10,
),
),
],
@@ -280,36 +386,36 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
],
),
body: ResponsiveLayout(
mobile: _buildMobileLayout(),
desktop: _buildDesktopLayout(),
mobile: _buildMobileLayout(tr),
desktop: _buildDesktopLayout(tr),
),
bottomNavigationBar: ResponsiveLayout.isDesktop(context)
? null
: _buildMobileCartBottomBar(),
: _buildMobileCartBottomBar(tr),
);
}
// Mobile View
Widget _buildMobileLayout() {
Widget _buildMobileLayout(AppStrings tr) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildSearchBar(),
_buildSearchBar(tr),
const SizedBox(height: 20),
_buildTingiGridHeader(),
_buildTingiGridHeader(tr),
const SizedBox(height: 12),
_buildTingiTiles(),
const SizedBox(height: 24),
_buildRecentCartList(),
_buildRecentCartList(tr),
],
),
);
}
// Desktop View (Split-Screen 2-Column POS Station)
Widget _buildDesktopLayout() {
Widget _buildDesktopLayout(AppStrings tr) {
return Row(
children: [
// Left Column (Catalog & Fast Tingi Grid)
@@ -320,9 +426,9 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildSearchBar(),
_buildSearchBar(tr),
const SizedBox(height: 20),
_buildTingiGridHeader(),
_buildTingiGridHeader(tr),
const SizedBox(height: 12),
_buildTingiTiles(),
],
@@ -331,7 +437,7 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
),
// Right Column (Sticky Live Ticket & Checkout Panel)
Container(
width: 320,
width: 340,
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
border: const Border(
@@ -347,22 +453,22 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
children: [
Expanded(
child: Text(
'🛒 Kasalukuyang Kart',
'🛒 ${tr.liveTicketTitle}',
style: Theme.of(context).textTheme.titleMedium,
overflow: TextOverflow.ellipsis,
),
),
Text('$_totalItems item',
Text('$_totalItems ${tr.itemsCountLabel}',
style: Theme.of(context).textTheme.bodyMedium),
],
),
const Divider(height: 24),
Expanded(child: _buildCartItemsList()),
Expanded(child: _buildCartItemsList(tr)),
const Divider(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('KABUUAN:', style: Theme.of(context).textTheme.titleMedium),
Text('${tr.totalLabel}:', style: Theme.of(context).textTheme.titleMedium),
Flexible(
child: Text(
'₱ ${_totalPrice.toStringAsFixed(2)}',
@@ -377,9 +483,9 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _cart.isEmpty ? null : _showCheckoutDialog,
onPressed: _cart.isEmpty ? null : () => _showCheckoutDialog(tr),
icon: const Icon(Icons.point_of_sale, size: 24),
label: const Text('BAYARAN / MANINGIL'),
label: Text(tr.checkoutButton),
),
],
),
@@ -388,16 +494,18 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
);
}
Widget _buildSearchBar() {
Widget _buildSearchBar(AppStrings tr) {
return TextField(
decoration: InputDecoration(
hintText: 'I-type ang paninda o i-scan ang barcode...',
hintText: tr.searchPlaceholder,
prefixIcon: const Icon(Icons.search, size: 24),
suffixIcon: IconButton(
icon: const Icon(Icons.qr_code_scanner, color: AppColors.primary),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('📷 Binubuksan ang scanner...')),
SnackBar(
content: Text(tr.isTaglish ? '📷 Binubuksan ang scanner...' : '📷 Opening camera scanner...'),
),
);
},
),
@@ -405,14 +513,14 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
);
}
Widget _buildTingiGridHeader() {
Widget _buildTingiGridHeader(AppStrings tr) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Mabilisan Tingi Grid',
style: Theme.of(context).textTheme.titleMedium,
tr.quickTingiTitle,
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontSize: 13, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
),
@@ -485,45 +593,60 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
);
}
Widget _buildRecentCartList() {
Widget _buildRecentCartList(AppStrings tr) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Mga Bagay sa Kart',
tr.recentCartTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
_buildCartItemsList(),
_buildCartItemsList(tr),
],
);
}
Widget _buildCartItemsList() {
Widget _buildCartItemsList([AppStrings? tr]) {
if (_cart.isEmpty) {
return const Center(
return Center(
child: Padding(
padding: EdgeInsets.all(32),
child: Text('Walang laman ang kart. Pindutin ang paninda sa itaas.'),
padding: const EdgeInsets.symmetric(vertical: 24),
child: Text(
tr?.emptyCartPrompt ?? 'Walang laman ang kart.',
style: const TextStyle(color: Colors.grey, fontStyle: FontStyle.italic),
),
),
);
}
return ListView.separated(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
physics: const NeverScrollableScrollPhysics(),
itemCount: _cart.length,
separatorBuilder: (context, index) => const SizedBox(height: 8),
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final item = _cart[index];
return Card(
margin: EdgeInsets.zero,
child: ListTile(
dense: true,
title: Text(item.name, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text('₱ ${item.price.toStringAsFixed(2)} bawat isa'),
subtitle: Text(
'${item.quantity}x @ ₱${item.price.toStringAsFixed(2)} ${tr?.eachPrice ?? ''}',
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'₱ ${(item.price * item.quantity).toStringAsFixed(2)}',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
IconButton(
icon: const Icon(Icons.remove_circle_outline, size: 20),
icon: const Icon(Icons.remove_circle_outline, color: AppColors.alert, size: 20),
onPressed: () {
setState(() {
if (item.quantity > 1) {
@@ -534,16 +657,6 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
});
},
),
Text(
'${item.quantity}',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
IconButton(
icon: const Icon(Icons.add_circle_outline, size: 20),
onPressed: () {
setState(() => item.quantity++);
},
),
],
),
),
@@ -552,7 +665,7 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
);
}
Widget _buildMobileCartBottomBar() {
Widget _buildMobileCartBottomBar(AppStrings tr) {
if (_cart.isEmpty) return const SizedBox.shrink();
return Container(
@@ -576,7 +689,7 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$_totalItems bagay sa kart',
'$_totalItems ${tr.itemsCountLabel} sa ${tr.cartLabel.toLowerCase()}',
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
),
Text(
@@ -593,8 +706,8 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: _showCheckoutDialog,
child: const Text('MANINGIL'),
onPressed: () => _showCheckoutDialog(tr),
child: Text(tr.checkoutButton),
),
),
],
+55 -3
View File
@@ -1,18 +1,70 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tindagrid/main.dart';
void main() {
testWidgets('TindaGrid app smoke test', (WidgetTester tester) async {
testWidgets('TindaGrid navigation and bilingual test', (WidgetTester tester) async {
// Provide a standard phone screen size
tester.view.physicalSize = const Size(1080, 2400);
tester.view.devicePixelRatio = 2.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(
const ProviderScope(
child: TindaGridApp(),
),
);
await tester.pumpAndSettle();
// Verify TindaGrid brand name is present
// 1. Verify Brand name and default Taglish state on POS
expect(find.text('TindaGrid'), findsOneWidget);
// Verify offline safe badge is present
expect(find.text('Ligtas (Offline)'), findsOneWidget);
// 2. Verify Android Navigation Bar destinations are present in Taglish
expect(find.text('POS'), findsOneWidget);
expect(find.text('Paninda'), findsOneWidget);
expect(find.text('Utang'), findsOneWidget);
expect(find.text('GCash'), findsOneWidget);
expect(find.text('Settings'), findsOneWidget);
// 3. Test 1-Tap Language Toggle in AppBar (Taglish -> English)
final langToggle = find.text('Taglish');
expect(langToggle, findsOneWidget);
await tester.tap(langToggle);
await tester.pumpAndSettle();
// Verify UI switched to English
expect(find.text('Safe (Offline)'), findsOneWidget);
expect(find.text('English'), findsOneWidget);
expect(find.text('Inventory'), findsOneWidget);
expect(find.text('Debts'), findsOneWidget);
// 4. Test Navigation Bar Switch to Settings Screen
await tester.tap(find.text('Settings'));
await tester.pumpAndSettle();
// Settings screen should be visible with English labels
expect(find.text('Store Settings'), findsOneWidget);
expect(find.text('Display Language'), findsOneWidget);
expect(find.text('Cloud Backup & Sync'), findsOneWidget);
// 5. Test Switching back to Taglish via Settings button
await tester.tap(find.text('Taglish'));
await tester.pumpAndSettle();
// Settings screen should update to Taglish labels
expect(find.text('Pag-aayos ng Tindahan'), findsOneWidget);
expect(find.text('Piliin ang Wika'), findsOneWidget);
expect(find.text('Paninda'), findsOneWidget);
expect(find.text('Utang'), findsOneWidget);
// 6. Navigate back to POS
await tester.tap(find.text('POS'));
await tester.pumpAndSettle();
expect(find.text('Ligtas (Offline)'), findsOneWidget);
expect(find.text('MABILISANG TINGI (Walang Barcode — Pindutin Agad):'), findsOneWidget);
});
}