805 lines
23 KiB
TypeScript
805 lines
23 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
FlatList,
|
|
TextInput,
|
|
TouchableOpacity,
|
|
RefreshControl,
|
|
Alert,
|
|
Modal,
|
|
ScrollView,
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
Image,
|
|
} from 'react-native';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { colors, spacing, fontSize, borderRadius } from '../constants/theme';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
import api from '../services/api';
|
|
import { Product, Category } from '../types';
|
|
|
|
interface ProductFormData {
|
|
name: string;
|
|
description: string;
|
|
sku: string;
|
|
barcode: string;
|
|
price: string;
|
|
cost: string;
|
|
currentStock: string;
|
|
minStock: string;
|
|
categoryId: string;
|
|
}
|
|
|
|
const initialFormData: ProductFormData = {
|
|
name: '',
|
|
description: '',
|
|
sku: '',
|
|
barcode: '',
|
|
price: '',
|
|
cost: '',
|
|
currentStock: '0',
|
|
minStock: '5',
|
|
categoryId: '',
|
|
};
|
|
|
|
export default function ProductsScreen({ navigation }: any) {
|
|
const { user } = useAuth();
|
|
const [products, setProducts] = useState<Product[]>([]);
|
|
const [categories, setCategories] = useState<Category[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
|
const [modalVisible, setModalVisible] = useState(false);
|
|
const [editingProduct, setEditingProduct] = useState<Product | null>(null);
|
|
const [formData, setFormData] = useState<ProductFormData>(initialFormData);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
const fetchProducts = useCallback(async () => {
|
|
try {
|
|
const params: any = {};
|
|
if (searchQuery) params.search = searchQuery;
|
|
if (selectedCategory) params.categoryId = selectedCategory;
|
|
|
|
const response = await api.get('/products', { params });
|
|
setProducts(response.data.data || response.data);
|
|
} catch (error) {
|
|
console.error('Error fetching products:', error);
|
|
}
|
|
}, [searchQuery, selectedCategory]);
|
|
|
|
const fetchCategories = async () => {
|
|
try {
|
|
const response = await api.get('/categories');
|
|
setCategories(response.data.data || response.data);
|
|
} catch (error) {
|
|
console.error('Error fetching categories:', error);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const loadData = async () => {
|
|
setLoading(true);
|
|
await Promise.all([fetchProducts(), fetchCategories()]);
|
|
setLoading(false);
|
|
};
|
|
loadData();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const delayedSearch = setTimeout(() => {
|
|
fetchProducts();
|
|
}, 300);
|
|
return () => clearTimeout(delayedSearch);
|
|
}, [searchQuery, selectedCategory]);
|
|
|
|
const onRefresh = async () => {
|
|
setRefreshing(true);
|
|
await fetchProducts();
|
|
setRefreshing(false);
|
|
};
|
|
|
|
const openCreateModal = () => {
|
|
setEditingProduct(null);
|
|
setFormData(initialFormData);
|
|
setModalVisible(true);
|
|
};
|
|
|
|
const openEditModal = (product: Product) => {
|
|
setEditingProduct(product);
|
|
setFormData({
|
|
name: product.name,
|
|
description: product.description || '',
|
|
sku: product.sku || '',
|
|
barcode: product.barcode || '',
|
|
price: product.price.toString(),
|
|
cost: product.cost?.toString() || '',
|
|
currentStock: product.currentStock?.toString() || '0',
|
|
minStock: product.minStock?.toString() || '5',
|
|
categoryId: product.categoryId || '',
|
|
});
|
|
setModalVisible(true);
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
if (!formData.name.trim()) {
|
|
Alert.alert('Error', 'El nombre del producto es requerido');
|
|
return;
|
|
}
|
|
if (!formData.price || parseFloat(formData.price) <= 0) {
|
|
Alert.alert('Error', 'El precio debe ser mayor a 0');
|
|
return;
|
|
}
|
|
|
|
setSaving(true);
|
|
try {
|
|
const payload = {
|
|
name: formData.name.trim(),
|
|
description: formData.description.trim() || null,
|
|
sku: formData.sku.trim() || null,
|
|
barcode: formData.barcode.trim() || null,
|
|
price: parseFloat(formData.price),
|
|
cost: formData.cost ? parseFloat(formData.cost) : null,
|
|
currentStock: parseInt(formData.currentStock) || 0,
|
|
minStock: parseInt(formData.minStock) || 5,
|
|
categoryId: formData.categoryId || null,
|
|
};
|
|
|
|
if (editingProduct) {
|
|
await api.put(`/products/${editingProduct.id}`, payload);
|
|
Alert.alert('Exito', 'Producto actualizado correctamente');
|
|
} else {
|
|
await api.post('/products', payload);
|
|
Alert.alert('Exito', 'Producto creado correctamente');
|
|
}
|
|
|
|
setModalVisible(false);
|
|
fetchProducts();
|
|
} catch (error: any) {
|
|
Alert.alert('Error', error.response?.data?.message || 'No se pudo guardar el producto');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleDelete = (product: Product) => {
|
|
Alert.alert(
|
|
'Eliminar Producto',
|
|
`¿Estas seguro de eliminar "${product.name}"?`,
|
|
[
|
|
{ text: 'Cancelar', style: 'cancel' },
|
|
{
|
|
text: 'Eliminar',
|
|
style: 'destructive',
|
|
onPress: async () => {
|
|
try {
|
|
await api.delete(`/products/${product.id}`);
|
|
fetchProducts();
|
|
} catch (error: any) {
|
|
Alert.alert('Error', 'No se pudo eliminar el producto');
|
|
}
|
|
},
|
|
},
|
|
]
|
|
);
|
|
};
|
|
|
|
const toggleFavorite = async (product: Product) => {
|
|
try {
|
|
await api.patch(`/products/${product.id}/toggle-favorite`);
|
|
fetchProducts();
|
|
} catch (error) {
|
|
console.error('Error toggling favorite:', error);
|
|
}
|
|
};
|
|
|
|
const renderProduct = ({ item }: { item: Product }) => {
|
|
const isLowStock = item.currentStock <= (item.minStock || 5);
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
style={styles.productCard}
|
|
onPress={() => openEditModal(item)}
|
|
onLongPress={() => handleDelete(item)}
|
|
>
|
|
<View style={styles.productImageContainer}>
|
|
{item.imageUrl ? (
|
|
<Image source={{ uri: item.imageUrl }} style={styles.productImage} />
|
|
) : (
|
|
<View style={styles.productImagePlaceholder}>
|
|
<Text style={styles.productImageText}>
|
|
{item.name.charAt(0).toUpperCase()}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
{item.isFavorite && (
|
|
<View style={styles.favoriteIndicator}>
|
|
<Text>★</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
<View style={styles.productInfo}>
|
|
<Text style={styles.productName} numberOfLines={2}>{item.name}</Text>
|
|
<Text style={styles.productSku}>{item.sku || 'Sin SKU'}</Text>
|
|
|
|
<View style={styles.productFooter}>
|
|
<Text style={styles.productPrice}>${item.price.toFixed(2)}</Text>
|
|
<View style={[
|
|
styles.stockBadge,
|
|
isLowStock ? styles.stockBadgeLow : styles.stockBadgeOk
|
|
]}>
|
|
<Text style={[
|
|
styles.stockText,
|
|
isLowStock ? styles.stockTextLow : styles.stockTextOk
|
|
]}>
|
|
{item.currentStock} uds
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
<TouchableOpacity
|
|
style={styles.favoriteButton}
|
|
onPress={() => toggleFavorite(item)}
|
|
>
|
|
<Text style={{ fontSize: 20 }}>{item.isFavorite ? '★' : '☆'}</Text>
|
|
</TouchableOpacity>
|
|
</TouchableOpacity>
|
|
);
|
|
};
|
|
|
|
const renderCategoryFilter = () => (
|
|
<ScrollView
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
style={styles.categoryFilter}
|
|
contentContainerStyle={styles.categoryFilterContent}
|
|
>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.categoryChip,
|
|
!selectedCategory && styles.categoryChipActive
|
|
]}
|
|
onPress={() => setSelectedCategory(null)}
|
|
>
|
|
<Text style={[
|
|
styles.categoryChipText,
|
|
!selectedCategory && styles.categoryChipTextActive
|
|
]}>
|
|
Todos
|
|
</Text>
|
|
</TouchableOpacity>
|
|
{categories.map((cat) => (
|
|
<TouchableOpacity
|
|
key={cat.id}
|
|
style={[
|
|
styles.categoryChip,
|
|
selectedCategory === cat.id && styles.categoryChipActive
|
|
]}
|
|
onPress={() => setSelectedCategory(cat.id)}
|
|
>
|
|
<Text style={[
|
|
styles.categoryChipText,
|
|
selectedCategory === cat.id && styles.categoryChipTextActive
|
|
]}>
|
|
{cat.name}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</ScrollView>
|
|
);
|
|
|
|
const renderFormModal = () => (
|
|
<Modal
|
|
visible={modalVisible}
|
|
animationType="slide"
|
|
presentationStyle="pageSheet"
|
|
onRequestClose={() => setModalVisible(false)}
|
|
>
|
|
<SafeAreaView style={styles.modalContainer}>
|
|
<View style={styles.modalHeader}>
|
|
<TouchableOpacity onPress={() => setModalVisible(false)}>
|
|
<Text style={styles.modalCancel}>Cancelar</Text>
|
|
</TouchableOpacity>
|
|
<Text style={styles.modalTitle}>
|
|
{editingProduct ? 'Editar Producto' : 'Nuevo Producto'}
|
|
</Text>
|
|
<TouchableOpacity onPress={handleSave} disabled={saving}>
|
|
<Text style={[styles.modalSave, saving && styles.modalSaveDisabled]}>
|
|
{saving ? 'Guardando...' : 'Guardar'}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
<KeyboardAvoidingView
|
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
style={{ flex: 1 }}
|
|
>
|
|
<ScrollView style={styles.formContainer}>
|
|
<View style={styles.formGroup}>
|
|
<Text style={styles.formLabel}>Nombre *</Text>
|
|
<TextInput
|
|
style={styles.formInput}
|
|
value={formData.name}
|
|
onChangeText={(text) => setFormData({ ...formData, name: text })}
|
|
placeholder="Nombre del producto"
|
|
placeholderTextColor={colors.textMuted}
|
|
/>
|
|
</View>
|
|
|
|
<View style={styles.formGroup}>
|
|
<Text style={styles.formLabel}>Descripcion</Text>
|
|
<TextInput
|
|
style={[styles.formInput, styles.formTextArea]}
|
|
value={formData.description}
|
|
onChangeText={(text) => setFormData({ ...formData, description: text })}
|
|
placeholder="Descripcion del producto"
|
|
placeholderTextColor={colors.textMuted}
|
|
multiline
|
|
numberOfLines={3}
|
|
/>
|
|
</View>
|
|
|
|
<View style={styles.formRow}>
|
|
<View style={[styles.formGroup, { flex: 1, marginRight: spacing.sm }]}>
|
|
<Text style={styles.formLabel}>SKU</Text>
|
|
<TextInput
|
|
style={styles.formInput}
|
|
value={formData.sku}
|
|
onChangeText={(text) => setFormData({ ...formData, sku: text })}
|
|
placeholder="SKU-001"
|
|
placeholderTextColor={colors.textMuted}
|
|
/>
|
|
</View>
|
|
<View style={[styles.formGroup, { flex: 1 }]}>
|
|
<Text style={styles.formLabel}>Codigo de Barras</Text>
|
|
<TextInput
|
|
style={styles.formInput}
|
|
value={formData.barcode}
|
|
onChangeText={(text) => setFormData({ ...formData, barcode: text })}
|
|
placeholder="7501234567890"
|
|
placeholderTextColor={colors.textMuted}
|
|
keyboardType="numeric"
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.formRow}>
|
|
<View style={[styles.formGroup, { flex: 1, marginRight: spacing.sm }]}>
|
|
<Text style={styles.formLabel}>Precio de Venta *</Text>
|
|
<TextInput
|
|
style={styles.formInput}
|
|
value={formData.price}
|
|
onChangeText={(text) => setFormData({ ...formData, price: text })}
|
|
placeholder="0.00"
|
|
placeholderTextColor={colors.textMuted}
|
|
keyboardType="decimal-pad"
|
|
/>
|
|
</View>
|
|
<View style={[styles.formGroup, { flex: 1 }]}>
|
|
<Text style={styles.formLabel}>Costo</Text>
|
|
<TextInput
|
|
style={styles.formInput}
|
|
value={formData.cost}
|
|
onChangeText={(text) => setFormData({ ...formData, cost: text })}
|
|
placeholder="0.00"
|
|
placeholderTextColor={colors.textMuted}
|
|
keyboardType="decimal-pad"
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.formRow}>
|
|
<View style={[styles.formGroup, { flex: 1, marginRight: spacing.sm }]}>
|
|
<Text style={styles.formLabel}>Stock Actual</Text>
|
|
<TextInput
|
|
style={styles.formInput}
|
|
value={formData.currentStock}
|
|
onChangeText={(text) => setFormData({ ...formData, currentStock: text })}
|
|
placeholder="0"
|
|
placeholderTextColor={colors.textMuted}
|
|
keyboardType="number-pad"
|
|
/>
|
|
</View>
|
|
<View style={[styles.formGroup, { flex: 1 }]}>
|
|
<Text style={styles.formLabel}>Stock Minimo</Text>
|
|
<TextInput
|
|
style={styles.formInput}
|
|
value={formData.minStock}
|
|
onChangeText={(text) => setFormData({ ...formData, minStock: text })}
|
|
placeholder="5"
|
|
placeholderTextColor={colors.textMuted}
|
|
keyboardType="number-pad"
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.formGroup}>
|
|
<Text style={styles.formLabel}>Categoria</Text>
|
|
<ScrollView
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
style={styles.categorySelector}
|
|
>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.categorySelectorItem,
|
|
!formData.categoryId && styles.categorySelectorItemActive
|
|
]}
|
|
onPress={() => setFormData({ ...formData, categoryId: '' })}
|
|
>
|
|
<Text style={[
|
|
styles.categorySelectorText,
|
|
!formData.categoryId && styles.categorySelectorTextActive
|
|
]}>
|
|
Sin categoria
|
|
</Text>
|
|
</TouchableOpacity>
|
|
{categories.map((cat) => (
|
|
<TouchableOpacity
|
|
key={cat.id}
|
|
style={[
|
|
styles.categorySelectorItem,
|
|
formData.categoryId === cat.id && styles.categorySelectorItemActive
|
|
]}
|
|
onPress={() => setFormData({ ...formData, categoryId: cat.id })}
|
|
>
|
|
<Text style={[
|
|
styles.categorySelectorText,
|
|
formData.categoryId === cat.id && styles.categorySelectorTextActive
|
|
]}>
|
|
{cat.name}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</ScrollView>
|
|
</View>
|
|
</ScrollView>
|
|
</KeyboardAvoidingView>
|
|
</SafeAreaView>
|
|
</Modal>
|
|
);
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container}>
|
|
{/* Header */}
|
|
<View style={styles.header}>
|
|
<Text style={styles.title}>Productos</Text>
|
|
<TouchableOpacity style={styles.addButton} onPress={openCreateModal}>
|
|
<Text style={styles.addButtonText}>+ Nuevo</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Search */}
|
|
<View style={styles.searchContainer}>
|
|
<TextInput
|
|
style={styles.searchInput}
|
|
placeholder="Buscar productos..."
|
|
placeholderTextColor={colors.textMuted}
|
|
value={searchQuery}
|
|
onChangeText={setSearchQuery}
|
|
/>
|
|
</View>
|
|
|
|
{/* Category Filter */}
|
|
{renderCategoryFilter()}
|
|
|
|
{/* Products List */}
|
|
{loading ? (
|
|
<View style={styles.centerContainer}>
|
|
<Text style={styles.loadingText}>Cargando productos...</Text>
|
|
</View>
|
|
) : products.length === 0 ? (
|
|
<View style={styles.centerContainer}>
|
|
<Text style={styles.emptyIcon}>📦</Text>
|
|
<Text style={styles.emptyText}>No hay productos</Text>
|
|
<TouchableOpacity style={styles.emptyButton} onPress={openCreateModal}>
|
|
<Text style={styles.emptyButtonText}>Crear primer producto</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
) : (
|
|
<FlatList
|
|
data={products}
|
|
keyExtractor={(item) => item.id}
|
|
renderItem={renderProduct}
|
|
contentContainerStyle={styles.listContent}
|
|
numColumns={2}
|
|
columnWrapperStyle={styles.columnWrapper}
|
|
refreshControl={
|
|
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
|
}
|
|
/>
|
|
)}
|
|
|
|
{renderFormModal()}
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: colors.background,
|
|
},
|
|
header: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
paddingHorizontal: spacing.lg,
|
|
paddingVertical: spacing.md,
|
|
},
|
|
title: {
|
|
fontSize: fontSize.xxl,
|
|
fontWeight: 'bold',
|
|
color: colors.text,
|
|
},
|
|
addButton: {
|
|
backgroundColor: colors.primary,
|
|
paddingHorizontal: spacing.md,
|
|
paddingVertical: spacing.sm,
|
|
borderRadius: borderRadius.md,
|
|
},
|
|
addButtonText: {
|
|
color: '#fff',
|
|
fontWeight: '600',
|
|
fontSize: fontSize.sm,
|
|
},
|
|
searchContainer: {
|
|
paddingHorizontal: spacing.lg,
|
|
marginBottom: spacing.sm,
|
|
},
|
|
searchInput: {
|
|
backgroundColor: colors.surface,
|
|
borderRadius: borderRadius.md,
|
|
paddingHorizontal: spacing.md,
|
|
paddingVertical: spacing.sm,
|
|
fontSize: fontSize.md,
|
|
color: colors.text,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
},
|
|
categoryFilter: {
|
|
maxHeight: 50,
|
|
marginBottom: spacing.sm,
|
|
},
|
|
categoryFilterContent: {
|
|
paddingHorizontal: spacing.lg,
|
|
gap: spacing.sm,
|
|
},
|
|
categoryChip: {
|
|
backgroundColor: colors.surface,
|
|
paddingHorizontal: spacing.md,
|
|
paddingVertical: spacing.xs,
|
|
borderRadius: borderRadius.full,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
marginRight: spacing.sm,
|
|
},
|
|
categoryChipActive: {
|
|
backgroundColor: colors.primary,
|
|
borderColor: colors.primary,
|
|
},
|
|
categoryChipText: {
|
|
fontSize: fontSize.sm,
|
|
color: colors.textSecondary,
|
|
},
|
|
categoryChipTextActive: {
|
|
color: '#fff',
|
|
},
|
|
listContent: {
|
|
paddingHorizontal: spacing.md,
|
|
paddingBottom: spacing.xl,
|
|
},
|
|
columnWrapper: {
|
|
justifyContent: 'space-between',
|
|
},
|
|
productCard: {
|
|
width: '48%',
|
|
backgroundColor: colors.surface,
|
|
borderRadius: borderRadius.lg,
|
|
marginBottom: spacing.md,
|
|
overflow: 'hidden',
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
},
|
|
productImageContainer: {
|
|
position: 'relative',
|
|
height: 120,
|
|
backgroundColor: colors.background,
|
|
},
|
|
productImage: {
|
|
width: '100%',
|
|
height: '100%',
|
|
resizeMode: 'cover',
|
|
},
|
|
productImagePlaceholder: {
|
|
width: '100%',
|
|
height: '100%',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
backgroundColor: colors.primaryLight,
|
|
},
|
|
productImageText: {
|
|
fontSize: 40,
|
|
fontWeight: 'bold',
|
|
color: colors.primary,
|
|
},
|
|
favoriteIndicator: {
|
|
position: 'absolute',
|
|
top: spacing.xs,
|
|
left: spacing.xs,
|
|
backgroundColor: colors.warning,
|
|
borderRadius: borderRadius.full,
|
|
padding: 4,
|
|
},
|
|
productInfo: {
|
|
padding: spacing.sm,
|
|
},
|
|
productName: {
|
|
fontSize: fontSize.md,
|
|
fontWeight: '600',
|
|
color: colors.text,
|
|
marginBottom: spacing.xs,
|
|
},
|
|
productSku: {
|
|
fontSize: fontSize.xs,
|
|
color: colors.textMuted,
|
|
marginBottom: spacing.sm,
|
|
},
|
|
productFooter: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
},
|
|
productPrice: {
|
|
fontSize: fontSize.lg,
|
|
fontWeight: 'bold',
|
|
color: colors.primary,
|
|
},
|
|
stockBadge: {
|
|
paddingHorizontal: spacing.sm,
|
|
paddingVertical: 2,
|
|
borderRadius: borderRadius.sm,
|
|
},
|
|
stockBadgeOk: {
|
|
backgroundColor: colors.successLight,
|
|
},
|
|
stockBadgeLow: {
|
|
backgroundColor: colors.errorLight,
|
|
},
|
|
stockText: {
|
|
fontSize: fontSize.xs,
|
|
fontWeight: '600',
|
|
},
|
|
stockTextOk: {
|
|
color: colors.success,
|
|
},
|
|
stockTextLow: {
|
|
color: colors.error,
|
|
},
|
|
favoriteButton: {
|
|
position: 'absolute',
|
|
top: spacing.xs,
|
|
right: spacing.xs,
|
|
backgroundColor: 'rgba(255,255,255,0.9)',
|
|
borderRadius: borderRadius.full,
|
|
padding: spacing.xs,
|
|
},
|
|
centerContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
padding: spacing.xl,
|
|
},
|
|
loadingText: {
|
|
fontSize: fontSize.md,
|
|
color: colors.textMuted,
|
|
},
|
|
emptyIcon: {
|
|
fontSize: 60,
|
|
marginBottom: spacing.md,
|
|
},
|
|
emptyText: {
|
|
fontSize: fontSize.lg,
|
|
color: colors.textSecondary,
|
|
marginBottom: spacing.lg,
|
|
},
|
|
emptyButton: {
|
|
backgroundColor: colors.primary,
|
|
paddingHorizontal: spacing.lg,
|
|
paddingVertical: spacing.md,
|
|
borderRadius: borderRadius.md,
|
|
},
|
|
emptyButtonText: {
|
|
color: '#fff',
|
|
fontWeight: '600',
|
|
},
|
|
// Modal styles
|
|
modalContainer: {
|
|
flex: 1,
|
|
backgroundColor: colors.background,
|
|
},
|
|
modalHeader: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
paddingHorizontal: spacing.lg,
|
|
paddingVertical: spacing.md,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: colors.border,
|
|
},
|
|
modalCancel: {
|
|
fontSize: fontSize.md,
|
|
color: colors.textSecondary,
|
|
},
|
|
modalTitle: {
|
|
fontSize: fontSize.lg,
|
|
fontWeight: 'bold',
|
|
color: colors.text,
|
|
},
|
|
modalSave: {
|
|
fontSize: fontSize.md,
|
|
color: colors.primary,
|
|
fontWeight: '600',
|
|
},
|
|
modalSaveDisabled: {
|
|
opacity: 0.5,
|
|
},
|
|
formContainer: {
|
|
flex: 1,
|
|
padding: spacing.lg,
|
|
},
|
|
formGroup: {
|
|
marginBottom: spacing.md,
|
|
},
|
|
formRow: {
|
|
flexDirection: 'row',
|
|
},
|
|
formLabel: {
|
|
fontSize: fontSize.sm,
|
|
fontWeight: '600',
|
|
color: colors.textSecondary,
|
|
marginBottom: spacing.xs,
|
|
},
|
|
formInput: {
|
|
backgroundColor: colors.surface,
|
|
borderRadius: borderRadius.md,
|
|
paddingHorizontal: spacing.md,
|
|
paddingVertical: spacing.sm,
|
|
fontSize: fontSize.md,
|
|
color: colors.text,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
},
|
|
formTextArea: {
|
|
minHeight: 80,
|
|
textAlignVertical: 'top',
|
|
},
|
|
categorySelector: {
|
|
flexDirection: 'row',
|
|
},
|
|
categorySelectorItem: {
|
|
backgroundColor: colors.surface,
|
|
paddingHorizontal: spacing.md,
|
|
paddingVertical: spacing.sm,
|
|
borderRadius: borderRadius.md,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
marginRight: spacing.sm,
|
|
},
|
|
categorySelectorItemActive: {
|
|
backgroundColor: colors.primary,
|
|
borderColor: colors.primary,
|
|
},
|
|
categorySelectorText: {
|
|
fontSize: fontSize.sm,
|
|
color: colors.textSecondary,
|
|
},
|
|
categorySelectorTextActive: {
|
|
color: '#fff',
|
|
},
|
|
});
|