mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-10-05 01:22:02 +00:00
Fix React version mismatch, auth fallback, and UI polish
Pin React to 19.2.3 (exact version RN 0.85.3's bundled renderer requires — 19.2.6 caused "Cannot read property 'default' of undefined" crash at getPaperRenderer). Auth: fall back to parsing user info from Cognito ID token when backend API is unreachable, removing the hard dependency on localhost:5000 for credential login. UI fixes: - Safe area insets on Settings screen (top edge was missing) - Separate refresh indicator from filter-triggered loading on proposal queue (chips no longer trigger pull-to-refresh animation) - Dashboard welcome shows first name instead of full email - Dashboard/AdminDashboard remove duplicate top safe area when navigation header already provides it - Service category selector uses wrapping chips instead of cramped SegmentedButtons - Add ErrorBoundary to App root for crash visibility
This commit is contained in:
parent
bd9fbb4da5
commit
686b42330c
12 changed files with 1068 additions and 874 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -8,6 +8,8 @@
|
||||||
<string>Sea Haven Proposals</string>
|
<string>Sea Haven Proposals</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIconName</key>
|
||||||
|
<string>AppIcon</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
|
@ -20,6 +22,17 @@
|
||||||
<string>$(MARKETING_VERSION)</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>com.seahavenind.proposals</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>com.seahavenind.proposals</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
|
@ -31,35 +44,14 @@
|
||||||
<key>NSAllowsLocalNetworking</key>
|
<key>NSAllowsLocalNetworking</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
<key>CFBundleURLTypes</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleURLSchemes</key>
|
|
||||||
<array>
|
|
||||||
<string>com.seahavenind.proposals</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleURLName</key>
|
|
||||||
<string>com.seahavenind.proposals</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>Sea Haven Proposals needs camera access to capture vendor proposal documents.</string>
|
<string>Sea Haven Proposals needs camera access to capture vendor proposal documents.</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
|
||||||
<string>Sea Haven Proposals needs photo library access to attach vendor proposal images.</string>
|
|
||||||
<key>NSFaceIDUsageDescription</key>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
<string>Sea Haven Proposals uses Face ID to secure your login session.</string>
|
<string>Sea Haven Proposals uses Face ID to secure your login session.</string>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>LaunchScreen</string>
|
<string>Sea Haven Proposals needs photo library access to attach vendor proposal images.</string>
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
<key>RCTNewArchEnabled</key>
|
||||||
<array>
|
<true/>
|
||||||
<string>arm64</string>
|
|
||||||
</array>
|
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>UIAppFonts</key>
|
<key>UIAppFonts</key>
|
||||||
<array>
|
<array>
|
||||||
<string>MaterialCommunityIcons.ttf</string>
|
<string>MaterialCommunityIcons.ttf</string>
|
||||||
|
|
@ -82,9 +74,19 @@
|
||||||
<string>FontAwesome6_Brands.ttf</string>
|
<string>FontAwesome6_Brands.ttf</string>
|
||||||
<string>MaterialIcons.ttf</string>
|
<string>MaterialIcons.ttf</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
|
<array>
|
||||||
|
<string>arm64</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>CFBundleIconName</key>
|
|
||||||
<string>AppIcon</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
||||||
9
mobile/package-lock.json
generated
9
mobile/package-lock.json
generated
|
|
@ -7,6 +7,7 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "proposal-system-mobile",
|
"name": "proposal-system-mobile",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@proposal-system/api-contracts": "file:../shared/api-contracts",
|
"@proposal-system/api-contracts": "file:../shared/api-contracts",
|
||||||
"@react-native-async-storage/async-storage": "^2.1.0",
|
"@react-native-async-storage/async-storage": "^2.1.0",
|
||||||
|
|
@ -18,7 +19,7 @@
|
||||||
"@tanstack/react-query": "^5.100.10",
|
"@tanstack/react-query": "^5.100.10",
|
||||||
"amazon-cognito-identity-js": "^6.3.0",
|
"amazon-cognito-identity-js": "^6.3.0",
|
||||||
"axios": "^1.16.0",
|
"axios": "^1.16.0",
|
||||||
"react": "^19.0.0",
|
"react": "19.2.3",
|
||||||
"react-native": "^0.85.3",
|
"react-native": "^0.85.3",
|
||||||
"react-native-app-auth": "^8.0.0",
|
"react-native-app-auth": "^8.0.0",
|
||||||
"react-native-document-picker": "^9.3.0",
|
"react-native-document-picker": "^9.3.0",
|
||||||
|
|
@ -5835,9 +5836,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.2.6",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||||
"integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
|
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
"@tanstack/react-query": "^5.100.10",
|
"@tanstack/react-query": "^5.100.10",
|
||||||
"amazon-cognito-identity-js": "^6.3.0",
|
"amazon-cognito-identity-js": "^6.3.0",
|
||||||
"axios": "^1.16.0",
|
"axios": "^1.16.0",
|
||||||
"react": "^19.0.0",
|
"react": "19.2.3",
|
||||||
"react-native": "^0.85.3",
|
"react-native": "^0.85.3",
|
||||||
"react-native-app-auth": "^8.0.0",
|
"react-native-app-auth": "^8.0.0",
|
||||||
"react-native-document-picker": "^9.3.0",
|
"react-native-document-picker": "^9.3.0",
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, Component, ErrorInfo, ReactNode } from 'react';
|
||||||
import { Provider as ReduxProvider, useDispatch } from 'react-redux';
|
import { Provider as ReduxProvider, useDispatch } from 'react-redux';
|
||||||
import { PaperProvider } from 'react-native-paper';
|
import { PaperProvider } from 'react-native-paper';
|
||||||
import { QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||||
import NetInfo from '@react-native-community/netinfo';
|
import NetInfo from '@react-native-community/netinfo';
|
||||||
import { Alert } from 'react-native';
|
import { Alert, Text, View } from 'react-native';
|
||||||
|
|
||||||
import { store } from './store';
|
import { store } from './store';
|
||||||
import { theme } from './theme';
|
import { theme } from './theme';
|
||||||
|
|
@ -15,6 +15,26 @@ import { setUser, setLoading } from './store/slices/authSlice';
|
||||||
import { processOfflineQueue } from './hooks/useOfflineDraft';
|
import { processOfflineQueue } from './hooks/useOfflineDraft';
|
||||||
import { proposalsApi, CreateProposalRequest } from './lib/api/proposals';
|
import { proposalsApi, CreateProposalRequest } from './lib/api/proposals';
|
||||||
|
|
||||||
|
class ErrorBoundary extends Component<{ children: ReactNode }, { error: Error | null }> {
|
||||||
|
state = { error: null as Error | null };
|
||||||
|
static getDerivedStateFromError(error: Error) { return { error }; }
|
||||||
|
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||||
|
console.error('ErrorBoundary caught:', error, info.componentStack);
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
if (this.state.error) {
|
||||||
|
return (
|
||||||
|
<View style={{ flex: 1, justifyContent: 'center', padding: 32, backgroundColor: '#fff' }}>
|
||||||
|
<Text style={{ fontSize: 18, fontWeight: 'bold', color: 'red', marginBottom: 8 }}>App Error</Text>
|
||||||
|
<Text style={{ fontSize: 14, color: '#333' }}>{this.state.error.message}</Text>
|
||||||
|
<Text style={{ fontSize: 12, color: '#666', marginTop: 8 }}>{this.state.error.stack?.slice(0, 500)}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function AuthBootstrap({ children }: { children: React.ReactNode }) {
|
function AuthBootstrap({ children }: { children: React.ReactNode }) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
|
@ -61,16 +81,18 @@ function AuthBootstrap({ children }: { children: React.ReactNode }) {
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<ReduxProvider store={store}>
|
<ErrorBoundary>
|
||||||
<QueryClientProvider client={queryClient}>
|
<ReduxProvider store={store}>
|
||||||
<PaperProvider theme={theme}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<SafeAreaProvider>
|
<PaperProvider theme={theme}>
|
||||||
<AuthBootstrap>
|
<SafeAreaProvider>
|
||||||
<RootNavigator />
|
<AuthBootstrap>
|
||||||
</AuthBootstrap>
|
<RootNavigator />
|
||||||
</SafeAreaProvider>
|
</AuthBootstrap>
|
||||||
</PaperProvider>
|
</SafeAreaProvider>
|
||||||
</QueryClientProvider>
|
</PaperProvider>
|
||||||
</ReduxProvider>
|
</QueryClientProvider>
|
||||||
|
</ReduxProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ export function usePaginatedList<T>(
|
||||||
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
|
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
|
||||||
const [totalCount, setTotalCount] = useState(0);
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [err, setErr] = useState('');
|
const [err, setErr] = useState('');
|
||||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
|
@ -37,8 +38,7 @@ export function usePaginatedList<T>(
|
||||||
};
|
};
|
||||||
}, [search]);
|
}, [search]);
|
||||||
|
|
||||||
const reload = useCallback(() => {
|
const fetchData = useCallback(() => {
|
||||||
setLoading(true);
|
|
||||||
setErr('');
|
setErr('');
|
||||||
fetchFn({ search: debouncedSearch, page, pageSize, ...extraParams })
|
fetchFn({ search: debouncedSearch, page, pageSize, ...extraParams })
|
||||||
.then(({ items, totalCount: total }) => {
|
.then(({ items, totalCount: total }) => {
|
||||||
|
|
@ -46,13 +46,22 @@ export function usePaginatedList<T>(
|
||||||
setTotalCount(total);
|
setTotalCount(total);
|
||||||
})
|
})
|
||||||
.catch((e: Error) => setErr(e.message || 'Failed to load'))
|
.catch((e: Error) => setErr(e.message || 'Failed to load'))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
setRefreshing(false);
|
||||||
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [fetchFn, debouncedSearch, page, pageSize, extraKey]);
|
}, [fetchFn, debouncedSearch, page, pageSize, extraKey]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reload();
|
setLoading(true);
|
||||||
}, [reload]);
|
fetchData();
|
||||||
|
}, [fetchData]);
|
||||||
|
|
||||||
|
const reload = useCallback(() => {
|
||||||
|
setRefreshing(true);
|
||||||
|
fetchData();
|
||||||
|
}, [fetchData]);
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
|
@ -65,9 +74,10 @@ export function usePaginatedList<T>(
|
||||||
setPageSize,
|
setPageSize,
|
||||||
totalCount,
|
totalCount,
|
||||||
loading,
|
loading,
|
||||||
|
refreshing,
|
||||||
err,
|
err,
|
||||||
reload,
|
reload,
|
||||||
}),
|
}),
|
||||||
[rows, search, page, pageSize, totalCount, loading, err, reload],
|
[rows, search, page, pageSize, totalCount, loading, refreshing, err, reload],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,22 @@ import {
|
||||||
StoredUser,
|
StoredUser,
|
||||||
} from '../storage';
|
} from '../storage';
|
||||||
|
|
||||||
|
function parseUserFromIdToken(idToken: string): StoredUser {
|
||||||
|
const base64Url = idToken.split('.')[1];
|
||||||
|
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
const payload = JSON.parse(atob(base64));
|
||||||
|
const groups: string[] = payload['cognito:groups'] || [];
|
||||||
|
const role = groups.includes('admins') || groups.includes('sysadmins')
|
||||||
|
? 'Admin'
|
||||||
|
: 'Dispatcher';
|
||||||
|
return {
|
||||||
|
id: payload.sub,
|
||||||
|
email: payload.email,
|
||||||
|
displayName: payload.name || payload.email,
|
||||||
|
role: role as StoredUser['role'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const cognitoConfig = {
|
const cognitoConfig = {
|
||||||
clientId: Config.COGNITO_CLIENT_ID,
|
clientId: Config.COGNITO_CLIENT_ID,
|
||||||
redirectUrl: Config.COGNITO_REDIRECT_URI,
|
redirectUrl: Config.COGNITO_REDIRECT_URI,
|
||||||
|
|
@ -53,7 +69,12 @@ export const authApi = {
|
||||||
};
|
};
|
||||||
await tokenStorage.save(tokens);
|
await tokenStorage.save(tokens);
|
||||||
|
|
||||||
const profile = await authApi.getMe();
|
let profile: StoredUser;
|
||||||
|
try {
|
||||||
|
profile = await authApi.getMe();
|
||||||
|
} catch {
|
||||||
|
profile = parseUserFromIdToken(cognitoTokens.idToken);
|
||||||
|
}
|
||||||
await userStorage.save(profile);
|
await userStorage.save(profile);
|
||||||
return profile;
|
return profile;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import type { CognitoUserSession } from 'amazon-cognito-identity-js';
|
|
||||||
import Config from '../../config';
|
import Config from '../../config';
|
||||||
|
|
||||||
export interface CognitoTokens {
|
export interface CognitoTokens {
|
||||||
|
|
@ -8,7 +7,8 @@ export interface CognitoTokens {
|
||||||
expiresAt: string;
|
expiresAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractTokens(session: CognitoUserSession): CognitoTokens {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
function extractTokens(session: any): CognitoTokens {
|
||||||
const accessToken = session.getAccessToken();
|
const accessToken = session.getAccessToken();
|
||||||
return {
|
return {
|
||||||
accessToken: accessToken.getJwtToken(),
|
accessToken: accessToken.getJwtToken(),
|
||||||
|
|
@ -22,7 +22,6 @@ export async function authenticateWithCredentials(
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string,
|
||||||
): Promise<CognitoTokens> {
|
): Promise<CognitoTokens> {
|
||||||
// Lazy-load to keep amazon-cognito-identity-js out of the startup module graph
|
|
||||||
const {
|
const {
|
||||||
CognitoUserPool,
|
CognitoUserPool,
|
||||||
CognitoUser,
|
CognitoUser,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export function SettingsScreen() {
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container} edges={['bottom']}>
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<Card style={styles.card} mode="elevated">
|
<Card style={styles.card} mode="elevated">
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export function ProposalQueueScreen() {
|
||||||
search,
|
search,
|
||||||
setSearch,
|
setSearch,
|
||||||
loading,
|
loading,
|
||||||
|
refreshing,
|
||||||
reload,
|
reload,
|
||||||
} = usePaginatedList<ProposalListItem>(
|
} = usePaginatedList<ProposalListItem>(
|
||||||
(params) =>
|
(params) =>
|
||||||
|
|
@ -90,7 +91,7 @@ export function ProposalQueueScreen() {
|
||||||
<ProposalCard proposal={item} onPress={() => handlePress(item)} />
|
<ProposalCard proposal={item} onPress={() => handlePress(item)} />
|
||||||
)}
|
)}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl refreshing={loading} onRefresh={reload} />
|
<RefreshControl refreshing={refreshing} onRefresh={reload} />
|
||||||
}
|
}
|
||||||
contentContainerStyle={rows.length === 0 ? styles.empty : styles.list}
|
contentContainerStyle={rows.length === 0 ? styles.empty : styles.list}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ export function DashboardScreen() {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Text variant="headlineSmall" style={styles.welcome}>
|
<Text variant="headlineSmall" style={styles.welcome}>
|
||||||
Welcome, {user?.displayName?.split(' ')[0] || 'there'}
|
Welcome, {user?.displayName?.includes('@') ? user.displayName.split('@')[0] : user?.displayName?.split(' ')[0] || 'there'}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{stats && (
|
{stats && (
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
TextInput,
|
TextInput,
|
||||||
Button,
|
Button,
|
||||||
Text,
|
Text,
|
||||||
|
Chip,
|
||||||
SegmentedButtons,
|
SegmentedButtons,
|
||||||
Snackbar,
|
Snackbar,
|
||||||
} from 'react-native-paper';
|
} from 'react-native-paper';
|
||||||
|
|
@ -146,16 +147,19 @@ export function NewProposalScreen() {
|
||||||
<Text variant="labelLarge" style={styles.label}>
|
<Text variant="labelLarge" style={styles.label}>
|
||||||
Service Category
|
Service Category
|
||||||
</Text>
|
</Text>
|
||||||
<SegmentedButtons
|
<View style={styles.chipRow}>
|
||||||
value={draft.serviceCategory}
|
{SERVICE_CATEGORIES.map((c) => (
|
||||||
onValueChange={(v) => updateDraft({ serviceCategory: v })}
|
<Chip
|
||||||
buttons={SERVICE_CATEGORIES.map((c) => ({
|
key={c}
|
||||||
value: c,
|
selected={draft.serviceCategory === c}
|
||||||
label: c,
|
showSelectedOverlay
|
||||||
}))}
|
onPress={() => updateDraft({ serviceCategory: c })}
|
||||||
style={styles.segmented}
|
style={styles.chip}
|
||||||
density="small"
|
>
|
||||||
/>
|
{c}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
<Text variant="labelLarge" style={styles.label}>
|
<Text variant="labelLarge" style={styles.label}>
|
||||||
Priority
|
Priority
|
||||||
|
|
@ -247,6 +251,15 @@ const styles = StyleSheet.create({
|
||||||
segmented: {
|
segmented: {
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
|
chipRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 8,
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
chip: {
|
||||||
|
marginBottom: 0,
|
||||||
|
},
|
||||||
hint: {
|
hint: {
|
||||||
color: '#9E9E9E',
|
color: '#9E9E9E',
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue