Add equipment status workflow to beneficiary cards
- BeneficiaryCard now shows equipment status badges (ordered/shipped/delivered) - Added AwaitingEquipmentScreen with progress tracker - "I received my kit" button to mark as delivered - "Activate" button when equipment is delivered - Fixed address field height in add-loved-one form 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
7cb07c09ce
commit
2545aec485
@ -184,16 +184,14 @@ export default function AddLovedOneScreen() {
|
||||
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.inputLabel}>Address (optional)</Text>
|
||||
<View style={[styles.inputContainer, styles.addressInput]}>
|
||||
<Ionicons name="location-outline" size={20} color={AppColors.textMuted} style={styles.addressIcon} />
|
||||
<View style={styles.inputContainer}>
|
||||
<Ionicons name="location-outline" size={20} color={AppColors.textMuted} />
|
||||
<TextInput
|
||||
style={[styles.input, styles.addressTextInput]}
|
||||
style={styles.input}
|
||||
value={address}
|
||||
onChangeText={setAddress}
|
||||
placeholder="123 Main St, City, State"
|
||||
placeholderTextColor={AppColors.textMuted}
|
||||
multiline
|
||||
numberOfLines={2}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
</View>
|
||||
@ -327,16 +325,6 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: Spacing.md,
|
||||
marginLeft: Spacing.sm,
|
||||
},
|
||||
addressInput: {
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
addressIcon: {
|
||||
marginTop: Spacing.md,
|
||||
},
|
||||
addressTextInput: {
|
||||
minHeight: 60,
|
||||
textAlignVertical: 'top',
|
||||
},
|
||||
buttonContainer: {
|
||||
marginTop: Spacing.md,
|
||||
},
|
||||
|
||||
@ -41,7 +41,7 @@ const isLocalBeneficiary = (id: string | number): boolean => {
|
||||
};
|
||||
|
||||
// Setup state types
|
||||
type SetupState = 'loading' | 'no_devices' | 'no_subscription' | 'ready';
|
||||
type SetupState = 'loading' | 'awaiting_equipment' | 'no_devices' | 'no_subscription' | 'ready';
|
||||
|
||||
// No Devices Screen Component
|
||||
function NoDevicesScreen({
|
||||
@ -162,6 +162,133 @@ function NoSubscriptionScreen({
|
||||
);
|
||||
}
|
||||
|
||||
// Equipment status configuration
|
||||
const equipmentStatusInfo = {
|
||||
ordered: {
|
||||
icon: 'cube-outline' as const,
|
||||
title: 'Kit Ordered',
|
||||
subtitle: 'Your WellNuo kit is being prepared for shipping',
|
||||
color: AppColors.info,
|
||||
bgColor: AppColors.infoLight,
|
||||
steps: [
|
||||
{ label: 'Order placed', done: true },
|
||||
{ label: 'Preparing', done: true },
|
||||
{ label: 'Shipped', done: false },
|
||||
{ label: 'Delivered', done: false },
|
||||
],
|
||||
},
|
||||
shipped: {
|
||||
icon: 'car-outline' as const,
|
||||
title: 'In Transit',
|
||||
subtitle: 'Your WellNuo kit is on its way',
|
||||
color: AppColors.warning,
|
||||
bgColor: AppColors.warningLight,
|
||||
steps: [
|
||||
{ label: 'Order placed', done: true },
|
||||
{ label: 'Preparing', done: true },
|
||||
{ label: 'Shipped', done: true },
|
||||
{ label: 'Delivered', done: false },
|
||||
],
|
||||
},
|
||||
delivered: {
|
||||
icon: 'checkmark-circle-outline' as const,
|
||||
title: 'Delivered',
|
||||
subtitle: 'Your kit has arrived! Time to set it up.',
|
||||
color: AppColors.success,
|
||||
bgColor: AppColors.successLight,
|
||||
steps: [
|
||||
{ label: 'Order placed', done: true },
|
||||
{ label: 'Preparing', done: true },
|
||||
{ label: 'Shipped', done: true },
|
||||
{ label: 'Delivered', done: true },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// Awaiting Equipment Screen Component
|
||||
function AwaitingEquipmentScreen({
|
||||
beneficiary,
|
||||
onActivate,
|
||||
onMarkReceived,
|
||||
}: {
|
||||
beneficiary: Beneficiary;
|
||||
onActivate: () => void;
|
||||
onMarkReceived: () => void;
|
||||
}) {
|
||||
const status = beneficiary.equipmentStatus as 'ordered' | 'shipped' | 'delivered';
|
||||
const info = equipmentStatusInfo[status] || equipmentStatusInfo.ordered;
|
||||
const isDelivered = status === 'delivered';
|
||||
|
||||
return (
|
||||
<View style={styles.setupContainer}>
|
||||
<View style={[styles.setupIconContainer, { backgroundColor: info.bgColor }]}>
|
||||
<Ionicons name={info.icon} size={48} color={info.color} />
|
||||
</View>
|
||||
<Text style={styles.setupTitle}>{info.title}</Text>
|
||||
<Text style={styles.setupSubtitle}>{info.subtitle}</Text>
|
||||
|
||||
{/* Progress steps */}
|
||||
<View style={styles.progressContainer}>
|
||||
{info.steps.map((step, index) => (
|
||||
<View key={index} style={styles.progressStep}>
|
||||
<View style={[
|
||||
styles.progressDot,
|
||||
step.done && styles.progressDotDone,
|
||||
!step.done && styles.progressDotPending,
|
||||
]}>
|
||||
{step.done && (
|
||||
<Ionicons name="checkmark" size={12} color={AppColors.white} />
|
||||
)}
|
||||
</View>
|
||||
<Text style={[
|
||||
styles.progressLabel,
|
||||
step.done && styles.progressLabelDone,
|
||||
]}>
|
||||
{step.label}
|
||||
</Text>
|
||||
{index < info.steps.length - 1 && (
|
||||
<View style={[
|
||||
styles.progressLine,
|
||||
step.done && styles.progressLineDone,
|
||||
]} />
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Tracking number if available */}
|
||||
{beneficiary.trackingNumber && (
|
||||
<View style={styles.trackingCard}>
|
||||
<Ionicons name="locate-outline" size={20} color={AppColors.textSecondary} />
|
||||
<View style={styles.trackingInfo}>
|
||||
<Text style={styles.trackingLabel}>Tracking Number</Text>
|
||||
<Text style={styles.trackingNumber}>{beneficiary.trackingNumber}</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<View style={styles.awaitingActions}>
|
||||
{isDelivered ? (
|
||||
<Button
|
||||
title="Activate Sensors"
|
||||
onPress={onActivate}
|
||||
fullWidth
|
||||
size="lg"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<TouchableOpacity style={styles.receivedButton} onPress={onMarkReceived}>
|
||||
<Ionicons name="checkmark-circle-outline" size={20} color={AppColors.primary} />
|
||||
<Text style={styles.receivedButtonText}>I received my kit</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default function BeneficiaryDetailScreen() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
const { setCurrentBeneficiary, localBeneficiaries, updateLocalBeneficiary, removeLocalBeneficiary } = useBeneficiary();
|
||||
@ -178,6 +305,12 @@ export default function BeneficiaryDetailScreen() {
|
||||
if (isLoading) return 'loading';
|
||||
if (!beneficiary) return 'loading';
|
||||
|
||||
// Check if awaiting equipment (ordered, shipped, or delivered but not activated)
|
||||
const equipmentStatus = beneficiary.equipmentStatus;
|
||||
if (equipmentStatus && ['ordered', 'shipped', 'delivered'].includes(equipmentStatus)) {
|
||||
return 'awaiting_equipment';
|
||||
}
|
||||
|
||||
// Check if has devices
|
||||
const hasDevices = beneficiary.hasDevices ||
|
||||
(beneficiary.devices && beneficiary.devices.length > 0) ||
|
||||
@ -280,6 +413,31 @@ export default function BeneficiaryDetailScreen() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleMarkReceived = async () => {
|
||||
if (!beneficiary || !id) return;
|
||||
|
||||
try {
|
||||
// Update local beneficiary status to delivered
|
||||
if (isLocal) {
|
||||
await updateLocalBeneficiary(parseInt(id, 10), {
|
||||
equipmentStatus: 'delivered',
|
||||
});
|
||||
// Reload to refresh UI
|
||||
loadBeneficiary(false);
|
||||
}
|
||||
// For API beneficiaries, would call backend here
|
||||
} catch (err) {
|
||||
Alert.alert('Error', 'Failed to update status');
|
||||
}
|
||||
};
|
||||
|
||||
const handleActivateFromStatus = () => {
|
||||
router.push({
|
||||
pathname: '/(auth)/activate',
|
||||
params: { lovedOneName: beneficiary?.name, beneficiaryId: id },
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditPress = () => {
|
||||
if (beneficiary) {
|
||||
setEditForm({
|
||||
@ -388,6 +546,15 @@ export default function BeneficiaryDetailScreen() {
|
||||
// Render based on setup state
|
||||
const renderContent = () => {
|
||||
switch (setupState) {
|
||||
case 'awaiting_equipment':
|
||||
return (
|
||||
<AwaitingEquipmentScreen
|
||||
beneficiary={beneficiary}
|
||||
onActivate={handleActivateFromStatus}
|
||||
onMarkReceived={handleMarkReceived}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'no_devices':
|
||||
return (
|
||||
<NoDevicesScreen
|
||||
@ -810,6 +977,97 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
// Equipment Progress Styles
|
||||
progressContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: Spacing.xl,
|
||||
width: '100%',
|
||||
paddingHorizontal: Spacing.md,
|
||||
},
|
||||
progressStep: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
position: 'relative',
|
||||
},
|
||||
progressDot: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: Spacing.xs,
|
||||
},
|
||||
progressDotDone: {
|
||||
backgroundColor: AppColors.success,
|
||||
},
|
||||
progressDotPending: {
|
||||
backgroundColor: AppColors.border,
|
||||
},
|
||||
progressLabel: {
|
||||
fontSize: FontSizes.xs,
|
||||
color: AppColors.textMuted,
|
||||
textAlign: 'center',
|
||||
},
|
||||
progressLabelDone: {
|
||||
color: AppColors.textPrimary,
|
||||
fontWeight: FontWeights.medium,
|
||||
},
|
||||
progressLine: {
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
left: '50%',
|
||||
right: '-50%',
|
||||
height: 2,
|
||||
backgroundColor: AppColors.border,
|
||||
zIndex: -1,
|
||||
},
|
||||
progressLineDone: {
|
||||
backgroundColor: AppColors.success,
|
||||
},
|
||||
trackingCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: AppColors.surface,
|
||||
borderRadius: BorderRadius.lg,
|
||||
padding: Spacing.md,
|
||||
marginBottom: Spacing.lg,
|
||||
width: '100%',
|
||||
gap: Spacing.md,
|
||||
...Shadows.sm,
|
||||
},
|
||||
trackingInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
trackingLabel: {
|
||||
fontSize: FontSizes.xs,
|
||||
color: AppColors.textMuted,
|
||||
},
|
||||
trackingNumber: {
|
||||
fontSize: FontSizes.base,
|
||||
fontWeight: FontWeights.semibold,
|
||||
color: AppColors.textPrimary,
|
||||
},
|
||||
awaitingActions: {
|
||||
width: '100%',
|
||||
},
|
||||
receivedButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: AppColors.surface,
|
||||
borderRadius: BorderRadius.lg,
|
||||
padding: Spacing.md,
|
||||
borderWidth: 1,
|
||||
borderColor: AppColors.primary,
|
||||
gap: Spacing.sm,
|
||||
},
|
||||
receivedButtonText: {
|
||||
fontSize: FontSizes.base,
|
||||
fontWeight: FontWeights.semibold,
|
||||
color: AppColors.primary,
|
||||
},
|
||||
// Subscription Price Card
|
||||
subscriptionPriceCard: {
|
||||
width: '100%',
|
||||
|
||||
@ -26,19 +26,59 @@ import {
|
||||
} from '@/constants/theme';
|
||||
import type { Beneficiary } from '@/types';
|
||||
|
||||
// Simplified beneficiary card: Avatar + Name + Warning (if no subscription)
|
||||
// Beneficiary card with equipment status support
|
||||
interface BeneficiaryCardProps {
|
||||
beneficiary: Beneficiary;
|
||||
onPress: () => void;
|
||||
onActivate?: () => void;
|
||||
}
|
||||
|
||||
function BeneficiaryCard({ beneficiary, onPress }: BeneficiaryCardProps) {
|
||||
// Check if subscription is missing or expired
|
||||
const hasNoSubscription = !beneficiary.subscription ||
|
||||
beneficiary.subscription.status !== 'active';
|
||||
// Equipment status config
|
||||
const equipmentStatusConfig = {
|
||||
ordered: {
|
||||
icon: 'cube-outline' as const,
|
||||
label: 'Kit ordered',
|
||||
sublabel: 'Preparing to ship',
|
||||
color: AppColors.info,
|
||||
bgColor: AppColors.infoLight,
|
||||
},
|
||||
shipped: {
|
||||
icon: 'car-outline' as const,
|
||||
label: 'In transit',
|
||||
sublabel: 'Track your package',
|
||||
color: AppColors.warning,
|
||||
bgColor: AppColors.warningLight,
|
||||
},
|
||||
delivered: {
|
||||
icon: 'checkmark-circle-outline' as const,
|
||||
label: 'Delivered',
|
||||
sublabel: 'Ready to activate',
|
||||
color: AppColors.success,
|
||||
bgColor: AppColors.successLight,
|
||||
},
|
||||
};
|
||||
|
||||
function BeneficiaryCard({ beneficiary, onPress, onActivate }: BeneficiaryCardProps) {
|
||||
const equipmentStatus = beneficiary.equipmentStatus;
|
||||
const isAwaitingEquipment = equipmentStatus && ['ordered', 'shipped', 'delivered'].includes(equipmentStatus);
|
||||
const statusConfig = equipmentStatus ? equipmentStatusConfig[equipmentStatus as keyof typeof equipmentStatusConfig] : null;
|
||||
|
||||
// Check if subscription is missing or expired (only for active equipment)
|
||||
const hasNoSubscription = !isAwaitingEquipment && (
|
||||
!beneficiary.subscription || beneficiary.subscription.status !== 'active'
|
||||
);
|
||||
|
||||
// Handle press - if delivered, go to activate
|
||||
const handlePress = () => {
|
||||
if (equipmentStatus === 'delivered' && onActivate) {
|
||||
onActivate();
|
||||
} else {
|
||||
onPress();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TouchableOpacity style={styles.card} onPress={onPress} activeOpacity={0.7}>
|
||||
<TouchableOpacity style={styles.card} onPress={handlePress} activeOpacity={0.7}>
|
||||
{/* Avatar */}
|
||||
<View style={styles.avatarWrapper}>
|
||||
{beneficiary.avatar ? (
|
||||
@ -52,22 +92,41 @@ function BeneficiaryCard({ beneficiary, onPress }: BeneficiaryCardProps) {
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Name */}
|
||||
{/* Name and Status */}
|
||||
<View style={styles.info}>
|
||||
<Text style={styles.name} numberOfLines={1}>{beneficiary.name}</Text>
|
||||
{/* Equipment status badge */}
|
||||
{isAwaitingEquipment && statusConfig && (
|
||||
<View style={[styles.statusBadge, { backgroundColor: statusConfig.bgColor }]}>
|
||||
<Ionicons name={statusConfig.icon} size={14} color={statusConfig.color} />
|
||||
<Text style={[styles.statusText, { color: statusConfig.color }]}>
|
||||
{statusConfig.label}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Warning icon if no subscription */}
|
||||
{/* Warning icon if no subscription (only for active equipment) */}
|
||||
{hasNoSubscription && (
|
||||
<View style={styles.warningContainer}>
|
||||
<Ionicons name="warning" size={20} color={AppColors.warning} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Arrow */}
|
||||
<View style={styles.arrowContainer}>
|
||||
<Ionicons name="chevron-forward" size={20} color={AppColors.textMuted} />
|
||||
</View>
|
||||
{/* Action button or Arrow */}
|
||||
{equipmentStatus === 'delivered' ? (
|
||||
<TouchableOpacity
|
||||
style={styles.activateButton}
|
||||
onPress={onActivate}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={styles.activateButtonText}>Activate</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<View style={styles.arrowContainer}>
|
||||
<Ionicons name="chevron-forward" size={20} color={AppColors.textMuted} />
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
@ -133,7 +192,20 @@ export default function HomeScreen() {
|
||||
|
||||
const handleBeneficiaryPress = (beneficiary: Beneficiary) => {
|
||||
setCurrentBeneficiary(beneficiary);
|
||||
router.push(`/(tabs)/beneficiaries/${beneficiary.id}/dashboard`);
|
||||
// If equipment is not active yet, show status page instead of dashboard
|
||||
if (beneficiary.equipmentStatus && ['ordered', 'shipped'].includes(beneficiary.equipmentStatus)) {
|
||||
router.push(`/(tabs)/beneficiaries/${beneficiary.id}`);
|
||||
} else {
|
||||
router.push(`/(tabs)/beneficiaries/${beneficiary.id}/dashboard`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActivate = (beneficiary: Beneficiary) => {
|
||||
setCurrentBeneficiary(beneficiary);
|
||||
router.push({
|
||||
pathname: '/(auth)/activate',
|
||||
params: { lovedOneName: beneficiary.name, beneficiaryId: beneficiary.id.toString() },
|
||||
});
|
||||
};
|
||||
|
||||
const getDisplayName = () => {
|
||||
@ -213,6 +285,7 @@ export default function HomeScreen() {
|
||||
<BeneficiaryCard
|
||||
beneficiary={item}
|
||||
onPress={() => handleBeneficiaryPress(item)}
|
||||
onActivate={() => handleActivate(item)}
|
||||
/>
|
||||
)}
|
||||
contentContainerStyle={styles.listContent}
|
||||
@ -399,6 +472,31 @@ const styles = StyleSheet.create({
|
||||
warningContainer: {
|
||||
marginRight: Spacing.sm,
|
||||
},
|
||||
statusBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: Spacing.sm,
|
||||
paddingVertical: 4,
|
||||
borderRadius: BorderRadius.md,
|
||||
marginTop: 4,
|
||||
alignSelf: 'flex-start',
|
||||
gap: 4,
|
||||
},
|
||||
statusText: {
|
||||
fontSize: FontSizes.xs,
|
||||
fontWeight: FontWeights.medium,
|
||||
},
|
||||
activateButton: {
|
||||
backgroundColor: AppColors.primary,
|
||||
paddingHorizontal: Spacing.md,
|
||||
paddingVertical: Spacing.sm,
|
||||
borderRadius: BorderRadius.md,
|
||||
},
|
||||
activateButtonText: {
|
||||
color: AppColors.white,
|
||||
fontSize: FontSizes.sm,
|
||||
fontWeight: FontWeights.semibold,
|
||||
},
|
||||
arrowContainer: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user