Skip to content

Commit

Permalink
Feature create issue (#40)
Browse files Browse the repository at this point in the history
* added issue screen

* added issues tab bar and issue list

* updated issuesList

* navigate create issue screen

* added dropdown component

* after change value, give changed item

* fixed post request

* Checked validation

* fix id

* checked update and set value

* After adding data, list is updated

* added delete function

* added confirm, toast

* checked edit

* fixed ui

* fix data

* fix category, type

* fixed did not appeared value

* fixed validation
  • Loading branch information
doljko authored Mar 12, 2024
1 parent 498a1b6 commit 9282168
Show file tree
Hide file tree
Showing 10 changed files with 492 additions and 4 deletions.
76 changes: 76 additions & 0 deletions src/components/DropdownActionSheet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { createRef, useState, useEffect } from 'react';
import { ScrollView, Text, TouchableOpacity, View } from 'react-native';
import ActionSheet from 'react-native-actions-sheet';
import { faAngleDown, faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome';
import { useNavigation } from '@react-navigation/native';
import tailwind from 'tailwind';

import { getColorCode } from 'utils';

const DropdownActionSheet = ({ items, onChange, title, value }) => {
const actionSheetRef = createRef();
const navigation = useNavigation();
const [selectedItem, setSelectedItem] = useState(null);

useEffect(() => {
const selected = items.find(item => item.value === value);
if (selected) {
setSelectedItem(selected);
}
}, [value]);

const handleItemSelection = item => {
setSelectedItem(item);
onChange(item.value);
actionSheetRef.current?.hide();
};

return (
<View style={tailwind('mb-4')}>
<TouchableOpacity onPress={() => actionSheetRef.current?.setModalVisible()}>
<View style={tailwind('flex flex-row items-center justify-between pb-1 bg-gray-900 border border-gray-700 rounded-lg px-2')}>
<View style={tailwind('border-blue-700 py-2 pr-4 flex flex-row items-center')}>
<Text style={[tailwind('font-semibold text-blue-50 text-base'), selectedItem && tailwind('px-2')]}>{selectedItem ? selectedItem.label : title}</Text>
</View>
<View style={tailwind('flex flex-row items-center')}>{<FontAwesomeIcon icon={faAngleDown} style={tailwind('text-white')} />}</View>
</View>
</TouchableOpacity>
<ActionSheet
gestureEnabled={true}
bounceOnOpen={true}
nestedScrollEnabled={true}
onMomentumScrollEnd={() => actionSheetRef.current?.handleChildScrollEnd()}
ref={actionSheetRef}
containerStyle={tailwind('bg-gray-800')}
indicatorColor={getColorCode('text-gray-900')}>
<View>
<View style={tailwind('px-5 py-2 flex flex-row items-center justify-between mb-2')}>
<View style={tailwind('flex flex-row items-center')}>
<Text style={tailwind('text-lg text-white font-semibold')}>{selectedItem ? selectedItem.label : title}</Text>
</View>
<View>
<TouchableOpacity onPress={() => actionSheetRef.current?.hide()}>
<View style={tailwind('rounded-full bg-red-700 w-8 h-8 flex items-center justify-center')}>
<FontAwesomeIcon icon={faTimes} style={tailwind('text-red-100')} />
</View>
</TouchableOpacity>
</View>
</View>
<ScrollView showsHorizontalScrollIndicator={false} showsVerticalScrollIndicator={false}>
{items?.map(item => (
<TouchableOpacity key={item.value} onPress={() => handleItemSelection(item)}>
<View style={tailwind('flex flex-row items-center px-5 py-4 border-b border-gray-900')}>
<Text style={tailwind('font-semibold text-lg text-gray-100')}>{item.label}</Text>
</View>
</TouchableOpacity>
))}
<View style={tailwind('w-full h-40')}></View>
</ScrollView>
</View>
</ActionSheet>
</View>
);
};

export default DropdownActionSheet;
28 changes: 28 additions & 0 deletions src/constant/Enum.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const IssueType = {
VEHICLE: 'Vehicle',
DRIVER: 'Driver',
ROUTE: 'Route',
PAYLOAD_CARGO: 'Payload Cargo',
SOFTWARE_TECHNICAL: 'Software Technical',
OPERATIONAL: 'Operational',
CUSTOMER: 'Customer',
SECURITY: 'Security',
ENVIRONMENTAL_SUSTAINABILITY: 'Environmental Sustainability',
};
const IssueCategory = {
Compliance: 'Compliance',
ResourceAllocation: 'Resource Allocation',
CostOverruns: 'Cost Overruns',
Communication: 'Communication',
VendorManagementIssue: 'Vendor Management Issue',
};

const IssuePriority = {
LOW: 'Low',
MEDIUM: 'Medium',
HIGH: 'High',
CRITICAL: 'Critical',
SCHEDULED_MAINTENANCE: 'Scheduled Maintenance',
OPERATIONAL_SUGGESTION: 'Operational Suggestion',
};
export { IssuePriority, IssueType, IssueCategory };
20 changes: 20 additions & 0 deletions src/constant/GetIssueCategoy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export default function getIssueCategories(type = null, options) {
const issueCategories = {
VEHICLE: ['Mechanical Problems', 'Cosmetic Damages', 'Tire Issues', 'Electronics and Instruments', 'Maintenance Alerts', 'Fuel Efficiency Issues'],
DRIVER: ['Behavior Concerns', 'Documentation', 'Time Management', 'Communication', 'Training Needs', 'Health and Safety Violations'],
ROUTE: ['Inefficient Routes', 'Safety Concerns', 'Blocked Routes', 'Environmental Considerations', 'Unfavorable Weather Conditions'],
PAYLOAD_CARGO: ['Damaged Goods', 'Misplaced Goods', 'Documentation Issues', 'Temperature-Sensitive Goods', 'Incorrect Cargo Loading'],
SOFTWARE_TECHNICAL: ['Bugs', 'UI/UX Concerns', 'Integration Failures', 'Performance', 'Feature Requests', 'Security Vulnerabilities'],
OPERATIONAL: ['Compliance', 'Resource Allocation', 'Cost Overruns', 'Communication', 'Vendor Management Issues'],
CUSTOMER: ['Service Quality', 'Billing Discrepancies', 'Communication Breakdown', 'Feedback and Suggestions', 'Order Errors'],
SECURITY: ['Unauthorized Access', 'Data Concerns', 'Physical Security', 'Data Integrity Issues'],
ENVIRONMENTAL_SUSTAINABILITY: ['Fuel Consumption', 'Carbon Footprint', 'Waste Management', 'Green Initiatives Opportunities'],
};

if (type) {
return issueCategories[type] || [];
}

const allIssueCategories = Object.values(issueCategories).flat();
return allIssueCategories;
}
2 changes: 1 addition & 1 deletion src/features/Account/AccountStack.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const AccountStack = ({ route }) => {
<RootStack.Screen name="SearchScreen" component={SearchScreen} options={{ headerShown: false }} initialParams={route.params ?? {}} />
<RootStack.Screen name="ConfigScreen" component={ConfigScreen} options={{ headerShown: false }} />
<RootStack.Screen name="Organization" component={OrganizationScreen} options={{ headerShown: false }} />
</RootStack.Navigator>
</RootStack.Navigator>
</SafeAreaProvider>
);
};
Expand Down
2 changes: 1 addition & 1 deletion src/features/Account/screens/AccountScreen.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { faBuilding, faChevronRight, faIdBadge, faLink, faUser } from '@fortawesome/free-solid-svg-icons';
import { faBuilding, faChevronRight, faIdBadge, faLink, faUser, faFileAlt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome';
import { useFleetbase } from 'hooks';
import React, { useEffect, useState } from 'react';
Expand Down
2 changes: 2 additions & 0 deletions src/features/Core/MainStack.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import OrderScreen from 'shared/OrderScreen';
import ProofScreen from 'shared/ProofScreen';
import MainScreen from './screens/MainScreen';
import SearchScreen from './screens/SearchScreen';
import IssueScreen from './screens/IssueScreen';

const RootStack = createStackNavigator();

Expand All @@ -28,6 +29,7 @@ const MainStack = ({ route }) => {
<RootStack.Screen name="EntityScreen" component={EntityScreen} options={{ headerShown: false }} initialParams={route.params ?? {}} />
<RootStack.Screen name="ProofScreen" component={ProofScreen} options={{ headerShown: false }} initialParams={route.params ?? {}} />
<RootStack.Screen name="SearchScreen" component={SearchScreen} options={{ headerShown: false }} initialParams={route.params ?? {}} />
<RootStack.Screen name="IssueScreen" component={IssueScreen} options={{ headerShown: false }} initialParams={route.params ?? {}} />
</RootStack.Group>
</RootStack.Navigator>
</SafeAreaProvider>
Expand Down
239 changes: 239 additions & 0 deletions src/features/Core/screens/IssueScreen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import { faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome';
import { IssuePriority, IssueType } from 'constant/Enum';
import { useDriver, useFleetbase } from 'hooks';
import React, { useEffect, useState } from 'react';
import { ActivityIndicator, Alert, Keyboard, KeyboardAvoidingView, Pressable, Text, TextInput, TouchableOpacity, View } from 'react-native';
import Toast from 'react-native-toast-message';
import tailwind from 'tailwind';
import { getColorCode, getCurrentLocation, logError, translate } from 'utils';
import DropdownActionSheet from '../../../components/DropdownActionSheet';
import getIssueCategories from '../../../constant/GetIssueCategoy';

const IssueScreen = ({ navigation, route }) => {
const issue = route.params;
const isEdit = route.params;
const [isLoading, setIsLoading] = useState(false);
const fleetbase = useFleetbase();
const [driver] = useDriver();
const [driverId] = useState(driver.getAttribute('id'));

const [type, setType] = useState(issue.type);
const [categories, setCategories] = useState([]);
const [category, setCategory] = useState();
const [priority, setPriority] = useState();
const [report, setReport] = useState(issue.report);
const [error, setError] = useState('');

useEffect(() => {
if (issue) {
setCategory(issue.issue?.category);
setPriority(issue.issue?.priority);
setReport(issue.issue?.report);
setType(issue.issue?.type);
}
}, []);

useEffect(() => {
if (!type) return;

setCategories(getIssueCategories(type));
}, [type]);

const saveIssue = () => {
if (!validateInputs()) {
return;
}
setIsLoading(true);
const location = getCurrentLocation().then();
const adapter = fleetbase.getAdapter();

if (issue.issue?.id) {
adapter
.put(`issues/${issue.issue.id}`, {
type,
category,
priority,
report,
location: location,
driver: driverId,
})
.then(() => {
Toast.show({
type: 'success',
text1: `Successfully updated`,
});
setIsLoading(false);
navigation.goBack();
})
.catch(error => {
setIsLoading(false);
logError(error);
});
} else {
adapter
.post('issues', {
type,
category,
priority,
report,
location: location,
driver: driverId,
})
.then(() => {
Toast.show({
type: 'success',
text1: `Successfully created`,
});
setIsLoading(false);
navigation.goBack();
})
.catch(error => {
setIsLoading(false);
logError(error);
});
}
};

const deleteIssues = () => {
Alert.alert('Confirmation', 'Are you sure you want to delete this issue?', [
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Delete',
onPress: () => confirmDelete(),
},
]);
};

const confirmDelete = () => {
const adapter = fleetbase.getAdapter();
adapter
.delete(`issues/${issue.issue.id}`)
.then(() => {
Toast.show({
type: 'success',
text1: `Successfully deleted`,
});
setIsLoading(false);
navigation.goBack();
})
.catch(error => {
setIsLoading(false);
logError(error);
});
};

const validateInputs = () => {
if (!type || !category || !priority || !report?.trim()) {
setError('Please enter a required value.');
return false;
} else if (report.trim().length === 0) {
setError('Report cannot be empty.');
return false;
}
setError('');
return true;
};

return (
<View style={[tailwind('w-full h-full bg-gray-800')]}>
<Pressable onPress={Keyboard.dismiss} style={tailwind('w-full h-full relative')}>
<View style={tailwind('flex flex-row items-center justify-between p-4')}>
{issue.isEdit ? (
<Text style={tailwind('text-xl text-gray-50 font-semibold')}>{translate('Core.IssueScreen.update')}</Text>
) : (
<Text style={tailwind('text-xl text-gray-50 font-semibold')}>{translate('Core.IssueScreen.create')}</Text>
)}
<TouchableOpacity onPress={() => navigation.goBack()} style={tailwind('mr-4')}>
<View style={tailwind('rounded-full bg-gray-900 w-10 h-10 flex items-center justify-center')}>
<FontAwesomeIcon icon={faTimes} style={tailwind('text-red-400')} />
</View>
</TouchableOpacity>
</View>
<View style={tailwind('flex w-full h-full')}>
<KeyboardAvoidingView style={tailwind('p-4')}>
<View style={isEdit.isEdit ? tailwind('flex flex-row items-center justify-between pb-1') : {}}>
<Text style={tailwind('font-semibold text-base text-gray-50 mb-2')}>{translate('Core.IssueScreen.type')}</Text>
{isEdit.isEdit ? (
<Text style={tailwind('text-white')}>{type}</Text>
) : (
<DropdownActionSheet
value={type}
items={Object.keys(IssueType).map(type => {
return { label: IssueType[type], value: type };
})}
onChange={setType}
title={translate('Core.IssueScreen.selectType')}
/>
)}

{error && !type ? <Text style={tailwind('text-red-500 mb-2')}>{error}</Text> : null}
</View>

<View style={isEdit.isEdit ? tailwind('flex flex-row items-center justify-between pb-1') : {}}>
<Text style={tailwind('font-semibold text-base text-gray-50 mb-2')}>{translate('Core.IssueScreen.category')}</Text>
{isEdit.isEdit ? (
<Text style={tailwind('text-white')}>{category}</Text>
) : (
<DropdownActionSheet
value={category}
items={categories?.map(category => {
return { label: category, value: category };
})}
onChange={setCategory}
title={'Select category'}
/>
)}
{error && !category ? <Text style={tailwind('text-red-500 mb-2')}>{error}</Text> : null}
</View>
<View style={tailwind('mb-4')}>
<Text style={tailwind('font-semibold text-base text-gray-50 mb-2')}>{translate('Core.IssueScreen.report')}</Text>
<TextInput
value={report}
onChangeText={setReport}
numberOfLines={4}
multiline={true}
placeholder={translate('Core.IssueScreen.enterReport')}
placeholderTextColor={getColorCode('text-gray-600')}
style={tailwind('form-input text-white h-28')}
/>
{error && !report?.trim() ? <Text style={tailwind('text-red-500 mb-2')}>{error}</Text> : null}
</View>
<View>
<Text style={tailwind('font-semibold text-base text-gray-50 mb-2')}>{translate('Core.IssueScreen.priority')}</Text>
<DropdownActionSheet
value={priority}
items={Object.keys(IssuePriority).map(priority => {
return { label: IssuePriority[priority], value: priority };
})}
onChange={setPriority}
title={translate('Core.IssueScreen.selectPriority')}
/>

{error && !priority ? <Text style={tailwind('text-red-500 mb-2')}>{error}</Text> : null}
</View>

<TouchableOpacity onPress={saveIssue} disabled={isLoading} style={tailwind('flex')}>
<View style={tailwind('btn bg-gray-900 border border-gray-700 mt-4 ')}>
{isLoading && <ActivityIndicator color={getColorCode('text-gray-50')} style={tailwind('mr-2')} />}
<Text style={tailwind('font-semibold text-lg text-gray-50 text-center')}>{translate('Core.IssueScreen.save')}</Text>
</View>
</TouchableOpacity>
{isEdit.isEdit && (
<TouchableOpacity onPress={deleteIssues} disabled={isLoading} style={tailwind('flex')}>
<View style={tailwind('btn bg-gray-900 border border-gray-700 mt-4')}>
<Text style={tailwind('font-semibold text-lg text-gray-50 text-center')}>Delete</Text>
</View>
</TouchableOpacity>
)}
</KeyboardAvoidingView>
</View>
</Pressable>
</View>
);
};

export default IssueScreen;
Loading

0 comments on commit 9282168

Please sign in to comment.