216 lines
6.7 KiB
TypeScript
216 lines
6.7 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
|
|
import { useInboxStore, type ActivityItem, type ApprovalItem, type InboxState } from '../../store/inbox-store';
|
|
import { buttonA11y, textInputA11y, dynamicType } from '../../lib/accessibility';
|
|
import { colors } from '../../theme';
|
|
|
|
export default function InboxScreen() {
|
|
const approvals = useInboxStore((state: InboxState) => state.approvals);
|
|
const activity = useInboxStore((state: InboxState) => state.activity);
|
|
const isLoading = useInboxStore((state: InboxState) => state.isLoading);
|
|
const hydrate = useInboxStore((state: InboxState) => state.hydrate);
|
|
const approve = useInboxStore((state: InboxState) => state.approve);
|
|
const reject = useInboxStore((state: InboxState) => state.reject);
|
|
const [pendingApprovalId, setPendingApprovalId] = useState<string | null>(null);
|
|
const [reviewNote, setReviewNote] = useState('');
|
|
|
|
useEffect(() => {
|
|
void hydrate();
|
|
}, [hydrate]);
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<Text style={styles.title}>Inbox</Text>
|
|
<Text style={styles.body}>Review simple agent actions and approve or reject them from mobile.</Text>
|
|
{isLoading ? <Text style={styles.empty}>Loading approvals…</Text> : null}
|
|
{approvals.length > 0 ? (
|
|
<TextInput
|
|
{...textInputA11y('Review note')}
|
|
style={styles.reviewNoteInput}
|
|
placeholder="Optional review note…"
|
|
placeholderTextColor={colors.textSecondary}
|
|
value={reviewNote}
|
|
onChangeText={setReviewNote}
|
|
multiline
|
|
numberOfLines={2}
|
|
/>
|
|
) : null}
|
|
{approvals.map((item: ApprovalItem) => (
|
|
<View key={item.id} style={styles.card}>
|
|
<Text style={styles.cardTitle}>{item.title}</Text>
|
|
<Text style={styles.cardBody}>{item.summary}</Text>
|
|
<Text style={styles.status}>Status: {item.status}</Text>
|
|
<View style={styles.actionRow}>
|
|
<Pressable
|
|
{...buttonA11y(`Approve ${item.title}`, { disabled: pendingApprovalId === item.id || item.status !== 'pending' })}
|
|
style={[
|
|
styles.approveButton,
|
|
pendingApprovalId === item.id || item.status !== 'pending' ? styles.buttonDisabled : null,
|
|
]}
|
|
disabled={pendingApprovalId === item.id || item.status !== 'pending'}
|
|
onPress={async () => {
|
|
setPendingApprovalId(item.id);
|
|
try {
|
|
const note = reviewNote.trim() || undefined;
|
|
await approve(item.id, note);
|
|
setReviewNote('');
|
|
} finally {
|
|
setPendingApprovalId(null);
|
|
}
|
|
}}
|
|
>
|
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.approveText}>
|
|
{pendingApprovalId === item.id ? 'Approving…' : 'Approve'}
|
|
</Text>
|
|
</Pressable>
|
|
<Pressable
|
|
{...buttonA11y(`Reject ${item.title}`, { disabled: pendingApprovalId === item.id || item.status !== 'pending' })}
|
|
style={[
|
|
styles.rejectButton,
|
|
pendingApprovalId === item.id || item.status !== 'pending' ? styles.buttonDisabled : null,
|
|
]}
|
|
disabled={pendingApprovalId === item.id || item.status !== 'pending'}
|
|
onPress={async () => {
|
|
setPendingApprovalId(item.id);
|
|
try {
|
|
const note = reviewNote.trim() || undefined;
|
|
await reject(item.id, note);
|
|
setReviewNote('');
|
|
} finally {
|
|
setPendingApprovalId(null);
|
|
}
|
|
}}
|
|
>
|
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.rejectText}>
|
|
{pendingApprovalId === item.id ? 'Rejecting…' : 'Reject'}
|
|
</Text>
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
))}
|
|
{!isLoading && approvals.length === 0 ? (
|
|
<Text style={styles.empty}>No approval items are waiting right now.</Text>
|
|
) : null}
|
|
<View style={styles.feedSection}>
|
|
<Text style={styles.feedTitle}>Recent activity</Text>
|
|
{activity.map((item: ActivityItem) => (
|
|
<View key={item.id} style={styles.feedCard}>
|
|
<Text style={styles.feedKind}>{item.kind}</Text>
|
|
<Text style={styles.cardTitle}>{item.title}</Text>
|
|
<Text style={styles.cardBody}>{item.summary}</Text>
|
|
</View>
|
|
))}
|
|
{!isLoading && activity.length === 0 ? (
|
|
<Text style={styles.empty}>No recent agent activity yet.</Text>
|
|
) : null}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
padding: 20,
|
|
backgroundColor: colors.bgCanvas,
|
|
gap: 14,
|
|
},
|
|
title: {
|
|
color: colors.textPrimary,
|
|
fontSize: 28,
|
|
fontWeight: '700',
|
|
},
|
|
body: {
|
|
color: colors.textSecondary,
|
|
fontSize: 15,
|
|
lineHeight: 21,
|
|
},
|
|
empty: {
|
|
color: colors.textSecondary,
|
|
fontSize: 14,
|
|
},
|
|
card: {
|
|
backgroundColor: colors.surfaceCard,
|
|
borderRadius: 16,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderDefault,
|
|
padding: 16,
|
|
gap: 8,
|
|
},
|
|
cardTitle: {
|
|
color: colors.textPrimary,
|
|
fontSize: 16,
|
|
fontWeight: '700',
|
|
},
|
|
cardBody: {
|
|
color: colors.textSecondary,
|
|
fontSize: 14,
|
|
},
|
|
status: {
|
|
color: colors.textSecondary,
|
|
fontSize: 13,
|
|
textTransform: 'capitalize',
|
|
},
|
|
actionRow: {
|
|
flexDirection: 'row',
|
|
gap: 10,
|
|
},
|
|
buttonDisabled: {
|
|
opacity: 0.6,
|
|
},
|
|
approveButton: {
|
|
backgroundColor: colors.accentPrimary,
|
|
borderRadius: 10,
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 10,
|
|
},
|
|
approveText: {
|
|
color: colors.textPrimary,
|
|
fontWeight: '700',
|
|
},
|
|
rejectButton: {
|
|
borderWidth: 1,
|
|
borderColor: colors.borderDefault,
|
|
borderRadius: 10,
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 10,
|
|
},
|
|
rejectText: {
|
|
color: colors.textSecondary,
|
|
fontWeight: '600',
|
|
},
|
|
feedSection: {
|
|
gap: 10,
|
|
},
|
|
feedTitle: {
|
|
color: colors.textPrimary,
|
|
fontSize: 18,
|
|
fontWeight: '700',
|
|
},
|
|
feedCard: {
|
|
backgroundColor: colors.bgElevated,
|
|
borderRadius: 14,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderDefault,
|
|
padding: 14,
|
|
gap: 6,
|
|
},
|
|
feedKind: {
|
|
color: colors.accentSecondary,
|
|
fontSize: 12,
|
|
fontWeight: '700',
|
|
textTransform: 'uppercase',
|
|
},
|
|
reviewNoteInput: {
|
|
backgroundColor: colors.bgElevated,
|
|
borderRadius: 12,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderDefault,
|
|
color: colors.textPrimary,
|
|
padding: 12,
|
|
fontSize: 14,
|
|
minHeight: 48,
|
|
textAlignVertical: 'top' as const,
|
|
},
|
|
});
|