@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.
Files changed (123) hide show
  1. package/README.md +339 -0
  2. package/dist/api/client.d.ts +102 -0
  3. package/dist/api/client.js +189 -0
  4. package/dist/api/client.js.map +1 -0
  5. package/dist/api/client.types.d.ts +39 -0
  6. package/dist/api/client.types.js +12 -0
  7. package/dist/api/client.types.js.map +1 -0
  8. package/dist/api/core/auth.d.ts +26 -0
  9. package/dist/api/core/auth.js +52 -0
  10. package/dist/api/core/auth.js.map +1 -0
  11. package/dist/api/core/index.d.ts +4 -0
  12. package/dist/api/core/index.js +5 -0
  13. package/dist/api/core/index.js.map +1 -0
  14. package/dist/api/core/teams.d.ts +20 -0
  15. package/dist/api/core/teams.js +19 -0
  16. package/dist/api/core/teams.js.map +1 -0
  17. package/dist/api/core/types.d.ts +58 -0
  18. package/dist/api/core/types.js +1 -0
  19. package/dist/api/core/types.js.map +1 -0
  20. package/dist/api/core/users.d.ts +43 -0
  21. package/dist/api/core/users.js +41 -0
  22. package/dist/api/core/users.js.map +1 -0
  23. package/dist/api/entities/factory.d.ts +43 -0
  24. package/dist/api/entities/factory.js +31 -0
  25. package/dist/api/entities/factory.js.map +1 -0
  26. package/dist/api/entities/index.d.ts +3 -0
  27. package/dist/api/entities/index.js +3 -0
  28. package/dist/api/entities/index.js.map +1 -0
  29. package/dist/api/entities/types.d.ts +32 -0
  30. package/dist/api/entities/types.js +1 -0
  31. package/dist/api/entities/types.js.map +1 -0
  32. package/dist/api/index.d.ts +7 -0
  33. package/dist/api/index.js +15 -0
  34. package/dist/api/index.js.map +1 -0
  35. package/dist/hooks/index.d.ts +4 -0
  36. package/dist/hooks/index.js +5 -0
  37. package/dist/hooks/index.js.map +1 -0
  38. package/dist/index.d.ts +14 -0
  39. package/dist/index.js +28 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/lib/alert.d.ts +34 -0
  42. package/dist/lib/alert.js +73 -0
  43. package/dist/lib/alert.js.map +1 -0
  44. package/dist/lib/index.d.ts +2 -0
  45. package/dist/lib/index.js +10 -0
  46. package/dist/lib/index.js.map +1 -0
  47. package/dist/lib/storage.d.ts +1 -0
  48. package/dist/lib/storage.js +29 -0
  49. package/dist/lib/storage.js.map +1 -0
  50. package/dist/providers/AuthProvider.d.ts +21 -0
  51. package/dist/providers/AuthProvider.js +113 -0
  52. package/dist/providers/AuthProvider.js.map +1 -0
  53. package/dist/providers/QueryProvider.d.ts +11 -0
  54. package/dist/providers/QueryProvider.js +23 -0
  55. package/dist/providers/QueryProvider.js.map +1 -0
  56. package/dist/providers/index.d.ts +6 -0
  57. package/dist/providers/index.js +9 -0
  58. package/dist/providers/index.js.map +1 -0
  59. package/dist/storage-BaRppHUz.d.ts +22 -0
  60. package/package.json +99 -0
  61. package/templates/app/(app)/_layout.tsx +216 -0
  62. package/templates/app/(app)/customer/[id].tsx +68 -0
  63. package/templates/app/(app)/customer/create.tsx +24 -0
  64. package/templates/app/(app)/customers.tsx +164 -0
  65. package/templates/app/(app)/index.tsx +310 -0
  66. package/templates/app/(app)/notifications.tsx +242 -0
  67. package/templates/app/(app)/profile.tsx +254 -0
  68. package/templates/app/(app)/settings.tsx +241 -0
  69. package/templates/app/(app)/task/[id].tsx +70 -0
  70. package/templates/app/(app)/task/create.tsx +24 -0
  71. package/templates/app/(app)/tasks.tsx +164 -0
  72. package/templates/app/_layout.tsx +54 -0
  73. package/templates/app/index.tsx +35 -0
  74. package/templates/app/login.tsx +179 -0
  75. package/templates/app.config.ts +39 -0
  76. package/templates/babel.config.js +9 -0
  77. package/templates/eas.json +18 -0
  78. package/templates/jest.config.js +12 -0
  79. package/templates/metro.config.js +23 -0
  80. package/templates/package.json.template +52 -0
  81. package/templates/src/components/entities/customers/CustomerCard.tsx +59 -0
  82. package/templates/src/components/entities/customers/CustomerForm.tsx +194 -0
  83. package/templates/src/components/entities/customers/index.ts +6 -0
  84. package/templates/src/components/entities/index.ts +9 -0
  85. package/templates/src/components/entities/tasks/TaskCard.tsx +89 -0
  86. package/templates/src/components/entities/tasks/TaskForm.tsx +231 -0
  87. package/templates/src/components/entities/tasks/index.ts +6 -0
  88. package/templates/src/components/features/index.ts +6 -0
  89. package/templates/src/components/navigation/BottomTabBar.tsx +80 -0
  90. package/templates/src/components/navigation/CreateSheet.tsx +108 -0
  91. package/templates/src/components/navigation/MoreSheet.tsx +403 -0
  92. package/templates/src/components/navigation/TopBar.tsx +74 -0
  93. package/templates/src/components/navigation/index.ts +8 -0
  94. package/templates/src/components/ui/index.ts +89 -0
  95. package/templates/src/components/ui/text.tsx +64 -0
  96. package/templates/src/config/api.config.ts +26 -0
  97. package/templates/src/config/app.config.ts +15 -0
  98. package/templates/src/config/hooks.ts +58 -0
  99. package/templates/src/config/permissions.config.ts +119 -0
  100. package/templates/src/constants/colors.ts +55 -0
  101. package/templates/src/data/notifications.mock.json +100 -0
  102. package/templates/src/entities/customers/api.ts +10 -0
  103. package/templates/src/entities/customers/constants.internal.ts +6 -0
  104. package/templates/src/entities/customers/constants.ts +14 -0
  105. package/templates/src/entities/customers/index.ts +9 -0
  106. package/templates/src/entities/customers/mutations.ts +58 -0
  107. package/templates/src/entities/customers/queries.ts +40 -0
  108. package/templates/src/entities/customers/types.ts +43 -0
  109. package/templates/src/entities/index.ts +8 -0
  110. package/templates/src/entities/tasks/api.ts +10 -0
  111. package/templates/src/entities/tasks/constants.internal.ts +6 -0
  112. package/templates/src/entities/tasks/constants.ts +39 -0
  113. package/templates/src/entities/tasks/index.ts +9 -0
  114. package/templates/src/entities/tasks/mutations.ts +108 -0
  115. package/templates/src/entities/tasks/queries.ts +42 -0
  116. package/templates/src/entities/tasks/types.ts +52 -0
  117. package/templates/src/hooks/useCustomers.ts +17 -0
  118. package/templates/src/hooks/useTasks.ts +18 -0
  119. package/templates/src/lib/utils.ts +10 -0
  120. package/templates/src/styles/globals.css +103 -0
  121. package/templates/src/types/index.ts +45 -0
  122. package/templates/tailwind.config.js +108 -0
  123. 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,9 @@
1
+ module.exports = function (api) {
2
+ api.cache(true);
3
+ return {
4
+ presets: [
5
+ ["babel-preset-expo", { jsxImportSource: "nativewind" }],
6
+ "nativewind/babel",
7
+ ],
8
+ };
9
+ };
@@ -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,6 @@
1
+ /**
2
+ * Customer entity components - barrel exports
3
+ */
4
+
5
+ export { CustomerCard } from './CustomerCard'
6
+ export { CustomerForm } from './CustomerForm'
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Entity components - barrel exports
3
+ */
4
+
5
+ // Tasks
6
+ export * from './tasks'
7
+
8
+ // Customers
9
+ export * from './customers'
@@ -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
+ }