@nextsparkjs/mobile 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +339 -0
- package/dist/api/client.d.ts +102 -0
- package/dist/api/client.js +189 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/client.types.d.ts +39 -0
- package/dist/api/client.types.js +12 -0
- package/dist/api/client.types.js.map +1 -0
- package/dist/api/core/auth.d.ts +26 -0
- package/dist/api/core/auth.js +52 -0
- package/dist/api/core/auth.js.map +1 -0
- package/dist/api/core/index.d.ts +4 -0
- package/dist/api/core/index.js +5 -0
- package/dist/api/core/index.js.map +1 -0
- package/dist/api/core/teams.d.ts +20 -0
- package/dist/api/core/teams.js +19 -0
- package/dist/api/core/teams.js.map +1 -0
- package/dist/api/core/types.d.ts +58 -0
- package/dist/api/core/types.js +1 -0
- package/dist/api/core/types.js.map +1 -0
- package/dist/api/core/users.d.ts +43 -0
- package/dist/api/core/users.js +41 -0
- package/dist/api/core/users.js.map +1 -0
- package/dist/api/entities/factory.d.ts +43 -0
- package/dist/api/entities/factory.js +31 -0
- package/dist/api/entities/factory.js.map +1 -0
- package/dist/api/entities/index.d.ts +3 -0
- package/dist/api/entities/index.js +3 -0
- package/dist/api/entities/index.js.map +1 -0
- package/dist/api/entities/types.d.ts +32 -0
- package/dist/api/entities/types.js +1 -0
- package/dist/api/entities/types.js.map +1 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.js +15 -0
- package/dist/api/index.js.map +1 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +5 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/alert.d.ts +34 -0
- package/dist/lib/alert.js +73 -0
- package/dist/lib/alert.js.map +1 -0
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.js +10 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/storage.d.ts +1 -0
- package/dist/lib/storage.js +29 -0
- package/dist/lib/storage.js.map +1 -0
- package/dist/providers/AuthProvider.d.ts +21 -0
- package/dist/providers/AuthProvider.js +113 -0
- package/dist/providers/AuthProvider.js.map +1 -0
- package/dist/providers/QueryProvider.d.ts +11 -0
- package/dist/providers/QueryProvider.js +23 -0
- package/dist/providers/QueryProvider.js.map +1 -0
- package/dist/providers/index.d.ts +6 -0
- package/dist/providers/index.js +9 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/storage-BaRppHUz.d.ts +22 -0
- package/package.json +99 -0
- package/templates/app/(app)/_layout.tsx +216 -0
- package/templates/app/(app)/customer/[id].tsx +68 -0
- package/templates/app/(app)/customer/create.tsx +24 -0
- package/templates/app/(app)/customers.tsx +164 -0
- package/templates/app/(app)/index.tsx +310 -0
- package/templates/app/(app)/notifications.tsx +242 -0
- package/templates/app/(app)/profile.tsx +254 -0
- package/templates/app/(app)/settings.tsx +241 -0
- package/templates/app/(app)/task/[id].tsx +70 -0
- package/templates/app/(app)/task/create.tsx +24 -0
- package/templates/app/(app)/tasks.tsx +164 -0
- package/templates/app/_layout.tsx +54 -0
- package/templates/app/index.tsx +35 -0
- package/templates/app/login.tsx +179 -0
- package/templates/app.config.ts +39 -0
- package/templates/babel.config.js +9 -0
- package/templates/eas.json +18 -0
- package/templates/jest.config.js +12 -0
- package/templates/metro.config.js +23 -0
- package/templates/package.json.template +52 -0
- package/templates/src/components/entities/customers/CustomerCard.tsx +59 -0
- package/templates/src/components/entities/customers/CustomerForm.tsx +194 -0
- package/templates/src/components/entities/customers/index.ts +6 -0
- package/templates/src/components/entities/index.ts +9 -0
- package/templates/src/components/entities/tasks/TaskCard.tsx +89 -0
- package/templates/src/components/entities/tasks/TaskForm.tsx +231 -0
- package/templates/src/components/entities/tasks/index.ts +6 -0
- package/templates/src/components/features/index.ts +6 -0
- package/templates/src/components/navigation/BottomTabBar.tsx +80 -0
- package/templates/src/components/navigation/CreateSheet.tsx +108 -0
- package/templates/src/components/navigation/MoreSheet.tsx +403 -0
- package/templates/src/components/navigation/TopBar.tsx +74 -0
- package/templates/src/components/navigation/index.ts +8 -0
- package/templates/src/components/ui/index.ts +89 -0
- package/templates/src/components/ui/text.tsx +64 -0
- package/templates/src/config/api.config.ts +26 -0
- package/templates/src/config/app.config.ts +15 -0
- package/templates/src/config/hooks.ts +58 -0
- package/templates/src/config/permissions.config.ts +119 -0
- package/templates/src/constants/colors.ts +55 -0
- package/templates/src/data/notifications.mock.json +100 -0
- package/templates/src/entities/customers/api.ts +10 -0
- package/templates/src/entities/customers/constants.internal.ts +6 -0
- package/templates/src/entities/customers/constants.ts +14 -0
- package/templates/src/entities/customers/index.ts +9 -0
- package/templates/src/entities/customers/mutations.ts +58 -0
- package/templates/src/entities/customers/queries.ts +40 -0
- package/templates/src/entities/customers/types.ts +43 -0
- package/templates/src/entities/index.ts +8 -0
- package/templates/src/entities/tasks/api.ts +10 -0
- package/templates/src/entities/tasks/constants.internal.ts +6 -0
- package/templates/src/entities/tasks/constants.ts +39 -0
- package/templates/src/entities/tasks/index.ts +9 -0
- package/templates/src/entities/tasks/mutations.ts +108 -0
- package/templates/src/entities/tasks/queries.ts +42 -0
- package/templates/src/entities/tasks/types.ts +52 -0
- package/templates/src/hooks/useCustomers.ts +17 -0
- package/templates/src/hooks/useTasks.ts +18 -0
- package/templates/src/lib/utils.ts +10 -0
- package/templates/src/styles/globals.css +103 -0
- package/templates/src/types/index.ts +45 -0
- package/templates/tailwind.config.js +108 -0
- package/templates/tsconfig.json +15 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login Screen
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useState } from 'react'
|
|
6
|
+
import {
|
|
7
|
+
View,
|
|
8
|
+
Text,
|
|
9
|
+
TextInput,
|
|
10
|
+
StyleSheet,
|
|
11
|
+
KeyboardAvoidingView,
|
|
12
|
+
Platform,
|
|
13
|
+
} from 'react-native'
|
|
14
|
+
import { SafeAreaView } from 'react-native-safe-area-context'
|
|
15
|
+
import { router } from 'expo-router'
|
|
16
|
+
import { useAuth } from '@nextsparkjs/mobile'
|
|
17
|
+
import { Colors } from '@/src/constants/colors'
|
|
18
|
+
import { Button } from '@/src/components/ui'
|
|
19
|
+
|
|
20
|
+
export default function LoginScreen() {
|
|
21
|
+
const { login, isLoading } = useAuth()
|
|
22
|
+
const [email, setEmail] = useState('')
|
|
23
|
+
const [password, setPassword] = useState('')
|
|
24
|
+
const [error, setError] = useState<string | null>(null)
|
|
25
|
+
|
|
26
|
+
const handleLogin = async () => {
|
|
27
|
+
setError(null)
|
|
28
|
+
|
|
29
|
+
if (!email.trim() || !password.trim()) {
|
|
30
|
+
setError('Por favor ingresa tu email y contraseña')
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
await login(email.trim(), password)
|
|
36
|
+
router.replace('/(app)')
|
|
37
|
+
} catch (err) {
|
|
38
|
+
setError(err instanceof Error ? err.message : 'Error al iniciar sesión. Por favor intenta de nuevo.')
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<SafeAreaView style={styles.safeArea}>
|
|
44
|
+
<KeyboardAvoidingView
|
|
45
|
+
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
46
|
+
style={styles.container}
|
|
47
|
+
>
|
|
48
|
+
<View style={styles.content}>
|
|
49
|
+
{/* Header */}
|
|
50
|
+
<View style={styles.header}>
|
|
51
|
+
<Text style={styles.logo}>NextSpark</Text>
|
|
52
|
+
<Text style={styles.subtitle}>Inicia sesión en tu cuenta</Text>
|
|
53
|
+
</View>
|
|
54
|
+
|
|
55
|
+
{/* Error */}
|
|
56
|
+
{error && (
|
|
57
|
+
<View style={styles.errorContainer}>
|
|
58
|
+
<Text style={styles.errorText}>{error}</Text>
|
|
59
|
+
</View>
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
{/* Form */}
|
|
63
|
+
<View style={styles.form}>
|
|
64
|
+
<View style={styles.inputContainer}>
|
|
65
|
+
<Text style={styles.label}>Email</Text>
|
|
66
|
+
<TextInput
|
|
67
|
+
style={styles.input}
|
|
68
|
+
value={email}
|
|
69
|
+
onChangeText={setEmail}
|
|
70
|
+
placeholder="Ingresa tu email"
|
|
71
|
+
placeholderTextColor={Colors.foregroundMuted}
|
|
72
|
+
keyboardType="email-address"
|
|
73
|
+
autoCapitalize="none"
|
|
74
|
+
autoCorrect={false}
|
|
75
|
+
editable={!isLoading}
|
|
76
|
+
/>
|
|
77
|
+
</View>
|
|
78
|
+
|
|
79
|
+
<View style={styles.inputContainer}>
|
|
80
|
+
<Text style={styles.label}>Contraseña</Text>
|
|
81
|
+
<TextInput
|
|
82
|
+
style={styles.input}
|
|
83
|
+
value={password}
|
|
84
|
+
onChangeText={setPassword}
|
|
85
|
+
placeholder="Ingresa tu contraseña"
|
|
86
|
+
placeholderTextColor={Colors.foregroundMuted}
|
|
87
|
+
secureTextEntry
|
|
88
|
+
editable={!isLoading}
|
|
89
|
+
/>
|
|
90
|
+
</View>
|
|
91
|
+
|
|
92
|
+
<Button
|
|
93
|
+
onPress={handleLogin}
|
|
94
|
+
isLoading={isLoading}
|
|
95
|
+
style={{ marginTop: 8 }}
|
|
96
|
+
>
|
|
97
|
+
Iniciar Sesión
|
|
98
|
+
</Button>
|
|
99
|
+
</View>
|
|
100
|
+
|
|
101
|
+
{/* Dev Hint */}
|
|
102
|
+
<View style={styles.hint}>
|
|
103
|
+
<Text style={styles.hintText}>
|
|
104
|
+
Dev: carlos.mendoza@nextspark.dev / Test1234
|
|
105
|
+
</Text>
|
|
106
|
+
</View>
|
|
107
|
+
</View>
|
|
108
|
+
</KeyboardAvoidingView>
|
|
109
|
+
</SafeAreaView>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const styles = StyleSheet.create({
|
|
114
|
+
safeArea: {
|
|
115
|
+
flex: 1,
|
|
116
|
+
backgroundColor: Colors.backgroundSecondary,
|
|
117
|
+
},
|
|
118
|
+
container: {
|
|
119
|
+
flex: 1,
|
|
120
|
+
},
|
|
121
|
+
content: {
|
|
122
|
+
flex: 1,
|
|
123
|
+
justifyContent: 'center',
|
|
124
|
+
padding: 24,
|
|
125
|
+
},
|
|
126
|
+
header: {
|
|
127
|
+
alignItems: 'center',
|
|
128
|
+
marginBottom: 32,
|
|
129
|
+
},
|
|
130
|
+
logo: {
|
|
131
|
+
fontSize: 32,
|
|
132
|
+
fontWeight: '700',
|
|
133
|
+
color: Colors.foreground,
|
|
134
|
+
},
|
|
135
|
+
subtitle: {
|
|
136
|
+
fontSize: 16,
|
|
137
|
+
color: Colors.foregroundSecondary,
|
|
138
|
+
marginTop: 8,
|
|
139
|
+
},
|
|
140
|
+
errorContainer: {
|
|
141
|
+
backgroundColor: '#FEE2E2',
|
|
142
|
+
padding: 12,
|
|
143
|
+
borderRadius: 8,
|
|
144
|
+
marginBottom: 16,
|
|
145
|
+
},
|
|
146
|
+
errorText: {
|
|
147
|
+
color: Colors.destructive,
|
|
148
|
+
fontSize: 14,
|
|
149
|
+
textAlign: 'center',
|
|
150
|
+
},
|
|
151
|
+
form: {
|
|
152
|
+
gap: 16,
|
|
153
|
+
},
|
|
154
|
+
inputContainer: {
|
|
155
|
+
gap: 6,
|
|
156
|
+
},
|
|
157
|
+
label: {
|
|
158
|
+
fontSize: 14,
|
|
159
|
+
fontWeight: '500',
|
|
160
|
+
color: Colors.foreground,
|
|
161
|
+
},
|
|
162
|
+
input: {
|
|
163
|
+
backgroundColor: Colors.card,
|
|
164
|
+
borderWidth: 1,
|
|
165
|
+
borderColor: Colors.border,
|
|
166
|
+
borderRadius: 8,
|
|
167
|
+
padding: 14,
|
|
168
|
+
fontSize: 16,
|
|
169
|
+
color: Colors.foreground,
|
|
170
|
+
},
|
|
171
|
+
hint: {
|
|
172
|
+
marginTop: 24,
|
|
173
|
+
alignItems: 'center',
|
|
174
|
+
},
|
|
175
|
+
hintText: {
|
|
176
|
+
fontSize: 12,
|
|
177
|
+
color: Colors.foregroundMuted,
|
|
178
|
+
},
|
|
179
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ExpoConfig, ConfigContext } from 'expo/config'
|
|
2
|
+
|
|
3
|
+
export default ({ config }: ConfigContext): ExpoConfig => ({
|
|
4
|
+
...config,
|
|
5
|
+
name: 'My App',
|
|
6
|
+
slug: 'my-app',
|
|
7
|
+
version: '1.0.0',
|
|
8
|
+
orientation: 'portrait',
|
|
9
|
+
icon: './assets/icon.png',
|
|
10
|
+
userInterfaceStyle: 'automatic',
|
|
11
|
+
splash: {
|
|
12
|
+
image: './assets/splash.png',
|
|
13
|
+
resizeMode: 'contain',
|
|
14
|
+
backgroundColor: '#ffffff',
|
|
15
|
+
},
|
|
16
|
+
assetBundlePatterns: ['**/*'],
|
|
17
|
+
ios: {
|
|
18
|
+
supportsTablet: true,
|
|
19
|
+
bundleIdentifier: 'com.mycompany.myapp',
|
|
20
|
+
},
|
|
21
|
+
android: {
|
|
22
|
+
adaptiveIcon: {
|
|
23
|
+
foregroundImage: './assets/adaptive-icon.png',
|
|
24
|
+
backgroundColor: '#ffffff',
|
|
25
|
+
},
|
|
26
|
+
package: 'com.mycompany.myapp',
|
|
27
|
+
},
|
|
28
|
+
web: {
|
|
29
|
+
favicon: './assets/favicon.png',
|
|
30
|
+
},
|
|
31
|
+
extra: {
|
|
32
|
+
// API URL - Configure for your environment
|
|
33
|
+
// Development: auto-detected from Expo dev server
|
|
34
|
+
// Production: set via EAS environment variables
|
|
35
|
+
apiUrl: process.env.EXPO_PUBLIC_API_URL,
|
|
36
|
+
},
|
|
37
|
+
plugins: ['expo-router', 'expo-secure-store'],
|
|
38
|
+
scheme: 'myapp',
|
|
39
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cli": {
|
|
3
|
+
"version": ">= 7.0.0"
|
|
4
|
+
},
|
|
5
|
+
"build": {
|
|
6
|
+
"development": {
|
|
7
|
+
"developmentClient": true,
|
|
8
|
+
"distribution": "internal"
|
|
9
|
+
},
|
|
10
|
+
"preview": {
|
|
11
|
+
"distribution": "internal"
|
|
12
|
+
},
|
|
13
|
+
"production": {}
|
|
14
|
+
},
|
|
15
|
+
"submit": {
|
|
16
|
+
"production": {}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
preset: 'jest-expo',
|
|
4
|
+
testMatch: ['**/__tests__/**/*.test.ts?(x)'],
|
|
5
|
+
transformIgnorePatterns: [
|
|
6
|
+
'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|@nextsparkjs/.*)',
|
|
7
|
+
],
|
|
8
|
+
setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'],
|
|
9
|
+
moduleNameMapper: {
|
|
10
|
+
'^@/(.*)$': '<rootDir>/$1',
|
|
11
|
+
},
|
|
12
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const { getDefaultConfig } = require("expo/metro-config");
|
|
2
|
+
const { withNativeWind } = require("nativewind/metro");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
const config = getDefaultConfig(__dirname);
|
|
6
|
+
|
|
7
|
+
// Force @nextsparkjs/ui to use the native entry point for consistent styling
|
|
8
|
+
// across web and native platforms (uses React Native primitives)
|
|
9
|
+
config.resolver.resolveRequest = (context, moduleName, platform) => {
|
|
10
|
+
if (moduleName === "@nextsparkjs/ui") {
|
|
11
|
+
// Resolve to the native entry point in node_modules
|
|
12
|
+
const pkgPath = path.dirname(require.resolve("@nextsparkjs/ui/package.json"));
|
|
13
|
+
const nativeEntry = path.join(pkgPath, "dist", "index.native.js");
|
|
14
|
+
return {
|
|
15
|
+
filePath: nativeEntry,
|
|
16
|
+
type: "sourceFile",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
// Fall back to default resolution
|
|
20
|
+
return context.resolveRequest(context, moduleName, platform);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
module.exports = withNativeWind(config, { input: "./src/styles/globals.css" });
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@myproject/mobile",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"main": "expo-router/entry",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "expo start",
|
|
8
|
+
"android": "expo start --android",
|
|
9
|
+
"ios": "expo start --ios",
|
|
10
|
+
"web": "expo start --web",
|
|
11
|
+
"lint": "eslint .",
|
|
12
|
+
"typecheck": "tsc --noEmit",
|
|
13
|
+
"test": "jest",
|
|
14
|
+
"test:watch": "jest --watch",
|
|
15
|
+
"test:coverage": "jest --coverage"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@nextsparkjs/mobile": "^0.1.0",
|
|
19
|
+
"@nextsparkjs/ui": "^0.1.0-beta.1",
|
|
20
|
+
"@tanstack/react-query": "^5.62.0",
|
|
21
|
+
"expo": "^54.0.0",
|
|
22
|
+
"expo-constants": "~18.0.13",
|
|
23
|
+
"expo-linking": "~8.0.11",
|
|
24
|
+
"expo-router": "~6.0.22",
|
|
25
|
+
"expo-secure-store": "~15.0.8",
|
|
26
|
+
"expo-status-bar": "~3.0.9",
|
|
27
|
+
"lucide-react-native": "^0.563.0",
|
|
28
|
+
"nativewind": "^4.2.1",
|
|
29
|
+
"react": "19.1.0",
|
|
30
|
+
"react-dom": "19.1.0",
|
|
31
|
+
"react-native": "0.81.5",
|
|
32
|
+
"react-native-web": "^0.21.0",
|
|
33
|
+
"react-native-gesture-handler": "~2.28.0",
|
|
34
|
+
"react-native-reanimated": "~4.1.1",
|
|
35
|
+
"react-native-safe-area-context": "~5.6.0",
|
|
36
|
+
"react-native-screens": "~4.16.0",
|
|
37
|
+
"react-native-svg": "15.12.1",
|
|
38
|
+
"tailwind-merge": "^3.4.0",
|
|
39
|
+
"tailwindcss": "^3"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@babel/core": "^7.25.0",
|
|
43
|
+
"@testing-library/jest-native": "^5.4.3",
|
|
44
|
+
"@testing-library/react-native": "^13.3.3",
|
|
45
|
+
"@types/jest": "^29.5.0",
|
|
46
|
+
"@types/react": "^19",
|
|
47
|
+
"jest": "^29.7.0",
|
|
48
|
+
"jest-expo": "^54.0.16",
|
|
49
|
+
"react-test-renderer": "19.1.0",
|
|
50
|
+
"typescript": "^5.3.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Customer Card Component
|
|
3
|
+
* Migrated to NativeWind + UI primitives
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { View } from "react-native";
|
|
7
|
+
import type { Customer } from "../../../entities/customers";
|
|
8
|
+
import { Text, PressableCard, CardContent, Badge } from "../../ui";
|
|
9
|
+
|
|
10
|
+
interface CustomerCardProps {
|
|
11
|
+
customer: Customer;
|
|
12
|
+
onPress: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function CustomerCard({ customer, onPress }: CustomerCardProps) {
|
|
16
|
+
return (
|
|
17
|
+
<PressableCard className="mx-4 my-1.5" onPress={onPress}>
|
|
18
|
+
<CardContent>
|
|
19
|
+
{/* Header: Name + Account Badge */}
|
|
20
|
+
<View className="mb-3 flex-row items-center justify-between">
|
|
21
|
+
<Text weight="semibold" className="flex-1 text-base" numberOfLines={1}>
|
|
22
|
+
{customer.name}
|
|
23
|
+
</Text>
|
|
24
|
+
<Badge variant="secondary" className="ml-2">
|
|
25
|
+
#{customer.account}
|
|
26
|
+
</Badge>
|
|
27
|
+
</View>
|
|
28
|
+
|
|
29
|
+
{/* Details */}
|
|
30
|
+
<View className="gap-1.5">
|
|
31
|
+
<View className="flex-row">
|
|
32
|
+
<Text variant="muted" className="w-20 text-[13px]">
|
|
33
|
+
Oficina:
|
|
34
|
+
</Text>
|
|
35
|
+
<Text className="flex-1 text-[13px]">{customer.office}</Text>
|
|
36
|
+
</View>
|
|
37
|
+
|
|
38
|
+
{customer.phone && (
|
|
39
|
+
<View className="flex-row">
|
|
40
|
+
<Text variant="muted" className="w-20 text-[13px]">
|
|
41
|
+
Teléfono:
|
|
42
|
+
</Text>
|
|
43
|
+
<Text className="flex-1 text-[13px]">{customer.phone}</Text>
|
|
44
|
+
</View>
|
|
45
|
+
)}
|
|
46
|
+
|
|
47
|
+
{customer.salesRep && (
|
|
48
|
+
<View className="flex-row">
|
|
49
|
+
<Text variant="muted" className="w-20 text-[13px]">
|
|
50
|
+
Vendedor:
|
|
51
|
+
</Text>
|
|
52
|
+
<Text className="flex-1 text-[13px]">{customer.salesRep}</Text>
|
|
53
|
+
</View>
|
|
54
|
+
)}
|
|
55
|
+
</View>
|
|
56
|
+
</CardContent>
|
|
57
|
+
</PressableCard>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Customer Form Component for Create/Edit
|
|
3
|
+
* Migrated to NativeWind + UI primitives
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useEffect } from "react";
|
|
7
|
+
import { View, ScrollView } from "react-native";
|
|
8
|
+
import { Alert } from "@nextsparkjs/mobile";
|
|
9
|
+
import type {
|
|
10
|
+
Customer,
|
|
11
|
+
CreateCustomerInput,
|
|
12
|
+
UpdateCustomerInput,
|
|
13
|
+
} from "../../../entities/customers";
|
|
14
|
+
import { Text, Input, Button, Card } from "../../ui";
|
|
15
|
+
|
|
16
|
+
type CustomerFormProps =
|
|
17
|
+
| {
|
|
18
|
+
mode: "create";
|
|
19
|
+
initialData?: undefined;
|
|
20
|
+
onSubmit: (data: CreateCustomerInput) => Promise<void>;
|
|
21
|
+
onDelete?: undefined;
|
|
22
|
+
isLoading?: boolean;
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
mode: "edit";
|
|
26
|
+
initialData: Customer;
|
|
27
|
+
onSubmit: (data: UpdateCustomerInput) => Promise<void>;
|
|
28
|
+
onDelete: () => Promise<void>;
|
|
29
|
+
isLoading?: boolean;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function CustomerForm({
|
|
33
|
+
initialData,
|
|
34
|
+
onSubmit,
|
|
35
|
+
onDelete,
|
|
36
|
+
isLoading = false,
|
|
37
|
+
mode,
|
|
38
|
+
}: CustomerFormProps) {
|
|
39
|
+
const [name, setName] = useState(initialData?.name || "");
|
|
40
|
+
const [account, setAccount] = useState(
|
|
41
|
+
initialData?.account?.toString() || ""
|
|
42
|
+
);
|
|
43
|
+
const [office, setOffice] = useState(initialData?.office || "");
|
|
44
|
+
const [phone, setPhone] = useState(initialData?.phone || "");
|
|
45
|
+
const [salesRep, setSalesRep] = useState(initialData?.salesRep || "");
|
|
46
|
+
const [error, setError] = useState<string | null>(null);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (initialData) {
|
|
50
|
+
setName(initialData.name);
|
|
51
|
+
setAccount(initialData.account?.toString() || "");
|
|
52
|
+
setOffice(initialData.office);
|
|
53
|
+
setPhone(initialData.phone || "");
|
|
54
|
+
setSalesRep(initialData.salesRep || "");
|
|
55
|
+
}
|
|
56
|
+
}, [initialData]);
|
|
57
|
+
|
|
58
|
+
const handleSubmit = async () => {
|
|
59
|
+
setError(null);
|
|
60
|
+
|
|
61
|
+
// Validation
|
|
62
|
+
if (!name.trim()) {
|
|
63
|
+
setError("El nombre es requerido");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!account.trim() || isNaN(Number(account))) {
|
|
67
|
+
setError("El número de cuenta es requerido");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!office.trim()) {
|
|
71
|
+
setError("La oficina es requerida");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const data = {
|
|
76
|
+
name: name.trim(),
|
|
77
|
+
account: Number(account),
|
|
78
|
+
office: office.trim(),
|
|
79
|
+
phone: phone.trim() || undefined,
|
|
80
|
+
salesRep: salesRep.trim() || undefined,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await onSubmit(data as CreateCustomerInput & UpdateCustomerInput);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
setError(err instanceof Error ? err.message : "Ocurrió un error");
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleDelete = async () => {
|
|
91
|
+
const confirmed = await Alert.confirmDestructive(
|
|
92
|
+
"Eliminar Cliente",
|
|
93
|
+
"¿Estás seguro que deseas eliminar este cliente? Esta acción no se puede deshacer.",
|
|
94
|
+
"Eliminar"
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (confirmed) {
|
|
98
|
+
try {
|
|
99
|
+
await onDelete?.();
|
|
100
|
+
} catch (err) {
|
|
101
|
+
setError(err instanceof Error ? err.message : "Error al eliminar");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<ScrollView
|
|
108
|
+
className="flex-1 bg-secondary p-4"
|
|
109
|
+
keyboardShouldPersistTaps="handled"
|
|
110
|
+
>
|
|
111
|
+
{/* Error Display */}
|
|
112
|
+
{error && (
|
|
113
|
+
<Card className="mb-4 border-destructive bg-red-50">
|
|
114
|
+
<Text variant="error">{error}</Text>
|
|
115
|
+
</Card>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{/* Name */}
|
|
119
|
+
<Input
|
|
120
|
+
label="Nombre"
|
|
121
|
+
required
|
|
122
|
+
value={name}
|
|
123
|
+
onChangeText={setName}
|
|
124
|
+
placeholder="Ingresa el nombre del cliente..."
|
|
125
|
+
editable={!isLoading}
|
|
126
|
+
containerClassName="mb-5"
|
|
127
|
+
/>
|
|
128
|
+
|
|
129
|
+
{/* Account Number */}
|
|
130
|
+
<Input
|
|
131
|
+
label="Número de Cuenta"
|
|
132
|
+
required
|
|
133
|
+
value={account}
|
|
134
|
+
onChangeText={setAccount}
|
|
135
|
+
placeholder="Ingresa el número de cuenta..."
|
|
136
|
+
keyboardType="numeric"
|
|
137
|
+
editable={!isLoading}
|
|
138
|
+
containerClassName="mb-5"
|
|
139
|
+
/>
|
|
140
|
+
|
|
141
|
+
{/* Office */}
|
|
142
|
+
<Input
|
|
143
|
+
label="Oficina"
|
|
144
|
+
required
|
|
145
|
+
value={office}
|
|
146
|
+
onChangeText={setOffice}
|
|
147
|
+
placeholder="Ingresa la ubicación de oficina..."
|
|
148
|
+
editable={!isLoading}
|
|
149
|
+
containerClassName="mb-5"
|
|
150
|
+
/>
|
|
151
|
+
|
|
152
|
+
{/* Phone */}
|
|
153
|
+
<Input
|
|
154
|
+
label="Teléfono"
|
|
155
|
+
value={phone}
|
|
156
|
+
onChangeText={setPhone}
|
|
157
|
+
placeholder="Ingresa el número de teléfono..."
|
|
158
|
+
keyboardType="phone-pad"
|
|
159
|
+
editable={!isLoading}
|
|
160
|
+
containerClassName="mb-5"
|
|
161
|
+
/>
|
|
162
|
+
|
|
163
|
+
{/* Sales Rep */}
|
|
164
|
+
<Input
|
|
165
|
+
label="Representante de Ventas"
|
|
166
|
+
value={salesRep}
|
|
167
|
+
onChangeText={setSalesRep}
|
|
168
|
+
placeholder="Ingresa el nombre del vendedor..."
|
|
169
|
+
editable={!isLoading}
|
|
170
|
+
containerClassName="mb-5"
|
|
171
|
+
/>
|
|
172
|
+
|
|
173
|
+
{/* Submit Button */}
|
|
174
|
+
<Button onPress={handleSubmit} isLoading={isLoading} className="mt-2">
|
|
175
|
+
{mode === "create" ? "Crear Cliente" : "Guardar Cambios"}
|
|
176
|
+
</Button>
|
|
177
|
+
|
|
178
|
+
{/* Delete Button (edit mode only) */}
|
|
179
|
+
{mode === "edit" && (
|
|
180
|
+
<Button
|
|
181
|
+
variant="outline-destructive"
|
|
182
|
+
onPress={handleDelete}
|
|
183
|
+
disabled={isLoading}
|
|
184
|
+
className="mt-3"
|
|
185
|
+
>
|
|
186
|
+
Eliminar Cliente
|
|
187
|
+
</Button>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
{/* Spacer */}
|
|
191
|
+
<View className="h-10" />
|
|
192
|
+
</ScrollView>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Card Component
|
|
3
|
+
* Migrated to NativeWind + UI primitives
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { View } from "react-native";
|
|
7
|
+
import type { Task } from "../../../entities/tasks";
|
|
8
|
+
import { STATUS_LABELS, PRIORITY_LABELS } from "../../../entities/tasks";
|
|
9
|
+
import {
|
|
10
|
+
Text,
|
|
11
|
+
PressableCard,
|
|
12
|
+
CardContent,
|
|
13
|
+
CardFooter,
|
|
14
|
+
Badge,
|
|
15
|
+
} from "../../ui";
|
|
16
|
+
|
|
17
|
+
interface TaskCardProps {
|
|
18
|
+
task: Task;
|
|
19
|
+
onPress: () => void;
|
|
20
|
+
onStatusChange?: (status: Task["status"]) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function TaskCard({ task, onPress }: TaskCardProps) {
|
|
24
|
+
const formatDate = (dateString?: string | null) => {
|
|
25
|
+
if (!dateString) return null;
|
|
26
|
+
const date = new Date(dateString);
|
|
27
|
+
return date.toLocaleDateString();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Map status to badge variant
|
|
31
|
+
const getStatusVariant = (status: Task["status"]) => {
|
|
32
|
+
const map: Record<Task["status"], "todo" | "in-progress" | "review" | "done" | "blocked"> = {
|
|
33
|
+
todo: "todo",
|
|
34
|
+
"in-progress": "in-progress",
|
|
35
|
+
review: "review",
|
|
36
|
+
done: "done",
|
|
37
|
+
blocked: "blocked",
|
|
38
|
+
};
|
|
39
|
+
return map[status];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Map priority to badge variant
|
|
43
|
+
const getPriorityVariant = (priority: Task["priority"]) => {
|
|
44
|
+
const map: Record<Task["priority"], "low" | "medium" | "high" | "urgent"> = {
|
|
45
|
+
low: "low",
|
|
46
|
+
medium: "medium",
|
|
47
|
+
high: "high",
|
|
48
|
+
urgent: "urgent",
|
|
49
|
+
};
|
|
50
|
+
return map[priority];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<PressableCard className="mx-4 my-1.5" onPress={onPress}>
|
|
55
|
+
<CardContent className="gap-2">
|
|
56
|
+
{/* Title */}
|
|
57
|
+
<Text weight="semibold" className="text-base" numberOfLines={2}>
|
|
58
|
+
{task.title}
|
|
59
|
+
</Text>
|
|
60
|
+
|
|
61
|
+
{/* Description */}
|
|
62
|
+
{task.description && (
|
|
63
|
+
<Text variant="muted" className="text-sm leading-5" numberOfLines={2}>
|
|
64
|
+
{task.description}
|
|
65
|
+
</Text>
|
|
66
|
+
)}
|
|
67
|
+
|
|
68
|
+
{/* Badges */}
|
|
69
|
+
<View className="mt-1 flex-row flex-wrap gap-2">
|
|
70
|
+
<Badge variant={getStatusVariant(task.status)} showDot>
|
|
71
|
+
{STATUS_LABELS[task.status]}
|
|
72
|
+
</Badge>
|
|
73
|
+
<Badge variant={getPriorityVariant(task.priority)}>
|
|
74
|
+
{PRIORITY_LABELS[task.priority]}
|
|
75
|
+
</Badge>
|
|
76
|
+
</View>
|
|
77
|
+
</CardContent>
|
|
78
|
+
|
|
79
|
+
{/* Due Date */}
|
|
80
|
+
{task.dueDate && (
|
|
81
|
+
<CardFooter>
|
|
82
|
+
<Text variant="muted" className="text-xs">
|
|
83
|
+
Vence: {formatDate(task.dueDate)}
|
|
84
|
+
</Text>
|
|
85
|
+
</CardFooter>
|
|
86
|
+
)}
|
|
87
|
+
</PressableCard>
|
|
88
|
+
);
|
|
89
|
+
}
|