- 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.
152 lines
7.0 KiB
TypeScript
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 },
|
|
});
|