Files
dayMaker_lp/lib/Screens/item_screen.dart
2026-05-17 17:05:01 +01:00

726 lines
23 KiB
Dart

import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../constants/item_categories.dart';
import 'add_item_screen.dart';
class ItemScreen extends StatefulWidget {
const ItemScreen({super.key});
@override
State<ItemScreen> createState() => _ItemScreenState();
}
class _ItemScreenState extends State<ItemScreen> {
List<Map<String, dynamic>> _items = [];
bool _isLoading = true;
String _searchQuery = '';
String? _selectedCategoryFilter;
List<Map<String, dynamic>> get _filteredItems {
return _items.where((item) {
final name = (item['nome'] ?? '').toString().toLowerCase();
final tags = List<String>.from(
item['tags'] ?? [],
).join(' ').toLowerCase();
final matchesSearch =
_searchQuery.isEmpty ||
name.contains(_searchQuery.toLowerCase()) ||
tags.contains(_searchQuery.toLowerCase());
final matchesCategory =
_selectedCategoryFilter == null ||
item['categoria'] == _selectedCategoryFilter;
return matchesSearch && matchesCategory;
}).toList();
}
@override
void initState() {
super.initState();
_loadItems();
}
Future<void> _loadItems() async {
setState(() => _isLoading = true);
try {
final user = Supabase.instance.client.auth.currentUser;
if (user == null) return;
final response = await Supabase.instance.client
.from('items')
.select('*, item_images(image_url)')
.eq('user_id', user.id)
.order('id', ascending: false);
setState(() {
_items = List<Map<String, dynamic>>.from(response);
_isLoading = false;
});
} catch (e) {
debugPrint('Error loading items: $e');
setState(() => _isLoading = false);
}
}
String? _imageUrl(Map<String, dynamic> item) {
final images = item['item_images'] as List?;
if (images != null && images.isNotEmpty) {
return images.first['image_url'] as String?;
}
return null;
}
String _categoryName(String? id) {
if (id == null) return 'Outros';
return itemCategories
.firstWhere((c) => c.id == id, orElse: () => itemCategories.last)
.name;
}
String _categoryIcon(String? id) {
if (id == null) return '📦';
return itemCategories
.firstWhere((c) => c.id == id, orElse: () => itemCategories.last)
.icon;
}
Future<void> _deleteItem(Map<String, dynamic> item) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Apagar item'),
content: Text('Tem certeza que deseja apagar "${item['nome']}"?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Cancelar'),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Apagar'),
),
],
),
);
if (confirmed != true) return;
try {
await Supabase.instance.client
.from('item_images')
.delete()
.eq('item_id', item['id']);
await Supabase.instance.client
.from('items')
.delete()
.eq('id', item['id']);
_loadItems();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Item apagado'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erro: $e'), backgroundColor: Colors.red),
);
}
}
}
void _viewItem(Map<String, dynamic> item) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ItemDetailScreen(item: item, imageUrl: _imageUrl(item)),
),
).then((_) => _loadItems());
}
void _editItem(Map<String, dynamic> item) {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => EditItemScreen(item: item)),
).then((_) => _loadItems());
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFFFE5CC),
appBar: AppBar(
backgroundColor: const Color(0xFF0066CC),
elevation: 0,
title: const Text(
'Meus Itens',
style: TextStyle(color: Colors.white, fontSize: 20),
),
centerTitle: true,
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: Column(
children: [
_buildSearchAndFilters(),
Expanded(
child: _items.isEmpty
? _buildEmpty()
: _filteredItems.isEmpty
? const Center(
child: Text(
'Nenhum item encontrado',
style: TextStyle(color: Color(0xFF666666)),
),
)
: RefreshIndicator(
onRefresh: _loadItems,
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _filteredItems.length,
itemBuilder: (context, i) =>
_buildItemCard(_filteredItems[i]),
),
),
),
],
),
floatingActionButton: FloatingActionButton(
backgroundColor: const Color(0xFF0066CC),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const AddItemScreen()),
).then((_) => _loadItems());
},
child: const Icon(Icons.add, color: Colors.white),
),
);
}
Widget _buildSearchAndFilters() {
return Container(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
color: const Color(0xFFFFE5CC),
child: Column(
children: [
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: TextField(
onChanged: (v) => setState(() => _searchQuery = v),
decoration: const InputDecoration(
prefixIcon: Icon(Icons.search, color: Color(0xFF666666)),
hintText: 'Pesquisar por nome ou tag...',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 14,
),
),
),
),
const SizedBox(height: 8),
SizedBox(
height: 38,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
_categoryChip(null, 'Todos', '🗂'),
...itemCategories.map(
(c) => _categoryChip(c.id, c.name, c.icon),
),
],
),
),
],
),
);
}
Widget _categoryChip(String? id, String name, String icon) {
final selected = _selectedCategoryFilter == id;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: GestureDetector(
onTap: () => setState(() => _selectedCategoryFilter = id),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: selected ? const Color(0xFF0066CC) : Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: selected
? const Color(0xFF0066CC)
: const Color(0xFFE0E0E0),
),
),
child: Row(
children: [
Text(icon, style: const TextStyle(fontSize: 14)),
const SizedBox(width: 6),
Text(
name,
style: TextStyle(
color: selected ? Colors.white : const Color(0xFF333333),
fontWeight: FontWeight.w500,
),
),
],
),
),
),
);
}
Widget _buildEmpty() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inventory_2_outlined, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
const Text(
'Nenhum item ainda',
style: TextStyle(fontSize: 18, color: Color(0xFF666666)),
),
const SizedBox(height: 8),
const Text(
'Toque no + para adicionar',
style: TextStyle(fontSize: 14, color: Color(0xFF999999)),
),
],
),
);
}
Widget _buildItemCard(Map<String, dynamic> item) {
final categoryName = _categoryName(item['categoria']);
final categoryIcon = _categoryIcon(item['categoria']);
final tags = List<String>.from(item['tags'] ?? []);
final imageUrl = _imageUrl(item);
return Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () => _viewItem(item),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: imageUrl != null
? Image.network(
imageUrl,
width: 72,
height: 72,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
_iconPlaceholder(categoryIcon),
)
: _iconPlaceholder(categoryIcon),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item['nome'] ?? 'Sem nome',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF333333),
),
),
const SizedBox(height: 2),
Text(
categoryName,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF666666),
),
),
if (tags.isNotEmpty) ...[
const SizedBox(height: 6),
Wrap(
spacing: 4,
runSpacing: 4,
children: tags.take(3).map((tag) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: const Color(
0xFF0066CC,
).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(
tag,
style: const TextStyle(
fontSize: 10,
color: Color(0xFF0066CC),
),
),
);
}).toList(),
),
],
],
),
),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert, color: Color(0xFF666666)),
onSelected: (value) {
if (value == 'view') _viewItem(item);
if (value == 'edit') _editItem(item);
if (value == 'delete') _deleteItem(item);
},
itemBuilder: (_) => [
const PopupMenuItem(
value: 'view',
child: Row(
children: [
Icon(Icons.visibility_outlined, size: 18),
SizedBox(width: 8),
Text('Ver'),
],
),
),
const PopupMenuItem(
value: 'edit',
child: Row(
children: [
Icon(Icons.edit_outlined, size: 18),
SizedBox(width: 8),
Text('Editar'),
],
),
),
const PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete_outline, size: 18, color: Colors.red),
SizedBox(width: 8),
Text('Apagar', style: TextStyle(color: Colors.red)),
],
),
),
],
),
],
),
),
),
);
}
Widget _iconPlaceholder(String icon) {
return Container(
width: 72,
height: 72,
color: const Color(0xFF0066CC).withValues(alpha: 0.1),
alignment: Alignment.center,
child: Text(icon, style: const TextStyle(fontSize: 32)),
);
}
}
// =============================================
// Detalhe do item
// =============================================
class ItemDetailScreen extends StatelessWidget {
final Map<String, dynamic> item;
final String? imageUrl;
const ItemDetailScreen({super.key, required this.item, this.imageUrl});
@override
Widget build(BuildContext context) {
final categoryId = item['categoria'] as String?;
final categoryName = categoryId == null
? 'Outros'
: itemCategories
.firstWhere(
(c) => c.id == categoryId,
orElse: () => itemCategories.last,
)
.name;
final tags = List<String>.from(item['tags'] ?? []);
return Scaffold(
backgroundColor: const Color(0xFFFFE5CC),
appBar: AppBar(
backgroundColor: const Color(0xFF0066CC),
title: Text(
item['nome'] ?? 'Item',
style: const TextStyle(color: Colors.white),
),
iconTheme: const IconThemeData(color: Colors.white),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (imageUrl != null)
ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.network(
imageUrl!,
width: double.infinity,
height: 250,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
const SizedBox.shrink(),
),
),
const SizedBox(height: 20),
_label('Nome'),
_value(item['nome'] ?? ''),
const SizedBox(height: 16),
_label('Categoria'),
_value(categoryName),
if (tags.isNotEmpty) ...[
const SizedBox(height: 16),
_label('Tags'),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: tags
.map(
(t) => Chip(
label: Text(t),
backgroundColor: const Color(
0xFF0066CC,
).withValues(alpha: 0.1),
labelStyle: const TextStyle(color: Color(0xFF0066CC)),
),
)
.toList(),
),
],
],
),
),
);
}
Widget _label(String t) => Text(
t,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Color(0xFF666666),
),
);
Widget _value(String t) => Container(
width: double.infinity,
margin: const EdgeInsets.only(top: 6),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Text(t, style: const TextStyle(fontSize: 16)),
);
}
// =============================================
// Editar item
// =============================================
class EditItemScreen extends StatefulWidget {
final Map<String, dynamic> item;
const EditItemScreen({super.key, required this.item});
@override
State<EditItemScreen> createState() => _EditItemScreenState();
}
class _EditItemScreenState extends State<EditItemScreen> {
late TextEditingController _nameController;
ItemCategory? _selectedCategory;
final Set<String> _selectedTags = {};
bool _isLoading = false;
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.item['nome'] ?? '');
final catId = widget.item['categoria'] as String?;
if (catId != null) {
_selectedCategory = itemCategories.firstWhere(
(c) => c.id == catId,
orElse: () => itemCategories.last,
);
}
final tags = List<String>.from(widget.item['tags'] ?? []);
_selectedTags.addAll(tags);
}
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
Future<void> _save() async {
if (_nameController.text.trim().isEmpty) {
_snack('Nome não pode ser vazio', Colors.red);
return;
}
setState(() => _isLoading = true);
try {
await Supabase.instance.client
.from('items')
.update({
'nome': _nameController.text.trim(),
'categoria': _selectedCategory?.id,
'tags': _selectedTags.toList(),
})
.eq('id', widget.item['id']);
_snack('Item atualizado!', Colors.green);
if (mounted) Navigator.pop(context);
} catch (e) {
_snack('Erro: $e', Colors.red);
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
void _snack(String msg, Color color) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(msg), backgroundColor: color));
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFFFE5CC),
appBar: AppBar(
backgroundColor: const Color(0xFF0066CC),
title: const Text('Editar Item', style: TextStyle(color: Colors.white)),
iconTheme: const IconThemeData(color: Colors.white),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: TextField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Nome do item',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
),
),
),
const SizedBox(height: 16),
const Text(
'Categoria',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<ItemCategory>(
value: _selectedCategory,
isExpanded: true,
items: itemCategories
.map(
(c) => DropdownMenuItem(
value: c,
child: Text('${c.icon} ${c.name}'),
),
)
.toList(),
onChanged: (v) => setState(() => _selectedCategory = v),
),
),
),
const SizedBox(height: 16),
const Text(
'Tags',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: contextTags.map((tag) {
final selected = _selectedTags.contains(tag.id);
return FilterChip(
label: Text(tag.name),
selected: selected,
onSelected: (_) {
setState(() {
if (selected) {
_selectedTags.remove(tag.id);
} else {
_selectedTags.add(tag.id);
}
});
},
selectedColor: const Color(0xFF0066CC).withValues(alpha: 0.2),
checkmarkColor: const Color(0xFF0066CC),
backgroundColor: Colors.white,
);
}).toList(),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
height: 52,
child: ElevatedButton(
onPressed: _isLoading ? null : _save,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0066CC),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: _isLoading
? const CircularProgressIndicator(color: Colors.white)
: const Text(
'Guardar alterações',
style: TextStyle(color: Colors.white, fontSize: 16),
),
),
),
],
),
),
);
}
}