Files
MeowSpool/mobile/app/(app)/(tabs)/inventory.tsx
T
Felipe d7fb768d3b feat: add domain models and repositories for user, spool presets, and filaments
- Introduced SpoolPreset and User domain models with necessary DTOs and utility functions.
- Created AuthRepository, FilamentRepository, and SpoolPresetRepository interfaces for authentication and data management.
- Implemented UI components for filament display, including ColorSwatch, FilamentCard, and StockBar.
- Developed layout components such as Header and Screen for consistent app structure.
- Added reusable UI components like Badge, Button, Card, and Input for better user interaction.
- Established global constants and theme settings for consistent styling across the application.
- Implemented utility functions for filament calculations and formatting.
- Created Zustand stores for managing authentication, filament, and preset states.
- Configured TypeScript settings for improved development experience.
2026-03-14 10:36:13 -03:00

133 lines
6.3 KiB
TypeScript

import React, { useState } from 'react';
import {
View, Text, FlatList, TouchableOpacity, StyleSheet, TextInput,
} 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 { 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 } = useFilamentStore();
const [search, setSearch] = useState('');
const [activeTab, setActiveTab] = useState<string>('Todos');
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;
});
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}
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 },
});