Files
MeowSpool/mobile/app/(app)/(tabs)/inventory.tsx
T
Felipe 9c0b3d584c feat: enhance filament management and UI improvements
- Update SpoolPresetRepository to include user_id in the update query.
- Expand .gitignore to include various environment and temporary files.
- Revise agent documentation for better clarity and formatting.
- Implement pull-to-refresh functionality in the inventory list.
- Integrate API calls for deleting and updating filaments, ensuring state synchronization.
- Add custom color picker for filament color selection with hex validation.
- Update AndroidManifest and Gradle files for improved configuration and permissions.
- Refactor MainActivity and MainApplication for better splash screen handling.
- Update styles and colors for a cohesive UI experience.
- Replace splash screen logos and icons with new assets.
2026-03-14 14:14:06 -03:00

152 lines
7.0 KiB
TypeScript

import React, { useState } from 'react';
import {
View, Text, FlatList, TouchableOpacity, StyleSheet, TextInput, RefreshControl,
} from 'react-native';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useFilamentStore } from '@store/filamentStore';
import { listFilamentsUseCase } from '@infrastructure/container';
import { useAuthStore } from '@store/authStore';
import { FilamentCard } from '@presentation/components/filament/FilamentCard';
import type { Filament } from '@domain/Filament';
import { colors, typography, spacing, radius } from '@shared/theme';
import { MATERIALS } from '@shared/constants';
const TABS = ['Todos', ...MATERIALS.slice(0, 4)] as const;
/**
* Tela de Inventário — CW-0
* Busca, chips de material, lista de filamentos com peso e % de estoque.
*/
export default function InventoryScreen(): React.ReactElement {
const router = useRouter();
const { filaments, setFilaments } = useFilamentStore();
const { user } = useAuthStore();
const [search, setSearch] = useState('');
const [activeTab, setActiveTab] = useState<string>('Todos');
const [isRefreshing, setIsRefreshing] = useState(false);
const filtered = filaments.filter((f) => {
const matchMaterial = activeTab === 'Todos' || f.material === activeTab;
const matchSearch =
search.length === 0 ||
f.brand.toLowerCase().includes(search.toLowerCase()) ||
(f.model ?? '').toLowerCase().includes(search.toLowerCase()) ||
f.material.toLowerCase().includes(search.toLowerCase());
return matchMaterial && matchSearch;
});
async function onRefresh(): Promise<void> {
setIsRefreshing(true);
try {
const updated = await listFilamentsUseCase.execute(user?.id || '');
setFilaments(updated);
} catch (err) {
console.error('refresh filaments error', err);
} finally {
setIsRefreshing(false);
}
}
function renderItem({ item }: { item: Filament }): React.ReactElement {
return <FilamentCard filament={item} />;
}
return (
<SafeAreaView style={styles.safe}>
{/* Cabeçalho */}
<View style={styles.header}>
<View>
<Text style={styles.headerSub}>Seu estoque</Text>
<Text style={styles.headerTitle}>Inventário</Text>
</View>
<TouchableOpacity
style={styles.filterBtn}
onPress={() => router.push('/(app)/inventory/filters')}
>
<Ionicons name="options-outline" size={20} color={colors.textPrimary} />
</TouchableOpacity>
</View>
{/* Busca */}
<View style={styles.searchRow}>
<Ionicons name="search-outline" size={18} color={colors.textSecondary} style={styles.searchIcon} />
<TextInput
style={styles.searchInput}
placeholder="Buscar por marca, material..."
placeholderTextColor={colors.textSecondary}
value={search}
onChangeText={setSearch}
/>
</View>
{/* Chips de material */}
<View style={styles.chipsRow}>
{TABS.map((tab) => (
<TouchableOpacity
key={tab}
onPress={() => setActiveTab(tab)}
style={[styles.chip, activeTab === tab && styles.chipActive]}
>
<Text style={[styles.chipText, activeTab === tab && styles.chipTextActive]}>
{tab}
</Text>
</TouchableOpacity>
))}
</View>
{/* Contagem */}
<View style={styles.countRow}>
<Text style={styles.countText}>{filtered.length} FILAMENTOS</Text>
<TouchableOpacity style={styles.sortBtn}>
<Ionicons name="swap-vertical-outline" size={14} color={colors.textSecondary} />
<Text style={styles.sortText}>Ordenar</Text>
</TouchableOpacity>
</View>
<FlatList
data={filtered}
keyExtractor={(item) => item.id}
renderItem={renderItem}
contentContainerStyle={styles.list}
ItemSeparatorComponent={() => <View style={styles.separator} />}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl refreshing={isRefreshing} onRefresh={onRefresh} tintColor={colors.accent} />
}
ListEmptyComponent={
<View style={styles.empty}>
<Ionicons name="layers-outline" size={40} color={colors.textSecondary} />
<Text style={styles.emptyText}>Nenhum filamento encontrado.</Text>
</View>
}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safe: { flex: 1, backgroundColor: colors.bgBase },
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', paddingHorizontal: spacing[5], paddingTop: spacing[4] },
headerSub: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.sm, color: colors.textSecondary },
headerTitle: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize['2xl'], fontWeight: typography.fontWeight.bold, color: colors.textPrimary },
filterBtn: { width: 40, height: 40, borderRadius: radius.md, backgroundColor: colors.bgSurface, alignItems: 'center', justifyContent: 'center', borderWidth: 1, borderColor: colors.border },
searchRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.bgSurface, borderRadius: radius.md, marginHorizontal: spacing[5], marginTop: spacing[4], paddingHorizontal: spacing[4], borderWidth: 1, borderColor: colors.border, height: 48 },
searchIcon: { marginRight: spacing[2] },
searchInput: { flex: 1, fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.base, color: colors.textPrimary },
chipsRow: { flexDirection: 'row', gap: spacing[2], paddingHorizontal: spacing[5], marginTop: spacing[3], flexWrap: 'wrap' },
chip: { paddingHorizontal: spacing[4], paddingVertical: spacing[2], borderRadius: radius.full, backgroundColor: colors.bgSurface, borderWidth: 1, borderColor: colors.border },
chipActive: { backgroundColor: colors.accent, borderColor: colors.accent },
chipText: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.sm, fontWeight: typography.fontWeight.medium, color: colors.textSecondary },
chipTextActive: { color: colors.bgBase },
countRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: spacing[5], marginTop: spacing[4], marginBottom: spacing[2] },
countText: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.xs, fontWeight: typography.fontWeight.bold, color: colors.textSecondary, letterSpacing: 0.8 },
sortBtn: { flexDirection: 'row', alignItems: 'center', gap: 4 },
sortText: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.sm, color: colors.textSecondary },
list: { paddingHorizontal: spacing[5], paddingBottom: spacing[10] },
separator: { height: spacing[2] },
empty: { alignItems: 'center', gap: spacing[3], paddingTop: spacing[12] },
emptyText: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.base, color: colors.textSecondary },
});