Delete unused code, remove logs, add dnd package.

This commit is contained in:
Patrick Fic
2025-10-27 15:01:02 -07:00
parent bedc1f9501
commit a207d5dff7
44 changed files with 42 additions and 3137 deletions

View File

@@ -15,7 +15,7 @@ export default ({ config }) => {
projectId: IS_ROME ? "df105e21-a07f-4425-af10-2200a7704a48" : "ffe01f3a-d507-4698-82cd-da1f1cad450b"
}
},
icon: IS_ROME ? "./assets/RomeIcon.png" : "./assets/logo192noa.png",
icon: IS_ROME ? "./assets/RomeIcon.png" : "./assets/ImEXlogo192noa.png",
ios: {
...config.ios,
bundleIdentifier: IS_ROME ? "com.rome.mobile" : "com.imex.imexmobile"
@@ -55,7 +55,5 @@ export default ({ config }) => {
}
})
console.log("New Expo Config:", JSON.stringify(newConfig, null, 2));
return newConfig;
};

View File

@@ -1,6 +1,7 @@
import { checkUserSession } from "@/redux/user/user.actions";
import { selectBodyshop, selectCurrentUser } from "@/redux/user/user.selectors";
import { ApolloProvider } from "@apollo/client";
import { loadDevMessages, loadErrorMessages } from "@apollo/client/dev";
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import {
DarkTheme,
@@ -28,6 +29,9 @@ import { persistor, store } from "../redux/store";
import "../translations/i18n";
import { registerForPushNotificationsAsync } from "../util/notificationHandler";
loadDevMessages();
loadErrorMessages();
function AuthenticatedLayout() {
const { t } = useTranslation();
const paperTheme = usePaperTheme();

View File

@@ -1,103 +0,0 @@
// // src/toolbar.component.js file
// import { Ionicons } from "@expo/vector-icons";
// import { Camera } from "expo-camera";
// import React from "react";
// import {
// StyleSheet,
// TouchableOpacity,
// TouchableWithoutFeedback,
// View,
// } from "react-native";
// const styles = StyleSheet.create({
// alignCenter: {
// flex: 1,
// alignItems: "center",
// justifyContent: "center",
// },
// bottomToolbar: {
// marginTop: "auto",
// height: 100,
// display: "flex",
// justifyContent: "space-evenly",
// alignItems: "center",
// flexDirection: "row",
// },
// captureBtn: {
// width: 60,
// height: 60,
// borderWidth: 2,
// borderRadius: 60,
// borderColor: "#FFFFFF",
// },
// captureBtnActive: {
// width: 80,
// height: 80,
// },
// captureBtnInternal: {
// width: 76,
// height: 76,
// borderWidth: 2,
// borderRadius: 76,
// backgroundColor: "red",
// borderColor: "transparent",
// },
// });
// const { FlashMode: CameraFlashModes, Type: CameraTypes } = Camera.Constants;
// export default function CameraControls({
// capturing = false,
// cameraType = CameraTypes.back,
// flashMode = CameraFlashModes.off,
// setFlashMode,
// setCameraType,
// onCaptureIn,
// onCaptureOut,
// onLongCapture,
// onShortCapture,
// }) {
// return (
// <View style={styles.bottomToolbar}>
// <TouchableOpacity
// onPress={() =>
// setFlashMode(
// flashMode === CameraFlashModes.on
// ? CameraFlashModes.off
// : CameraFlashModes.on
// )
// }
// >
// <Ionicons
// name={flashMode == CameraFlashModes.on ? "md-flash" : "md-flash-off"}
// color="white"
// size={30}
// />
// </TouchableOpacity>
// <TouchableWithoutFeedback
// onPressIn={onCaptureIn}
// onPressOut={onCaptureOut}
// onLongPress={onLongCapture}
// onPress={onShortCapture}
// disabled={capturing}
// >
// <View style={[styles.captureBtn, capturing && styles.captureBtnActive]}>
// {capturing && <View style={styles.captureBtnInternal} />}
// </View>
// </TouchableWithoutFeedback>
// <TouchableOpacity
// onPress={() =>
// setCameraType(
// cameraType === CameraTypes.back
// ? CameraTypes.front
// : CameraTypes.back
// )
// }
// >
// <Ionicons name="md-reverse-camera" color="white" size={30} />
// </TouchableOpacity>
// </View>
// );
// }

View File

@@ -1,184 +0,0 @@
import { useQuery } from "@apollo/client";
import { Ionicons } from "@expo/vector-icons";
import React from "react";
import { useTranslation } from "react-i18next";
import { FlatList, RefreshControl, View } from "react-native";
import { Button, List, Modal, Portal, Searchbar } from "react-native-paper";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
import { setCameraJob, setCameraJobId } from "../../redux/app/app.actions";
import {
selectCurrentCameraJob,
selectCurrentCameraJobId,
} from "../../redux/app/app.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ErrorDisplay from "../error-display/error-display.component";
import LoadingDisplay from "../loading-display/loading-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
cameraJobId: selectCurrentCameraJobId,
cameraJob: selectCurrentCameraJob,
});
const mapDispatchToProps = (dispatch) => ({
setCameraJobId: (id) => dispatch(setCameraJobId(id)),
setCameraJob: (job) => dispatch(setCameraJob(job)),
});
export function CameraSelectJob({
bodyshop,
cameraJobId,
setCameraJobId,
cameraJob,
setCameraJob,
}) {
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
variables: {
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"],
},
skip: !bodyshop,
});
const { t } = useTranslation();
const [visible, setVisible] = React.useState(false);
const [searchQuery, setSearchQuery] = React.useState("");
if (loading) return <LoadingDisplay />;
if (error) return <ErrorDisplay errorMessage={error.message} />;
const showModal = () => setVisible(true);
const hideModal = () => setVisible(false);
const onRefresh = async () => {
return refetch();
};
const onChangeSearch = (query) => setSearchQuery(query);
const jobs = data
? searchQuery === ""
? data.jobs
: data.jobs.filter(
(j) =>
(j.ro_number || "")
.toString()
.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
(j.ownr_co_nm || "")
.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
(j.ownr_fn || "")
.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
(j.ownr_ln || "")
.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
(j.plate_no || "")
.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
(j.v_model_desc || "")
.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
(j.v_make_desc || "")
.toLowerCase()
.includes(searchQuery.toLowerCase())
)
: [];
return (
<>
<Portal>
<Modal
visible={visible}
onDismiss={hideModal}
// eslint-disable-next-line react-native/no-color-literals
contentContainerStyle={{
paddingTop: 20,
paddingBottom: 20,
margin: 12,
flex: 1,
backgroundColor: "white",
}}
>
<View
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
margin: 8,
}}
>
<Button onPress={() => hideModal()}>
<Ionicons name="arrow-back" size={32} color="dodgerblue" />
</Button>
<Searchbar
style={{ flex: 1 }}
onChangeText={onChangeSearch}
value={searchQuery}
/>
</View>
<FlatList
refreshControl={
<RefreshControl refreshing={loading} onRefresh={onRefresh} />
}
data={[{ id: "temp", ro_number: "Temporary Storage" }, ...jobs]}
keyExtractor={(item) => item.id}
renderItem={(object) => (
<List.Item
onPress={() => {
setCameraJobId(object.item.id);
setCameraJob(object.item);
hideModal();
setSearchQuery("");
}}
left={() => {
if (object.item.id !== cameraJobId) return null;
return (
<Ionicons
name="checkmark-circle"
size={24}
color="dodgerblue"
style={{ alignSelf: "center" }}
/>
);
}}
titleStyle={{
...(object.item.id === cameraJobId
? { color: "dodgerblue" }
: {}),
}}
title={`${
object.item.ro_number ? `${object.item.ro_number} ` : ``
}${object.item.ownr_fn || ""} ${object.item.ownr_ln || ""} ${
object.item.ownr_co_nm || ""
} ${
object.item.v_model_yr ? `- ${object.item.v_model_yr}` : ""
} ${
object.item.v_make_desc ? `- ${object.item.v_make_desc}` : ""
} ${
object.item.v_model_desc
? `- ${object.item.v_model_desc}`
: ""
}`}
key={object.item.id}
/>
)}
/>
</Modal>
</Portal>
<Button mode="outlined" style={{ margin: 8 }} onPress={showModal}>
{cameraJobId
? cameraJobId === "temp"
? t("mediabrowser.labels.temporarystorage")
: `${cameraJob.ro_number ? `${cameraJob.ro_number} - ` : ``}${
cameraJob.ownr_fn || ""
} ${cameraJob.ownr_ln || ""} ${cameraJob.ownr_co_nm || ""} - ${
cameraJob.v_model_yr || ""
} ${cameraJob.v_make_desc || ""} ${cameraJob.v_model_desc || ""}`
: t("mediabrowser.labels.selectjob")}
</Button>
</>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(CameraSelectJob);

View File

@@ -1,10 +0,0 @@
import React from "react";
import { View, Text } from "react-native";
export default function ErrorDisplay({ errorMessage }) {
return (
<View style={{ backgroundColor: "red" }}>
<Text>{errorMessage}</Text>
</View>
);
}

View File

@@ -1,15 +0,0 @@
import React from "react";
import { View, Text } from "react-native";
export default function FlatListItemSeparator() {
return (
<View
style={{
height: 1,
width: "100%",
backgroundColor: "#000",
opacity: 0.2,
}}
/>
);
}

View File

@@ -1,112 +0,0 @@
import React, { useEffect, useState } from "react";
import {
FlatList,
Image,
RefreshControl,
Text,
TouchableOpacity,
View,
} from "react-native";
import MediaCacheOverlay from "../media-cache-overlay/media-cache-overlay.component";
import * as Sentry from "@sentry/react-native";
import Toast from "react-native-toast-message";
import cleanAxios from "../../util/CleanAxios";
export default function JobDocumentsLocalComponent({ bodyshop, job }) {
const [previewVisible, setPreviewVisible] = useState(false);
const [images, setImages] = useState([]);
const [imgIndex, setImgIndex] = useState(0);
useEffect(() => {
if (job.id) {
getPhotos({ bodyshop, jobid: job.id, setImages });
}
}, [job.id, bodyshop]);
const onRefresh = async () => {
return getPhotos({ bodyshop, jobid: job.id, setImages });
};
return (
<View style={{ flex: 1 }}>
<FlatList
refreshControl={
<RefreshControl refreshing={false} onRefresh={onRefresh} />
}
data={images}
numColumns={4}
style={{ flex: 1 }}
keyExtractor={(item) => item.id}
renderItem={(object) => (
<TouchableOpacity
style={{ flex: 1 / 4, aspectRatio: 1, margin: 4 }}
onPress={async () => {
setImgIndex(object.index);
setPreviewVisible(true);
}}
>
<Image
style={{ flex: 1 }}
resizeMode="cover"
source={{
uri: object.item.thumbUrl,
aspectRatio: 1,
}}
/>
</TouchableOpacity>
)}
/>
<Text>
{images?.filter((d) => d.type?.mime?.startsWith("image")).length}
</Text>
<MediaCacheOverlay
photos={images}
imgIndex={imgIndex}
setImgIndex={setImgIndex}
previewVisible={previewVisible}
setPreviewVisible={setPreviewVisible}
/>
</View>
);
}
async function getPhotos({ bodyshop, jobid, setImages }) {
let localmediaserverhttp = bodyshop.localmediaserverhttp.trim();
if (localmediaserverhttp.endsWith("/")) {
localmediaserverhttp = localmediaserverhttp.slice(0, -1);
}
try {
const imagesFetch = await cleanAxios.post(
`${localmediaserverhttp}/jobs/list`,
{
jobid,
},
{ headers: { ims_token: bodyshop.localmediatoken } }
);
const normalizedImages = imagesFetch.data
.filter((d) => d.type?.mime?.startsWith("image"))
.map((d, idx) => {
return {
...d,
// src: `${localmediaserverhttp}/${d.src}`,
uri: `${localmediaserverhttp}${d.src}`,
thumbUrl: `${localmediaserverhttp}${d.thumbnail}`,
id: idx,
};
});
setImages(normalizedImages);
} catch (error) {
Sentry.captureException(error);
Toast.show({
type: "error",
text1: `Error fetching photos.`,
text2: JSON.stringify(error),
});
}
}

View File

@@ -1,175 +0,0 @@
import axios from "axios";
import React, { useEffect, useState } from "react";
import {
FlatList,
Image,
RefreshControl,
Text,
TouchableOpacity,
View,
} from "react-native";
import env from "../../env";
import { DetermineFileType } from "../../util/document-upload.utility";
import MediaCacheOverlay from "../media-cache-overlay/media-cache-overlay.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
//import { splitClient } from "../screen-main/screen-main.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobDocumentsComponent);
export function JobDocumentsComponent({ bodyshop, job, loading, refetch }) {
const [previewVisible, setPreviewVisible] = useState(false);
const [fullphotos, setFullPhotos] = useState([]);
const [imgIndex, setImgIndex] = useState(0);
const useImgproxy = splitClient?.getTreatment("Imgproxy");
const onRefresh = async () => {
return refetch();
};
useEffect(() => {
async function getPhotos() {
if (useImgproxy) {
const result = await axios.post(
`${env.API_URL}/media/imgproxy/thumbnails`,
{
jobid: job.id,
}
);
setFullPhotos(
result.data.map((doc, idx) => {
return {
id: idx,
videoUrl:
DetermineFileType(doc.type) === "video" &&
doc.originalUrlViaProxyPath,
source:
DetermineFileType(doc.type) === "video"
? { uri: doc.thumbnailUrl }
: { uri: doc.originalUrl },
url:
DetermineFileType(doc.type) === "video"
? doc.thumbnailUrl
: doc.originalUrl,
uri:
DetermineFileType(doc.type) === "video"
? doc.originalUrlViaProxyPath
: doc.originalUrl,
thumbUrl: doc.thumbnailUrl,
};
})
);
} else {
setFullPhotos(
job.documents.map((doc, idx) => {
return {
id: idx,
videoUrl:
DetermineFileType(doc.type) === "video" && GenerateSrcUrl(doc),
source:
DetermineFileType(doc.type) === "video"
? { uri: GenerateThumbUrl(doc) }
: { uri: GenerateSrcUrl(doc) },
url:
DetermineFileType(doc.type) === "video"
? GenerateThumbUrl(doc)
: GenerateSrcUrl(doc),
uri:
DetermineFileType(doc.type) === "video"
? GenerateThumbUrl(doc)
: GenerateSrcUrl(doc),
thumbUrl: GenerateThumbUrl(doc),
};
})
);
}
}
getPhotos();
}, [job.documents]);
return (
<View style={{ flex: 1 }}>
<FlatList
refreshControl={
<RefreshControl refreshing={loading} onRefresh={onRefresh} />
}
data={fullphotos}
numColumns={4}
style={{ flex: 1 }}
keyExtractor={(item) => item.id}
renderItem={(object) => (
<TouchableOpacity
style={{ flex: 1 / 4, aspectRatio: 1, margin: 4 }}
onPress={async () => {
setImgIndex(object.index);
setPreviewVisible(true);
}}
>
<Image
style={{ flex: 1 }}
resizeMode="cover"
source={{
uri: object.item.thumbUrl,
aspectRatio: 1,
}}
/>
</TouchableOpacity>
)}
/>
<Text
style={{ textAlign: "center", color: useImgproxy ? "blue" : "black" }}
>
{fullphotos.length}
</Text>
<MediaCacheOverlay
photos={fullphotos}
imgIndex={imgIndex}
setImgIndex={setImgIndex}
previewVisible={previewVisible}
setPreviewVisible={setPreviewVisible}
/>
</View>
);
}
export const GenerateSrcUrl = (value) => {
let extension = value.extension;
if (extension && extension.toLowerCase().includes("heic")) extension = "jpg";
return `${env.REACT_APP_CLOUDINARY_ENDPOINT}/${DetermineFileType(
value.type
)}/upload/${value.key}${extension ? `.${extension}` : ""}`;
};
export const GenerateThumbUrl = (value) => {
let extension = value.extension;
if (extension && extension.includes("heic")) extension = "jpg";
else if (
DetermineFileType(value.type) !== "image" ||
(value.type && value.type.includes("application"))
)
extension = "jpg";
return `${env.REACT_APP_CLOUDINARY_ENDPOINT}/${DetermineFileType(
value.type
)}/upload/${env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS}/${value.key}${
extension ? `.${extension}` : ""
}`;
};

View File

@@ -1,72 +0,0 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { FlatList, RefreshControl, StyleSheet, Text, View } from "react-native";
import { Card, DataTable } from "react-native-paper";
export default function JobLines({ job, loading, refetch }) {
const { t } = useTranslation();
if (!job) {
<Card>
<Text>Job is not defined.</Text>
</Card>;
}
const onRefresh = async () => {
return refetch();
};
return (
<View style={{ flex: 1 }}>
<DataTable>
<DataTable.Header>
<DataTable.Title style={{ flex: 4 }}>
{t("jobdetail.labels.lines_desc")}
</DataTable.Title>
<DataTable.Title style={{ flex: 2 }}>
{t("jobdetail.labels.lines_lbr_ty")}
</DataTable.Title>
<DataTable.Title style={{ flex: 1 }}>
{t("jobdetail.labels.lines_lb_hrs")}
</DataTable.Title>
<DataTable.Title style={{ flex: 2 }}>
{t("jobdetail.labels.lines_part_type")}
</DataTable.Title>
<DataTable.Title style={{ flex: 1 }}>
{t("jobdetail.labels.lines_qty")}
</DataTable.Title>
</DataTable.Header>
</DataTable>
<FlatList
data={job.joblines}
refreshControl={
<RefreshControl refreshing={loading} onRefresh={onRefresh} />
}
keyExtractor={(item) => item.id}
renderItem={(object) => (
<DataTable.Row>
<DataTable.Cell style={{ flex: 4 }}>
{object.item.line_desc}
</DataTable.Cell>
<DataTable.Cell style={{ flex: 2 }}>
{object.item.mod_lbr_ty &&
t(`jobdetail.lbr_types.${object.item.mod_lbr_ty}`)}
</DataTable.Cell>
<DataTable.Cell style={{ flex: 1 }}>
{object.item.mod_lb_hrs}
</DataTable.Cell>
<DataTable.Cell style={{ flex: 2 }}>
{object.item.part_type &&
t(`jobdetail.part_types.${object.item.part_type}`)}
</DataTable.Cell>
<DataTable.Cell style={{ flex: 1 }}>
{object.item.part_qty}
</DataTable.Cell>
</DataTable.Row>
)}
/>
</View>
);
}
const localStyles = StyleSheet.create({});

View File

@@ -1,90 +0,0 @@
import { Ionicons } from "@expo/vector-icons";
import { useNavigation } from "@react-navigation/native";
import React from "react";
import { useTranslation } from "react-i18next";
import { Button, List, Title } from "react-native-paper";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.analytics";
import { setCameraJob, setCameraJobId } from "../../redux/app/app.actions";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
setCameraJobId: (id) => dispatch(setCameraJobId(id)),
setCameraJob: (job) => dispatch(setCameraJob(job)),
});
export function JobListItem({ setCameraJob, setCameraJobId, item }) {
const { t } = useTranslation();
const navigation = useNavigation();
// const _swipeableRow = useRef(null);
// const RenderRightAction = (progress, dragX) => {
// const scale = dragX.interpolate({
// inputRange: [-100, 0],
// outputRange: [0.7, 0],
// });
// return (
// <TouchableOpacity
// style={[styles.swipe_view, styles.swipe_view_blue]}
// onPress={() => {
// logImEXEvent("imexmobile_setcamerajobid_swipe");
// setCameraJobId(item.id);
// setCameraJob(item);
// navigation.navigate("MediaBrowserTab");
// _swipeableRow.current.close();
// }}
// >
// <Animated.View
// style={{
// transform: [{ scale }],
// }}
// >
// <Ionicons name="ios-camera" size={64} color="white" />
// </Animated.View>
// </TouchableOpacity>
// );
// };
const onPress = () => {
logImEXEvent("imexmobile_view_job_detail");
navigation.push("JobDetail", {
jobId: item.id,
title: item.ro_number || t("general.labels.na"),
job: item,
});
};
return (
<List.Item
onPress={onPress}
title={<Title>{item.ro_number || t("general.labels.na")}</Title>}
description={`${item.ownr_fn || ""} ${item.ownr_ln || ""} ${
item.ownr_co_nm || ""
} - ${item.v_model_yr || ""} ${item.v_make_desc || ""} ${
item.v_model_desc || ""
}`}
right={({ style }) => (
<Button
style={[style, { alignSelf: "center" }]}
onPress={() => {
logImEXEvent("imexmobile_setcamerajobid_row");
setCameraJobId(item.id);
setCameraJob(item);
navigation.navigate("MediaBrowserTab");
}}
>
<Ionicons
style={[style, { alignSelf: "center" }]}
name="add"
size={32}
color="dodgerblue"
/>
</Button>
)}
/>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(JobListItem);

View File

@@ -1,94 +0,0 @@
import { useQuery } from "@apollo/client";
import React from "react";
import { useTranslation } from "react-i18next";
import { FlatList, RefreshControl, Text, View } from "react-native";
import { Button, Searchbar, Title } from "react-native-paper";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ErrorDisplay from "../error-display/error-display.component";
import JobListItem from "../job-list-item/job-list-item.component";
import LoadingDisplay from "../loading-display/loading-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
export function JobListComponent({ bodyshop }) {
const { t } = useTranslation();
const [searchQuery, setSearchQuery] = React.useState("");
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
variables: {
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"],
},
skip: !bodyshop,
notifyOnNetworkStatusChange: true,
});
const onRefresh = async () => {
return refetch();
};
const onChangeSearch = (query) => setSearchQuery(query);
if (loading) return <LoadingDisplay />;
if (error) return <ErrorDisplay errorMessage={error.message} />;
if (data && data.jobs && data.jobs.length === 0)
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Title>
<Text>{t("joblist.labels.nojobs")}</Text>
</Title>
<Button onPress={() => refetch()}>
{t("joblist.actions.refresh")}
</Button>
</View>
);
const jobs = data
? searchQuery === ""
? data.jobs
: data.jobs.filter(
(j) =>
(j.ro_number || "")
.toString()
.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
(j.ownr_co_nm || "")
.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
(j.ownr_fn || "")
.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
(j.ownr_ln || "")
.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
(j.v_model_desc || "")
.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
(j.v_make_desc || "")
.toLowerCase()
.includes(searchQuery.toLowerCase())
)
: [];
return (
<View style={{ flex: 1 }}>
<Searchbar
onChangeText={onChangeSearch}
value={searchQuery}
placeholder={t("joblist.labels.search")}
/>
<FlatList
refreshControl={
<RefreshControl refreshing={loading} onRefresh={onRefresh} />
}
style={{ flex: 1 }}
data={jobs}
renderItem={(object) => <JobListItem item={object.item} />}
/>
</View>
);
}
export default connect(mapStateToProps, null)(JobListComponent);

View File

@@ -1,46 +0,0 @@
import { AntDesign } from "@expo/vector-icons";
import { DateTime } from "luxon";
import React from "react";
import { Text, View } from "react-native";
import { Card } from "react-native-paper";
export default function NoteListItem({ item }) {
return (
<Card style={{ margin: 8 }}>
<Card.Content>
<View style={{ display: "flex", flex: 1 }}>
<Text>{item.text}</Text>
<View
style={{
flexDirection: "column",
alignSelf: "flex-end",
alignItems: "center",
}}
>
{item.private && (
<AntDesign
name="eyeo"
style={{ margin: 4 }}
size={24}
color="black"
/>
)}
{item.critical && (
<AntDesign
name="warning"
style={{ margin: 4 }}
size={24}
color="tomato"
/>
)}
<Text style={{ fontSize: 12 }}>{item.created_by}</Text>
<Text style={{ fontSize: 12 }}>
{DateTime.fromISO(item.created_at).toLocaleString(
DateTime.DATETIME_SHORT
)}
</Text>
</View>
</View>
</Card.Content>
</Card>
);
}

View File

@@ -1,37 +0,0 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { FlatList, RefreshControl, Text } from "react-native";
import { Card } from "react-native-paper";
import JobNotesItem from "../job-notes-item/job-notes-item.component";
export default function JobNotes({ job, loading, refetch }) {
const { t } = useTranslation();
if (!job) {
<Card>
<Text>Job is not defined.</Text>
</Card>;
}
const onRefresh = async () => {
return refetch();
};
if (job.notes.length === 0)
return (
<Card>
<Card.Content>
<Text>{t("jobdetail.labels.nojobnotes")}</Text>
</Card.Content>
</Card>
);
return (
<FlatList
refreshControl={
<RefreshControl refreshing={loading} onRefresh={onRefresh} />
}
style={{ flex: 1 }}
data={job.notes}
renderItem={(object) => <JobNotesItem item={object.item} />}
/>
);
}

View File

@@ -1,44 +0,0 @@
import { useQuery } from "@apollo/client";
import React from "react";
import { ProgressBar } from "react-native-paper";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { GET_DOC_SIZE_TOTALS } from "../../graphql/documents.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { View, Text } from "react-native";
import { useTranslation } from "react-i18next";
import { formatBytes } from "../../util/document-upload.utility";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(JobSpaceAvailable);
export function JobSpaceAvailable({ bodyshop, style, jobid }) {
const { t } = useTranslation();
const { data } = useQuery(GET_DOC_SIZE_TOTALS, {
variables: { jobId: jobid },
skip: !jobid || jobid === "temp",
});
if (!jobid || !data) return <></>;
const progress =
data.documents_aggregate.aggregate.sum.size /
((bodyshop && bodyshop.jobsizelimit) || 1);
return (
<View style={{ margin: 10 }}>
<Text style={{ marginBottom: 5 }}>
{t("mediabrowser.labels.storageused", {
used: formatBytes(data.documents_aggregate.aggregate.sum.size),
total: formatBytes((bodyshop && bodyshop.jobsizelimit) || 1),
percent: Math.round(progress * 100),
})}
</Text>
<ProgressBar style={[style]} progress={progress} />
</View>
);
}

View File

@@ -1,172 +0,0 @@
import { useTranslation } from "react-i18next";
import {
RefreshControl,
ScrollView,
StyleSheet,
Text,
View,
} from "react-native";
import { Card, Headline, Subheading } from "react-native-paper";
import DataLabelComponent from "../../components/data-label/data-label";
import StyleRepeater from "../style-repeater/style-repeater";
import styles from "../styles";
export default function JobTombstone({ job, loading, refetch }) {
const { t } = useTranslation();
if (!job) {
<Card>
<Text>Job is not defined.</Text>
</Card>;
}
const onRefresh = async () => {
return refetch();
};
return (
<ScrollView
style={styles.cardBackground}
refreshControl={
<RefreshControl refreshing={loading} onRefresh={onRefresh} />
}
>
<StyleRepeater childStyle={{ margin: 4 }}>
<Card>
<Card.Title title={t("jobdetail.labels.jobinfo")} />
<Card.Content>
<Headline>{job.status}</Headline>
{job.inproduction && (
<Subheading>{t("objects.jobs.labels.inproduction")}</Subheading>
)}
{job.inproduction &&
job.production_vars &&
!!job.production_vars.note && (
<Subheading>{job.production_vars.note}</Subheading>
)}
</Card.Content>
</Card>
<Card>
<Card.Title title={t("jobdetail.labels.claiminformation")} />
<Card.Content style={localStyles.twoColumnCard}>
<View style={localStyles.twoColumnCardColumn}>
<DataLabelComponent
label={t("objects.jobs.fields.owner")}
content={`${job.ownr_fn || ""} ${job.ownr_ln || ""} ${
job.ownr_co_nm || ""
}`}
/>
<DataLabelComponent
label={t("objects.jobs.fields.vehicle")}
content={
<View>
<Text>{`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${
job.v_model_desc || ""
}`}</Text>
<Text>{job.v_vin}</Text>
</View>
}
/>
</View>
<View style={localStyles.twoColumnCardColumn}>
<DataLabelComponent
label={t("objects.jobs.fields.ins_co_nm")}
content={job.ins_co_nm}
/>
<DataLabelComponent
label={t("objects.jobs.fields.clm_no")}
content={job.clm_no}
/>
</View>
</Card.Content>
</Card>
<Card>
<Card.Title title={t("jobdetail.labels.employeeassignments")} />
<Card.Content>
<DataLabelComponent
label={t("objects.jobs.fields.employee_body")}
content={`${
(job.employee_body_rel && job.employee_body_rel.first_name) ||
""
} ${
(job.employee_body_rel && job.employee_body_rel.last_name) || ""
}`}
/>
<DataLabelComponent
label={t("objects.jobs.fields.employee_prep")}
content={`${
(job.employee_prep_rel && job.employee_prep_rel.first_name) ||
""
} ${
(job.employee_prep_rel && job.employee_prep_rel.last_name) || ""
}`}
/>
<DataLabelComponent
label={t("objects.jobs.fields.employee_refinish")}
content={`${
(job.employee_refinish_rel &&
job.employee_refinish_rel.first_name) ||
""
} ${
(job.employee_refinish_rel &&
job.employee_refinish_rel.last_name) ||
""
}`}
/>
<DataLabelComponent
label={t("objects.jobs.fields.employee_csr")}
content={`${
(job.employee_csr_rel && job.employee_csr_rel.first_name) || ""
} ${
(job.employee_csr_rel && job.employee_csr_rel.last_name) || ""
}`}
/>
</Card.Content>
</Card>
<Card>
<Card.Title title={t("jobdetail.labels.dates")} />
<Card.Content style={localStyles.twoColumnCard}>
<View style={localStyles.twoColumnCardColumn}>
<DataLabelComponent
label={t("objects.jobs.fields.scheduled_in")}
content={job.scheduled_in}
dateTime
/>
<DataLabelComponent
label={t("objects.jobs.fields.actual_in")}
content={job.actual_in}
dateTime
/>
</View>
<View style={localStyles.twoColumnCardColumn}>
<DataLabelComponent
label={t("objects.jobs.fields.scheduled_completion")}
content={job.scheduled_completion}
dateTime
/>
<DataLabelComponent
label={t("objects.jobs.fields.scheduled_delivery")}
content={job.scheduled_delivery}
dateTime
/>
</View>
</Card.Content>
</Card>
</StyleRepeater>
</ScrollView>
);
}
const localStyles = StyleSheet.create({
twoColumnCard: { display: "flex", flexDirection: "row" },
twoColumnCardColumn: { flex: 1 },
status: {
textAlign: "center",
flexDirection: "row",
justifyContent: "center",
},
inproduction: {
textAlign: "center",
flexDirection: "row",
justifyContent: "center",
},
});

View File

@@ -1,12 +0,0 @@
import React from "react";
import { View } from "react-native";
import { BarIndicator } from "react-native-indicators";
export default function LoadingDisplay({ count = 5 }) {
//TODO: This is throwing an error per expo, but it appears to be happening inside the component itself.
return (
<View style={{ flex: 1, alignContent: "center", justifyContent: "center" }}>
<BarIndicator count={count} color="dodgerblue" />
</View>
);
}

View File

@@ -1,27 +0,0 @@
import { SafeAreaView } from "react-native";
import React from "react";
import ImageView from "react-native-image-viewing";
export default function MediaCacheOverlay({
photos,
previewVisible,
setPreviewVisible,
imgIndex,
setImgIndex,
}) {
//const videoRef = React.useRef(null);
return (
<SafeAreaView>
<ImageView
onRequestClose={() => setPreviewVisible(false)}
visible={previewVisible}
images={photos}
imageIndex={imgIndex}
// onImageIndexChange={(...props) => {
// // console.log(props);
// }}
/>
</SafeAreaView>
);

View File

@@ -1,157 +0,0 @@
// import { useFocusEffect } from "@react-navigation/native";
// import { Camera } from "expo-camera";
// import * as FileSystem from "expo-file-system";
// import React, { useEffect, useRef, useState } from "react";
// import { Text, View } from "react-native";
// import { connect } from "react-redux";
// import { createStructuredSelector } from "reselect";
// import {
// selectCurrentCameraJob,
// selectCurrentCameraJobId,
// } from "../../redux/app/app.selectors";
// import { addPhoto } from "../../redux/photos/photos.actions";
// import CameraControls from "../camera-controls/camera-controls.component";
// import CameraSelectJob from "../camera-select-job/camera-select-job.component";
// const mapStateToProps = createStructuredSelector({
// cameraJobId: selectCurrentCameraJobId,
// cameraJob: selectCurrentCameraJob,
// });
// const mapDispatchToProps = (dispatch) => ({
// addPhoto: (photo) => dispatch(addPhoto(photo)),
// });
// export function ScreenCamera({ cameraJobId, addPhoto }) {
// const [hasPermission, setHasPermission] = useState(null);
// const [state, setState] = useState({
// flashMode: Camera.Constants.FlashMode.off,
// capturing: null,
// cameraType: Camera.Constants.Type.back,
// tabHasFocus: null,
// });
// const cameraRef = useRef(null);
// useFocusEffect(
// React.useCallback(() => {
// // Do something when the screen is focused
// setState({ ...state, tabHasFocus: true });
// return () => {
// // Do something when the screen is unfocused
// // Useful for cleanup functions
// setState({ ...state, tabHasFocus: false });
// };
// }, [])
// );
// useEffect(() => {
// (async () => {
// const { status } = await Camera.requestPermissionsAsync();
// setHasPermission(status === "granted");
// })();
// }, []);
// const setFlashMode = (flashMode) => setState({ ...state, flashMode });
// const setCameraType = (cameraType) => setState({ ...state, cameraType });
// const handleCaptureIn = () => setState({ ...state, capturing: true });
// const handleCaptureOut = () => {
// if (state.capturing) cameraRef.current.stopRecording();
// };
// const handleShortCapture = async () => {
// if (cameraRef.current) {
// const options = {
// quality: 0.8,
// //base64: true,
// skipProcessing: true,
// };
// let photo = await cameraRef.current.takePictureAsync(options);
// const filename = photo.uri.substring(photo.uri.lastIndexOf("/") + 1);
// const newUri = FileSystem.documentDirectory + "photos/" + filename;
// await FileSystem.moveAsync({
// from: photo.uri,
// to: newUri,
// });
// setState({ ...state, capturing: false });
// addPhoto({
// ...photo,
// id: filename,
// uri: newUri,
// jobId: cameraJobId,
// video: false,
// });
// }
// };
// const handleLongCapture = async () => {
// console.log("Taking a video!");
// if (cameraRef.current) {
// let video = await cameraRef.current.recordAsync();
// const filename = video.uri.substring(video.uri.lastIndexOf("/") + 1);
// const newUri = FileSystem.documentDirectory + "photos/" + filename;
// await FileSystem.moveAsync({
// from: video.uri,
// to: newUri,
// });
// setState({ ...state, capturing: false });
// console.log("Adding Photo", {
// ...video,
// id: filename,
// uri: newUri,
// jobId: cameraJobId,
// video: true,
// });
// addPhoto({
// ...video,
// id: filename,
// uri: newUri,
// jobId: cameraJobId,
// video: true,
// });
// }
// };
// if (hasPermission === null || !state.tabHasFocus) {
// return <View />;
// }
// if (hasPermission === false) {
// return <Text>No access to camera. Please ensure that you allow it.</Text>;
// }
// const { flashMode, cameraType, capturing } = state;
// return (
// <Camera
// style={{ flex: 1, display: "flex" }}
// type={state.cameraType}
// ref={cameraRef}
// ratio={"16:9"}
// >
// <View
// style={{
// flex: 1,
// }}
// >
// <CameraSelectJob />
// <CameraControls
// capturing={capturing}
// flashMode={flashMode}
// cameraType={cameraType}
// setFlashMode={setFlashMode}
// setCameraType={setCameraType}
// onCaptureIn={handleCaptureIn}
// onCaptureOut={handleCaptureOut}
// onLongCapture={handleLongCapture}
// onShortCapture={handleShortCapture}
// />
// </View>
// </Camera>
// );
// }
// export default connect(mapStateToProps, mapDispatchToProps)(ScreenCamera);

View File

@@ -1,101 +0,0 @@
import { useQuery } from "@apollo/client";
import React from "react";
import { useTranslation } from "react-i18next";
import { useWindowDimensions } from "react-native";
import { SceneMap, TabView, TabBar } from "react-native-tab-view";
import { GET_JOB_BY_PK } from "../../graphql/jobs.queries";
import ErrorDisplay from "../error-display/error-display.component";
import JobDocuments from "../job-documents/job-documents.component";
import JobLines from "../job-lines/job-lines.component";
import JobNotes from "../job-notes/job-notes.component";
import JobTombstone from "../job-tombstone/job-tombstone.component";
import LoadingDisplay from "../loading-display/loading-display.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import JobDocumentsLocalComponent from "../job-documents/job-documents-local.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(ScreenJobDetail);
export function ScreenJobDetail({ bodyshop, route }) {
const {
params: { jobId },
} = route;
const { t } = useTranslation();
const layout = useWindowDimensions();
const { loading, error, data, refetch } = useQuery(GET_JOB_BY_PK, {
variables: {
id: jobId,
},
skip: !jobId,
});
const renderTabBar = (props) => (
<TabBar
{...props}
indicatorStyle={{ backgroundColor: "#ffffff" }}
// style={{ backgroundColor: "dodgerblue" }}
/>
);
const renderScene = SceneMap({
job: () =>
JobTombstone({
job: data.jobs_by_pk,
loading: loading,
refetch: refetch,
}),
lines: () =>
JobLines({
job: data.jobs_by_pk,
loading: loading,
refetch: refetch,
}),
documents: () => {
return bodyshop.uselocalmediaserver ? (
<JobDocumentsLocalComponent job={data.jobs_by_pk} bodyshop={bodyshop} />
) : (
<JobDocuments
job={data.jobs_by_pk}
loading={loading}
refetch={refetch}
/>
);
},
notes: () =>
JobNotes({
job: data.jobs_by_pk,
loading: loading,
refetch: refetch,
}),
});
const [index, setIndex] = React.useState(0);
const [routes] = React.useState([
{ key: "job", title: t("jobdetail.labels.job") },
{ key: "lines", title: t("jobdetail.labels.lines") },
{ key: "documents", title: t("jobdetail.labels.documents") },
{ key: "notes", title: t("jobdetail.labels.notes") },
]);
if (loading) return <LoadingDisplay />;
if (error) return <ErrorDisplay errorMessage={error.message} />;
return (
<TabView
style={{ flex: 1 }}
navigationState={{ index, routes }}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
renderTabBar={renderTabBar}
/>
);
}

View File

@@ -1,6 +0,0 @@
import React from "react";
import JobListComponent from "../job-list/job-list.component.jsx";
export default function ScreenJobList({ navigation }) {
return <JobListComponent />;
}

View File

@@ -1,197 +0,0 @@
import { Ionicons } from "@expo/vector-icons";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import i18n from "i18next";
import moment from "moment";
import { useEffect } from "react";
import { Button } from "react-native-paper";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.analytics";
import { setCameraJob, setCameraJobId } from "../../redux/app/app.actions";
import {
checkUserSession,
emailSignInStart,
signOutStart,
} from "../../redux/user/user.actions";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import ScreenJobDetail from "../screen-job-detail/screen-job-detail.component";
import ScreenJobList from "../screen-job-list/screen-job-list.component";
import ScreenMediaBrowser from "../screen-media-browser/screen-media-browser.component";
import ScreenSettingsComponent from "../screen-settings/screen-settings.component";
import ScreenSignIn from "../screen-sign-in/screen-sign-in.component";
import ScreenSplash from "../screen-splash/screen-splash.component";
const ActiveJobStack = createNativeStackNavigator();
const MoreStack = createNativeStackNavigator();
const BottomTabs = createBottomTabNavigator();
const MediaBrowserStack = createNativeStackNavigator();
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
checkUserSession: () => dispatch(checkUserSession()),
emailSignInStart: (email, password) =>
dispatch(emailSignInStart({ email, password })),
signOutStart: () => dispatch(signOutStart()),
setCameraJobId: (id) => dispatch(setCameraJobId(id)),
setCameraJob: (job) => dispatch(setCameraJob(job)),
});
const JobsTabNavigator = connect(
mapStateToProps,
mapDispatchToProps
)(({ setCameraJobId, setCameraJob }) => (
<ActiveJobStack.Navigator initialRouteName="JobList">
<ActiveJobStack.Screen
name="JobList"
options={() => ({
title: i18n.t("joblist.labels.activejobs"),
})}
component={ScreenJobList}
/>
<ActiveJobStack.Screen
name="JobDetail"
component={ScreenJobDetail}
options={({ navigation, route }) => ({
title:
(route.params && route.params.title) ||
i18n.t("joblist.labels.detail"),
// eslint-disable-next-line react/display-name
headerRight: () => (
<Button
onPress={() => {
logImEXEvent("imexmobile_setcamerajobid_jobheader");
setCameraJobId(route.params.jobId);
setCameraJob(route.params.job);
navigation.navigate("MediaBrowserTab");
}}
>
<Ionicons name="add" size={32} color="dodgerblue" />
</Button>
),
})}
/>
</ActiveJobStack.Navigator>
));
const MediaBrowserStackNavigator = () => (
<MediaBrowserStack.Navigator initialRouteName="MediaBrowser">
<MediaBrowserStack.Screen
name="MediaBrowser"
options={{ title: i18n.t("mediabrowser.titles.mediabrowsertab") }}
component={ScreenMediaBrowser}
/>
</MediaBrowserStack.Navigator>
);
const MoreStackNavigator = () => (
<MoreStack.Navigator>
<MoreStack.Screen
name="Settings"
options={{
title: i18n.t("settings.titles.settings"),
}}
component={ScreenSettingsComponent}
/>
</MoreStack.Navigator>
);
const BottomTabsNavigator = () => (
<BottomTabs.Navigator
screenOptions={({ route }) => ({
// eslint-disable-next-line react/display-name
tabBarIcon: ({ color, size }) => {
let iconName;
if (route.name === "JobTab") {
iconName = "list";
} else if (route.name === "MoreTab") {
iconName = "settings";
} else if (route.name === "MediaBrowserTab") {
iconName = "camera";
} else {
//iconName = "customerservice";
}
return <Ionicons name={iconName} size={size} color={color} />;
},
})}
>
<BottomTabs.Screen
name="JobTab"
options={{
title: i18n.t("joblist.titles.jobtab"),
headerShown: false,
}}
component={JobsTabNavigator}
/>
<BottomTabs.Screen
name="MediaBrowserTab"
options={{
title: i18n.t("mediabrowser.titles.mediabrowsertab"),
headerShown: false,
}}
component={MediaBrowserStackNavigator}
/>
<BottomTabs.Screen
name="MoreTab"
options={{ title: i18n.t("more.titles.moretab"), headerShown: false }}
component={MoreStackNavigator}
/>
</BottomTabs.Navigator>
);
export function ScreenMainComponent({
checkUserSession,
currentUser,
bodyshop,
}) {
useEffect(() => {
checkUserSession();
}, [checkUserSession]);
// useEffect(() => {
// // LogRocket.init("idt6oy/imex-mobile", {
// // updateId: Updates.isEmbeddedLaunch ? null : Updates.updateId,
// // expoChannel: Updates.channel,
// // });
// }, []);
return (
<NavigationContainer>
{currentUser.authorized === null ? (
<ScreenSplash />
) : currentUser.authorized ? (
bodyshop ? (
HasAccess(bodyshop) ? (
<BottomTabsNavigator />
) : (
<ScreenSplash noAccess />
)
) : (
<ScreenSplash />
)
) : (
<ScreenSignIn />
)}
</NavigationContainer>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ScreenMainComponent);
function HasAccess({ features }) {
if (features.mobile === undefined || features.mobile === true) return true;
if (features.mobile === false) return false;
const d = moment(moment(features.mobile));
if (d.isValid()) return d.isAfter(moment());
}

View File

@@ -1,162 +0,0 @@
import Constants from "expo-constants";
import * as ImagePicker from "expo-image-picker";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { StyleSheet, Text, View } from "react-native";
import { Button } from "react-native-paper";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { toggleDeleteAfterUpload } from "../../redux/app/app.actions";
import {
selectCurrentCameraJobId,
selectDeleteAfterUpload,
} from "../../redux/app/app.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CameraSelectJob from "../camera-select-job/camera-select-job.component";
import JobSpaceAvailable from "../job-space-available/job-space-available.component";
import UploadDeleteSwitch from "../upload-delete-switch/upload-delete-switch.component";
import UploadProgressLocal from "../upload-progress-local/upload-progress-local.component";
import UploadProgress from "../upload-progress/upload-progress.component";
const mapStateToProps = createStructuredSelector({
selectedCameraJobId: selectCurrentCameraJobId,
bodyshop: selectBodyshop,
deleteAfterUpload: selectDeleteAfterUpload,
});
const mapDispatchToProps = (dispatch) => ({
toggleDeleteAfterUpload: () => dispatch(toggleDeleteAfterUpload()),
});
export function ImageBrowserScreen({
bodyshop,
selectedCameraJobId,
//toggleDeleteAfterUpload,
// deleteAfterUpload,
}) {
const { t } = useTranslation();
const [uploads, setUploads] = useState(null);
const [density, setDensity] = useState(3);
const [tick, setTick] = useState(0);
const forceRerender = useCallback(() => {
setTick((tick) => tick + 1);
}, []);
useEffect(() => {
(async () => {
if (Constants.platform.ios) {
const cameraRollStatus =
await ImagePicker.requestMediaLibraryPermissionsAsync();
const cameraStatus = await ImagePicker.requestCameraPermissionsAsync();
if (
cameraRollStatus.status !== "granted" ||
cameraStatus.status !== "granted"
) {
alert(
"Photo and Camera permissions have not been granted. Please open the settings app and allow these permissions to upload photos."
);
}
}
})();
}, []);
const pickImage = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ["images", "videos"],
aspect: [4, 3],
quality: 1,
allowsMultipleSelection: true,
});
setUploads(result.assets);
};
return (
<View style={[styles.flex, styles.container]}>
<CameraSelectJob />
{bodyshop.uselocalmediaserver ? (
<Text style={{ margin: 10 }}>
{t("mediabrowser.labels.localserver", {
url: bodyshop.localmediaserverhttp,
})}
</Text>
) : (
<JobSpaceAvailable jobid={selectedCameraJobId} key={`${tick}-space`} />
)}
<UploadDeleteSwitch />
{!selectedCameraJobId && (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
}}
>
<Text>{t("mediabrowser.labels.selectjobassetselector")}</Text>
</View>
)}
<Button onPress={pickImage}>Media Select</Button>
{bodyshop.uselocalmediaserver ? (
<UploadProgressLocal
uploads={uploads}
setUploads={setUploads}
forceRerender={forceRerender}
/>
) : (
<UploadProgress
uploads={uploads}
setUploads={setUploads}
forceRerender={forceRerender}
/>
)}
</View>
);
}
const styles = StyleSheet.create({
flex: {
flex: 1,
},
container: {
display: "flex",
// position: "relative",
},
buttonStyle: {
//backgroundColor: "tomato",
},
textStyle: {
color: "dodgerblue",
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ImageBrowserScreen);
// // Utility to get asset ID from URI if missing
// async function getAssetIdFromUri(uri, filename = null, maxPages = 10) {
// let after = null;
// let found = null;
// let pageCount = 0;
// while (!found && pageCount < maxPages) {
// const page = await MediaLibrary.getAssetsAsync({
// first: 100,
// mediaType: [MediaLibrary.MediaType.photo, MediaLibrary.MediaType.video],
// after,
// });
// // Try to match by URI
// found = page.assets.find((asset) => asset.uri === uri);
// // Fallback: try to match by filename if not found and filename is available
// if (!found && filename) {
// found = page.assets.find((asset) => asset.filename === filename);
// }
// after = page.endCursor;
// pageCount++;
// if (!after) break;
// }
// return found ? found.id : null;
// }

View File

@@ -1,140 +0,0 @@
import _ from "lodash";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import {
ActivityIndicator,
FlatList,
Image,
SafeAreaView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
import { Button } from "react-native-paper";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
removeAllPhotos,
uploadAllPhotos,
} from "../../redux/photos/photos.actions";
import {
selectPhotos,
selectUploadInProgress,
} from "../../redux/photos/photos.selectors";
import MediaCacheOverlay from "../media-cache-overlay/media-cache-overlay.component";
const mapStateToProps = createStructuredSelector({
photos: selectPhotos,
uploadInProgress: selectUploadInProgress,
});
const mapDispatchToProps = (dispatch) => ({
removeAllPhotos: () => dispatch(removeAllPhotos()),
uploadAllphotos: () => dispatch(uploadAllPhotos()),
});
export function ScreenMediaCache({
photos,
removeAllPhotos,
uploadAllphotos,
uploadInProgress,
}) {
const { t } = useTranslation();
const [previewVisible, setPreviewVisible] = useState(false);
const [imgIndex, setImgIndex] = useState(0);
const groupedPhotos = _.groupBy(photos, "jobId");
const RenderJobPictures = ({ jobId, jobPhotos }) => (
<View>
<Text>{jobId}</Text>
<FlatList
data={jobPhotos}
style={{ flex: 1 }}
contentContainerStyle={styles.listContentContainer}
keyExtractor={(item) => item.id}
numColumns={4}
renderItem={(object) =>
object.item.video ? (
<Text>Video</Text>
) : (
<TouchableOpacity
style={{
flex: 1 / 4, //here you can use flex:1 also
aspectRatio: 1,
}}
onPress={() => {
setImgIndex(object.index);
setPreviewVisible(true);
}}
>
<Image
source={{ uri: object.item.uri }}
style={{ flex: 1 }}
resizeMode="cover"
/>
</TouchableOpacity>
)
}
/>
</View>
);
return (
<SafeAreaView style={styles.container}>
<View style={styles.actions}>
<Button onPress={() => removeAllPhotos()}>
<Text>{t("mediacache.actions.deleteall")}</Text>
</Button>
<Button onPress={() => uploadAllphotos()}>
<Text>{t("mediacache.actions.uploadall")}</Text>
{uploadInProgress && <ActivityIndicator />}
</Button>
</View>
<FlatList
data={groupedPhotos ? Object.keys(groupedPhotos) : []}
style={{ flex: 1 }}
contentContainerStyle={styles.listContentContainer}
keyExtractor={(item) => item}
renderItem={(object) => (
<RenderJobPictures
jobPhotos={groupedPhotos[object.item]}
jobId={object.item}
/>
)}
/>
<MediaCacheOverlay
imgIndex={imgIndex}
setImgIndex={setImgIndex}
previewVisible={previewVisible}
setPreviewVisible={setPreviewVisible}
/>
<Text>{`${photos.length} Photos`}</Text>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
actions: {
display: "flex",
flexDirection: "row",
justifyContent: "space-evenly",
},
listContentContainer: {
//flex: 1,
justifyContent: "flex-start",
//flexDirection: "row",
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ScreenMediaCache);
// <FlatList
// data={imagesInDir}
// style={{}}
// keyExtractor={(item) => item.id}
// renderItem={(object) => <Text>{object.item}</Text>}
// />;

View File

@@ -1,10 +0,0 @@
import React from "react";
import { View, Text } from "react-native";
export default function ScreenMessagingConversation({ navigation }) {
return (
<View>
<Text></Text>
</View>
);
}

View File

@@ -1,10 +0,0 @@
import React from "react";
import { View, Text } from "react-native";
export default function ScreenMessagingList() {
return (
<View>
<Text>A list of conversations.</Text>
</View>
);
}

View File

@@ -1,90 +0,0 @@
import React, { useState } from "react";
import { Text, View, StyleSheet, TouchableOpacity } from "react-native";
import DraggableFlatList, {
ScaleDecorator,
NestableScrollContainer,
NestableDraggableFlatList,
} from "react-native-draggable-flatlist";
const NUM_ITEMS = 10;
function getColor(i) {
const multiplier = 255 / (NUM_ITEMS - 1);
const colorVal = i * multiplier;
return `rgb(${colorVal}, ${Math.abs(128 - colorVal)}, ${255 - colorVal})`;
}
const initialData = [...Array(NUM_ITEMS)].map((d, index) => {
const backgroundColor = getColor(index);
return {
key: `item-${index}`,
label: String(index) + "",
height: 100,
width: 60 + Math.random() * 40,
backgroundColor,
};
});
const initialData2 = [...Array(NUM_ITEMS)].map((d, index) => {
const backgroundColor = getColor(index);
return {
key: `item-${index}`,
label: String(index) + "",
height: 100,
width: 60 + Math.random() * 40,
backgroundColor,
};
});
export default function App() {
const [data, setData] = useState(initialData);
const [data2, setData2] = useState(initialData2);
const renderItem = ({ item, drag, isActive }) => {
return (
<ScaleDecorator>
<TouchableOpacity
onLongPress={drag}
disabled={isActive}
style={[
styles.rowItem,
{ backgroundColor: isActive ? "red" : item.backgroundColor },
]}
>
<Text style={styles.text}>{item.label}</Text>
</TouchableOpacity>
</ScaleDecorator>
);
};
return (
<NestableScrollContainer>
<NestableDraggableFlatList
data={data}
onDragEnd={({ data }) => setData(data)}
keyExtractor={(item) => item.key}
renderItem={renderItem}
/>
<NestableDraggableFlatList
data={data2}
onDragEnd={({ data }) => setData2(data)}
keyExtractor={(item) => item.key}
renderItem={renderItem}
/>
</NestableScrollContainer>
);
}
const styles = StyleSheet.create({
rowItem: {
height: 100,
width: 100,
alignItems: "center",
justifyContent: "center",
},
text: {
color: "white",
fontSize: 24,
fontWeight: "bold",
textAlign: "center",
},
});

View File

@@ -1,34 +0,0 @@
import Constants from "expo-constants";
import React from "react";
import { useTranslation } from "react-i18next";
import { View, Text } from "react-native";
import { Title, Button } from "react-native-paper";
import { purgeStoredState } from "redux-persist";
import SignOutButton from "../sign-out-button/sign-out-button.component";
import * as Updates from "expo-updates";
import * as Application from "expo-application";
export default function ScreenSettingsComponent() {
const { t } = useTranslation();
return (
<View
style={{
flex: 1,
display: "flex",
alignItems: "center",
flexDirection: "column",
}}
>
<Title>
{t("settings.labels.version", {
number: `${Constants.expoConfig.version}(${Application.nativeBuildVersion} - ${Constants.expoConfig.extra.expover})`,
})}
</Title>
<Text>Release Channel {Updates.channel}</Text>
<SignOutButton />
{/* <Button title="Purge State" onPress={() => purgeStoredState()} /> */}
</View>
);
}

View File

@@ -1,113 +0,0 @@
import { Formik } from "formik";
import React from "react";
import { useTranslation } from "react-i18next";
import { Image, StyleSheet, Text, View } from "react-native";
import { Button, TextInput, Title } from "react-native-paper";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import Logo from "../../assets/logo192.png";
import { emailSignInStart } from "../../redux/user/user.actions";
import {
selectCurrentUser,
selectSigningIn,
} from "../../redux/user/user.selectors";
import SignInErrorAlertComponent from "../sign-in-error-alert/sign-in-error-alert.component";
import Constants from "expo-constants";
import * as Updates from "expo-updates";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
signingIn: selectSigningIn,
});
const mapDispatchToProps = (dispatch) => ({
emailSignInStart: (email, password) =>
dispatch(emailSignInStart({ email, password })),
});
export function SignIn({ emailSignInStart, signingIn }) {
const { t } = useTranslation();
const formSubmit = (values) => {
const { email, password } = values;
emailSignInStart(email, password);
};
return (
<View style={localStyles.content}>
<View
style={{
display: "flex",
marginTop: 80,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-evenly",
}}
>
<Image style={localStyles.logo} source={Logo} />
<Title>{t("app.title")}</Title>
</View>
<View style={{ flex: 1 }}>
<Formik
initialValues={{ email: "", password: "" }}
onSubmit={formSubmit}
>
{({ handleChange, handleBlur, handleSubmit, values }) => (
<View>
<TextInput
label={t("signin.fields.email")}
mode="outlined"
autoCapitalize="none"
keyboardType="email-address"
onChangeText={handleChange("email")}
onBlur={handleBlur("email")}
value={values.email}
style={[localStyles.input]}
/>
<TextInput
label={t("signin.fields.password")}
mode="outlined"
secureTextEntry={true}
autoCorrect={false}
autoCapitalize="none"
onChangeText={handleChange("password")}
onBlur={handleBlur("password")}
value={values.password}
style={[localStyles.input]}
/>
<SignInErrorAlertComponent />
<Button
mode="outlined"
loading={signingIn}
onPress={handleSubmit}
>
<Text>{t("signin.actions.signin")}</Text>
</Button>
</View>
)}
</Formik>
</View>
<Text style={{ padding: 10, alignSelf: "center" }}>
{t("settings.labels.version", {
number: Constants.expoConfig.version,
})}
</Text>
</View>
);
}
const localStyles = StyleSheet.create({
content: {
display: "flex",
flex: 1,
},
logo: { width: 100, height: 100 },
input: {
margin: 12,
},
});
export default connect(mapStateToProps, mapDispatchToProps)(SignIn);

View File

@@ -1,46 +0,0 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { ActivityIndicator, Image, StyleSheet, View } from "react-native";
import { Title, Subheading, Divider } from "react-native-paper";
import Logo from "../../assets/logo192.png";
import SignOutButton from "../sign-out-button/sign-out-button.component";
export default function ScreenSplash({ noAccess }) {
const { t } = useTranslation();
return (
<View style={[localStyles.container]}>
<View style={[localStyles.logoContainer]}>
<Image style={localStyles.logo} source={Logo} />
<Title>{t("app.title")}</Title>
</View>
{noAccess ? (
<View style={[localStyles.logoContainer]}>
<Subheading style={{ textAlign: "center" }}>
{t("app.nomobileaccess")}
</Subheading>
<Divider />
<SignOutButton />
</View>
) : (
<ActivityIndicator color="dodgerblue" size="large" />
)}
</View>
);
}
const localStyles = StyleSheet.create({
container: {
display: "flex",
flex: 1,
flexDirection: "column",
alignContent: "center",
justifyContent: "center",
},
logoContainer: {
display: "flex",
flexDirection: "column",
margin: 10,
alignItems: "center",
},
logo: { width: 175, height: 175, margin: 20 },
});

View File

@@ -1,53 +0,0 @@
import { Title } from "react-native-paper";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { StyleSheet, View } from "react-native";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectSignInError } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
signInError: selectSignInError,
});
export function SignInErrorAlertComponent({ signInError }) {
const [errorText, setErrorText] = useState("");
const { t } = useTranslation();
useEffect(() => {
let text;
if (signInError && signInError.code)
switch (signInError.code) {
case "auth/user-not-found":
text = t("signin.errors.usernotfound");
break;
case "auth/invalid-email":
text = t("signin.errors.emailformat");
break;
case "auth/wrong-password":
text = t("signin.errors.wrongpassword");
break;
default:
text = signInError.code + " " + signInError.message;
break;
}
setErrorText(text);
}, [signInError, setErrorText]);
return (
<View>
{errorText ? <Title style={localStyles.alert}>{errorText}</Title> : null}
</View>
);
}
export default connect(mapStateToProps, null)(SignInErrorAlertComponent);
const localStyles = StyleSheet.create({
alert: {
color: "red",
textAlign: "center",
margin: 15,
padding: 15,
},
});

View File

@@ -1,13 +0,0 @@
import React from "react";
export default function StyleRepeater({ childStyle, children }) {
return (
<>
{React.Children.map(children, (child) =>
React.cloneElement(child, {
style: [child.props.style, childStyle],
})
)}
</>
);
}

View File

@@ -1,12 +0,0 @@
import { StyleSheet } from "react-native";
const cardBackgroundColor = "gainsboro";
export default StyleSheet.create({
cardBackground: {
padding: 5,
backgroundColor: cardBackgroundColor,
display: "flex",
flex: 1,
},
});

View File

@@ -1,52 +0,0 @@
import { useTranslation } from "react-i18next";
import { StyleSheet, Text, View } from "react-native";
import { Switch } from "react-native-paper";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { toggleDeleteAfterUpload } from "../../redux/app/app.actions";
import { selectDeleteAfterUpload } from "../../redux/app/app.selectors";
const mapStateToProps = createStructuredSelector({
deleteAfterUpload: selectDeleteAfterUpload,
});
const mapDispatchToProps = (dispatch) => ({
toggleDeleteAfterUpload: () => dispatch(toggleDeleteAfterUpload()),
});
export function UploadDeleteSwitch({
deleteAfterUpload,
toggleDeleteAfterUpload,
}) {
const { t } = useTranslation();
return (
<View style={styles.container}>
<Text style={styles.text}>
{t("mediabrowser.labels.deleteafterupload")}
</Text>
<Switch
// trackColor={{ false: '#767577', true: '#81b0ff' }}
// thumbColor={deleteAfterUpload ? 'tomato' : '#f4f3f4'}
//ios_backgroundColor="#3e3e3e"
onValueChange={() => {
toggleDeleteAfterUpload();
}}
value={deleteAfterUpload}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
display: "flex",
flexDirection: "row",
alignItems: "center",
margin: 10,
},
text: {
flex: 1,
},
});
export default connect(mapStateToProps, mapDispatchToProps)(UploadDeleteSwitch);

View File

@@ -1,218 +0,0 @@
import * as MediaLibrary from "expo-media-library";
import React, { useEffect, useState } from "react";
import {
ActivityIndicator,
Alert,
Modal,
Platform,
StyleSheet,
Text,
View,
} from "react-native";
import { ProgressBar } from "react-native-paper";
import Toast from "react-native-toast-message";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.analytics";
import {
selectCurrentCameraJobId,
selectDeleteAfterUpload,
} from "../../redux/app/app.selectors";
import * as Sentry from "@sentry/react-native";
import { formatBytes } from "../../util/document-upload.utility";
import { handleLocalUpload } from "../../util/local-document-upload.utility";
const mapStateToProps = createStructuredSelector({
selectedCameraJobId: selectCurrentCameraJobId,
deleteAfterUpload: selectDeleteAfterUpload,
});
export default connect(mapStateToProps, null)(UploadProgress);
export function UploadProgress({
selectedCameraJobId,
deleteAfterUpload,
uploads,
setUploads,
forceRerender,
}) {
const [progress, setProgress] = useState({
loading: false,
uploadInProgress: false,
speed: 0,
});
useEffect(() => {
//Set the state of uploads to do.
if (uploads) {
beginUploads(uploads);
setUploads(null);
}
}, [uploads]);
async function handleOnSuccess({ duration, data }) {
//If it's not in production, show a toast with the time.
Toast.show({
type: "success",
text1: ` Upload completed in ${duration}.`,
//
// text2: duration,
});
if (deleteAfterUpload) {
try {
if (Platform.OS === "android") {
//Create a new asset with the first file to delete.
// console.log('Trying new delete.');
await MediaLibrary.getPermissionsAsync(false);
const album = await MediaLibrary.createAlbumAsync(
"ImEX Mobile Deleted",
data.pop(),
false
);
//Move the rest.
if (data.length > 0) {
const moveResult = await MediaLibrary.addAssetsToAlbumAsync(
data,
album,
false
);
}
const deleteResult = await MediaLibrary.deleteAlbumsAsync(album);
//Delete the album.
//This defaults to delete all assets in the album.
} else {
await MediaLibrary.deleteAssetsAsync(data.map((f) => f.id));
}
} catch (error) {
console.log("Unable to delete picture.", error);
Sentry.captureException(error);
}
}
logImEXEvent("imexmobile_successful_upload");
forceRerender();
setProgress({ ...progress, speed: 0, percent: 1, uploadInProgress: false });
}
function handleOnProgress({ percent, loaded }) {
setProgress((progress) => ({
...progress,
speed: loaded - progress.loaded,
loaded: loaded,
percent,
}));
}
function handleOnError({ assetid, error }) {
logImEXEvent("imexmobile_upload_documents_error");
Toast.show({
type: "error",
text1: "Unable to upload documents.",
text2: error,
autoHide: false,
});
setProgress({
speed: 0,
percent: 1,
uploadInProgress: false,
});
}
const beginUploads = async (data) => {
//Validate to make sure the totals for the file sizes do not exceed the total on the job.
setProgress({
percent: 0,
loaded: 0,
uploadInProgress: true,
start: new Date(),
average: 0,
});
await handleLocalUpload({
files: data,
onError: ({ assetid, error }) => handleOnError({ assetid, error }),
onProgress: ({ percent, loaded }) =>
handleOnProgress({ percent, loaded }),
onSuccess: ({ duration }) => handleOnSuccess({ duration, data }),
context: {
jobid:
selectedCameraJobId !== "temp" ? selectedCameraJobId : "temporary",
},
});
};
return (
<Modal
visible={progress.uploadInProgress}
animationType="slide"
transparent={true}
onRequestClose={() => {
Alert.alert("Cancel?", "Do you want to abort the upload?", [
{
text: "Yes",
onPress: () => {
setUploads(null);
setProgress(null);
},
},
{ text: "No" },
]);
}}
>
<View style={styles.modalContainer}>
<View style={styles.modal}>
<ActivityIndicator style={{ alignSelf: "center", marginTop: 16 }} />
<ProgressBar
progress={progress.percent}
style={{ alignSelf: "center", marginTop: 16 }}
color={progress.percent === 1 ? "green" : "blue"}
/>
<Text style={{ alignSelf: "center", marginTop: 16 }}>{`${formatBytes(
progress.speed
)}/sec`}</Text>
<Text
style={{ alignSelf: "center", marginTop: 16 }}
>{`Avg. ${formatBytes(
progress.loaded / ((new Date() - progress.start) / 1000)
)}/sec`}</Text>
<Text
style={{ alignSelf: "center", marginTop: 16 }}
>{`Total Uploaded ${formatBytes(progress.loaded)}`}</Text>
<Text style={{ alignSelf: "center", marginTop: 16 }}>{`Duration ${(
(new Date() - progress.start) /
1000
).toFixed(1)} sec`}</Text>
</View>
</View>
</Modal>
);
}
const styles = StyleSheet.create({
modalContainer: {
display: "flex",
flex: 1,
justifyContent: "center",
},
modal: {
// flex: 1,
display: "flex",
marginLeft: 20,
marginRight: 20,
backgroundColor: "white",
borderRadius: 20,
padding: 18,
shadowColor: "#000",
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 5,
},
});

View File

@@ -1,426 +0,0 @@
import { useApolloClient } from "@apollo/client";
import { File } from "expo-file-system";
import * as MediaLibrary from "expo-media-library";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import {
ActivityIndicator,
Alert,
Modal,
Platform,
StyleSheet,
Text,
View,
} from "react-native";
import { Divider, ProgressBar } from "react-native-paper";
import Toast from "react-native-toast-message";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import * as Sentry from "@sentry/react-native";
import { logImEXEvent } from "../../firebase/firebase.analytics";
import { GET_DOC_SIZE_TOTALS } from "../../graphql/documents.queries";
import {
selectCurrentCameraJobId,
selectDeleteAfterUpload,
} from "../../redux/app/app.selectors";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { formatBytes, handleUpload } from "../../util/document-upload.utility";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
bodyshop: selectBodyshop,
selectedCameraJobId: selectCurrentCameraJobId,
deleteAfterUpload: selectDeleteAfterUpload,
});
export default connect(mapStateToProps, null)(UploadProgress);
export function UploadProgress({
currentUser,
bodyshop,
selectedCameraJobId,
deleteAfterUpload,
uploads,
setUploads,
forceRerender,
}) {
const [progress, setProgress] = useState({
uploadInProgress: false,
totalToUpload: 0,
totalUploaded: 0,
startTime: null,
totalFiles: 0,
totalFilesCompleted: 0,
currentFile: null,
files: {}, //uri is the key, value is progress
});
let filesToDelete = [];
const client = useApolloClient();
const { t } = useTranslation();
useEffect(() => {
if (uploads) {
onDone(uploads);
setUploads(null);
}
}, [uploads]);
function handleOnSuccess(asset) {
//NEEDS REDO.
filesToDelete.push(asset);
setProgress((progress) => ({
...progress,
// totalUploaded: progress.totalToUpload + asset.size,
totalFilesCompleted: progress.totalFilesCompleted + 1,
files: {
...progress.files,
[asset.uri]: {
...progress.files[asset.uri],
uploadEnd: new Date(),
},
},
}));
}
function handleOnProgress({ uri, filename }, percent, loaded) {
//NEED REDO
setProgress((progress) => {
return {
...progress,
totalUploaded:
progress.totalUploaded +
(loaded - (progress.files[uri]?.loaded || 0)),
files: {
...progress.files,
[uri]: {
...progress.files[uri],
percent,
filename,
speed: loaded - (progress.files[uri]?.loaded || 0),
loaded: loaded,
uploadStart: progress.files[uri]?.uploadStart || new Date(),
},
},
};
});
}
function handleOnError(error) {
logImEXEvent("imexmobile_upload_documents_error", { error });
Toast.show({
type: "error",
text1: "Unable to upload document.",
text2: error,
autoHide: false,
});
}
const onDone = async (selectedFiles) => {
setProgress((progress) => {
return {
...progress,
uploadInProgress: true,
statusText: "Preparing upload...",
};
});
try {
//Validate to make sure the totals for the file sizes do not exceed the total on the job.
const data = [];
const totalOfUploads = await selectedFiles.reduce(async (acc, val) => {
//Get the size of the file based on URI.
if (acc.fileSize) {
return acc + acc.fileSize;
} else {
const info = new File(val.uri).size;
data.push({ ...info, ...val }); //Add in the size.
val.albumId && MediaLibrary.migrateAlbumIfNeededAsync(val.albumId);
return (await acc) + info.size;
}
}, 0);
if (selectedCameraJobId !== "temp") {
const queryData = await client.query({
query: GET_DOC_SIZE_TOTALS,
fetchPolicy: "network-only",
variables: {
jobId: selectedCameraJobId,
},
});
if (
bodyshop.jobsizelimit -
queryData.data.documents_aggregate.aggregate.sum.size <=
totalOfUploads
) {
//No more room... abandon ship.
setProgress((progress) => ({
...progress,
speed: 0,
action: null,
statusText: null,
uploadInProgress: false,
}));
Alert.alert(
t("mediabrowser.labels.storageexceeded_title"),
t("mediabrowser.labels.storageexceeded")
);
return;
}
//We made it this far. We have enough space, so let's start uploading.
setProgress((progress) => ({
...progress,
totalToUpload: totalOfUploads,
totalUploaded: 0,
totalFilesCompleted: 0,
startTime: new Date(),
totalFiles: data.length,
currentFile: null,
statusText: null,
files: {}, //uri is the key, value is progress
}));
}
for (var i = 0; i < data.length + 4; i = i + 4) {
//Reset the files.
setProgress((progress) => ({ ...progress, files: {} }));
let proms = [];
if (data[i]) {
proms.push(CreateUploadProm(data[i]));
}
if (data[i + 1]) {
proms.push(CreateUploadProm(data[i + 1]));
}
if (data[i + 2]) {
proms.push(CreateUploadProm(data[i + 2]));
}
if (data[i + 3]) {
proms.push(CreateUploadProm(data[i + 3]));
}
await Promise.all(proms);
}
} catch (error) {
console.log("Error during upload.", error, error.stack);
Sentry.captureException(error);
setProgress((progress) => ({
...progress,
speed: 0,
action: null,
statusText: null,
uploadInProgress: false,
}));
Alert.alert(
t("mediabrowser.labels.uploaderror_title"),
t("mediabrowser.labels.uploaderror")
);
return;
}
//Everything is uploaded, delete the succesful ones.
if (deleteAfterUpload) {
try {
if (Platform.OS === "android") {
//Create a new asset with the first file to delete.
// console.log('Trying new delete.');
await MediaLibrary.getPermissionsAsync(false);
const album = await MediaLibrary.createAlbumAsync(
"ImEX Mobile Deleted",
filesToDelete.pop(),
false
);
//Move the rest.
if (filesToDelete.length > 0) {
const moveResult = await MediaLibrary.addAssetsToAlbumAsync(
filesToDelete,
album,
false
);
}
const deleteResult = await MediaLibrary.deleteAlbumsAsync(album);
//Delete the album.
//This defaults to delete all assets in the album.
} else {
await MediaLibrary.deleteAssetsAsync(filesToDelete.map((f) => f.id));
}
} catch (error) {
console.log("Unable to delete picture.", error);
Sentry.captureException(error);
}
}
filesToDelete = [];
Toast.show({
type: "success",
text1: ` Upload completed.`,
//
// text2: duration,
});
//Reset state.
setProgress({
uploadInProgress: false,
totalToUpload: 0,
totalUploaded: 0,
totalFilesCompleted: 0,
startTime: null,
totalFiles: 0,
currentFile: null,
files: {},
});
forceRerender();
};
const CreateUploadProm = async (p) => {
return handleUpload(
{
mediaId: p.id,
onError: handleOnError,
onProgress: ({ percent, loaded }) =>
handleOnProgress(p, percent, loaded),
onSuccess: () => handleOnSuccess(p),
},
{
bodyshop: bodyshop,
jobId: selectedCameraJobId !== "temp" ? selectedCameraJobId : null,
uploaded_by: currentUser.email,
photo: p,
}
);
};
return (
<Modal
visible={progress.uploadInProgress}
animationType="slide"
transparent={true}
onRequestClose={() => {
Alert.alert("Cancel?", "Do you want to abort the upload?", [
{
text: "Yes",
onPress: () => {
setUploads(null);
setProgress({
uploadInProgress: false,
totalToUpload: 0,
totalUploaded: 0,
totalFilesCompleted: 0,
startTime: null,
totalFiles: 0,
currentFile: null,
files: {},
});
},
},
{ text: "No" },
]);
}}
>
<View style={styles.modalContainer}>
<View style={styles.modal}>
{Object.keys(progress.files).map((key) => (
<View key={key} style={styles.progressItem}>
<Text style={styles.progressText}>
{progress.files[key].filename}
</Text>
<View style={styles.progressBarContainer}>
<ProgressBar
progress={progress.files[key].percent}
style={styles.progress}
color={progress.files[key].percent === 1 ? "green" : "blue"}
/>
<View
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
}}
>
<Text>{`${formatBytes(
progress.files[key].loaded /
(((progress.files[key].uploadEnd || new Date()) -
progress.files[key].uploadStart) /
1000)
)}/sec`}</Text>
{progress.files[key].percent === 1 && (
<>
<ActivityIndicator style={{ marginLeft: 12 }} />
<Text style={{ marginLeft: 4 }}>Processing...</Text>
</>
)}
</View>
</View>
</View>
))}
<View style={styles.centeredView}>
{progress.statusText ? (
<>
<ActivityIndicator style={{ marginLeft: 12 }} />
<Text style={{ marginLeft: 4 }}>{progress.statusText}</Text>
<Divider />
</>
) : (
<>
<Text>{`${progress.totalFilesCompleted} of ${progress.totalFiles} uploaded.`}</Text>
<Text>{`${formatBytes(progress.totalUploaded)} of ${formatBytes(
progress.totalToUpload
)} uploaded.`}</Text>
</>
)}
</View>
</View>
</View>
</Modal>
);
}
const styles = StyleSheet.create({
modalContainer: {
display: "flex",
flex: 1,
justifyContent: "center",
},
modal: {
//flex: 1,
display: "flex",
marginLeft: 20,
marginRight: 20,
backgroundColor: "white",
borderRadius: 20,
padding: 18,
shadowColor: "#000",
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 5,
},
centeredView: {
justifyContent: "center",
alignItems: "center",
marginTop: 22,
},
progressItem: {
display: "flex",
flexDirection: "row",
alignItems: "center",
marginBottom: 12,
marginLeft: 12,
marginRight: 12,
},
progressText: {
flex: 1,
},
progressBarContainer: {
flex: 3,
marginLeft: 12,
marginRight: 12,
},
});

View File

@@ -47,8 +47,6 @@ export default function GlobalSearch() {
if (!q) return;
setLoading(true);
setError(null);
// TODO: Integrate real search endpoint
console.log(`[GlobalSearch] (debounced placeholder) searching for: "${q}"`);
try {
const searchData = await axios.post(`${env.API_URL}/search`, {
search: q,

View File

@@ -105,11 +105,6 @@ export function JobDocumentsComponent({ bodyshop }) {
setFullPhotos(normalizedImages);
}
} catch (error) {
console.log(
"Error fetching photos:",
error.message,
JSON.stringify(error, null, 2)
);
setError(error.message || "Unknown error fetching photos.");
}
setLoading(false);

View File

@@ -144,7 +144,11 @@ function Tab({ bodyshop, currentUser, signOutStart }) {
"Error",
`Unable to register for notifications: ${error.message}`
);
console.log("Notification registration error:", error);
console.log(
"Notification registration error:",
error,
error.stack
);
}
}}
>
@@ -161,7 +165,9 @@ function Tab({ bodyshop, currentUser, signOutStart }) {
}`}
</Text>
<Text style={styles.paragraph}>
{`${t("settings.labels.signedinuser")} ${currentUser?.email || "Unknown"}`}
{`${t("settings.labels.signedinuser")} ${
currentUser?.email || "Unknown"
}`}
</Text>
</Card.Content>
<Card.Actions>

View File

@@ -1,53 +0,0 @@
/**
* Below are the colors that are used in the app. The colors are defined in the light and dark mode.
* There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
*/
import { Platform } from 'react-native';
const tintColorLight = '#0a7ea4';
const tintColorDark = '#fff';
export const Colors = {
light: {
text: '#11181C',
background: '#fff',
tint: tintColorLight,
icon: '#687076',
tabIconDefault: '#687076',
tabIconSelected: tintColorLight,
},
dark: {
text: '#ECEDEE',
background: '#151718',
tint: tintColorDark,
icon: '#9BA1A6',
tabIconDefault: '#9BA1A6',
tabIconSelected: tintColorDark,
},
};
export const Fonts = Platform.select({
ios: {
/** iOS `UIFontDescriptorSystemDesignDefault` */
sans: 'system-ui',
/** iOS `UIFontDescriptorSystemDesignSerif` */
serif: 'ui-serif',
/** iOS `UIFontDescriptorSystemDesignRounded` */
rounded: 'ui-rounded',
/** iOS `UIFontDescriptorSystemDesignMonospaced` */
mono: 'ui-monospace',
},
default: {
sans: 'normal',
serif: 'serif',
rounded: 'normal',
mono: 'monospace',
},
web: {
sans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
serif: "Georgia, 'Times New Roman', serif",
rounded: "'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif",
mono: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
},
});

2
env.js
View File

@@ -64,11 +64,9 @@ const ENV = {
}
};
const IS_ROME = Constants?.expoConfig?.extra?.appVariant === 'ROME';
//console.log("*** ~ Constants?.expoConfig?.extra:", Constants?.expoConfig);
function getEnvVars() {
if (Updates.channel !== "production") return IS_ROME ? ENV.rometest : ENV.test;
else return IS_ROME ? ENV.romeprod : ENV.prod;
}
console.log('APP_VARIANT:', Constants?.expoConfig?.extra?.appVariant, 'IS_ROME:', IS_ROME, "ENV: ", getEnvVars());
export default getEnvVars();

View File

@@ -25,34 +25,14 @@ const errorLink = onError(
}
);
const subscriptionMiddleware = {
applyMiddleware: async (options, next) => {
options.authToken =
auth.currentUser && (await auth.currentUser.getIdToken(true));
next();
},
};
//wsLink.subscriptionClient.use([subscriptionMiddleware]);
// const link = split(
// // split based on operation type
// ({ query }) => {
// const definition = getMainDefinition(query);
// // console.log(
// // "##Intercepted GQL Transaction : " +
// // definition.operation +
// // "|" +
// // // definition.name.value +
// // "##"
// // );
// return (
// definition.kind === "OperationDefinition" &&
// definition.operation === "subscription"
// );
// const subscriptionMiddleware = {
// applyMiddleware: async (options, next) => {
// options.authToken =
// auth.currentUser && (await auth.currentUser.getIdToken(true));
// next();
// },
// wsLink,
// httpLink
// );
// };
const authLink = setContext((_, { headers }) => {
return (
@@ -84,7 +64,7 @@ const retryLink = new RetryLink({
},
});
const cache = new InMemoryCache({});
const cache = new InMemoryCache();
export const client = new ApolloClient({
//link: ApolloLink.from(middlewares),

21
package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "imexmobile",
"version": "1.0.0",
"dependencies": {
"@apollo/client": "^3.12.11",
"@apollo/client": "^3.14.0",
"@expo/vector-icons": "^15.0.2",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-vector-icons/material-design-icons": "^12.3.0",
@@ -57,6 +57,7 @@
"react-native-image-viewing": "^0.2.2",
"react-native-paper": "^5.14.5",
"react-native-reanimated": "~4.1.3",
"react-native-reanimated-dnd": "^1.1.0",
"react-native-safe-area-context": "~5.6.1",
"react-native-screens": "~4.17.1",
"react-native-tab-view": "4.1.3",
@@ -2792,9 +2793,9 @@
"license": "MIT"
},
"node_modules/@expo/vector-icons": {
"version": "15.0.2",
"resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-15.0.2.tgz",
"integrity": "sha512-IiBjg7ZikueuHNf40wSGCf0zS73a3guJLdZzKnDUxsauB8VWPLMeWnRIupc+7cFhLUkqyvyo0jLNlcxG5xPOuQ==",
"version": "15.0.3",
"resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-15.0.3.tgz",
"integrity": "sha512-SBUyYKphmlfUBqxSfDdJ3jAdEVSALS2VUPOUyqn48oZmb2TL/O7t7/PQm5v4NQujYEPLPMTLn9KVw6H7twwbTA==",
"license": "MIT",
"peerDependencies": {
"expo-font": ">=14.0.4",
@@ -13285,6 +13286,18 @@
"react-native-worklets": ">=0.5.0"
}
},
"node_modules/react-native-reanimated-dnd": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/react-native-reanimated-dnd/-/react-native-reanimated-dnd-1.1.0.tgz",
"integrity": "sha512-9ZgdAFsw2rjB/0VE3wsR9+PnBOWIoO+1s6lEiyV51ptBcoz0NaUnUb8aqGocEztnXSbySSznpOh5vy6Ijo9A3Q==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0",
"react-native": ">=0.60.0",
"react-native-gesture-handler": ">=2.0.0",
"react-native-reanimated": ">=3.0.0"
}
},
"node_modules/react-native-reanimated/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",

View File

@@ -23,7 +23,7 @@
"lint": "expo lint"
},
"dependencies": {
"@apollo/client": "^3.12.11",
"@apollo/client": "^3.14.0",
"@expo/vector-icons": "^15.0.2",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-vector-icons/material-design-icons": "^12.3.0",
@@ -72,6 +72,7 @@
"react-native-image-viewing": "^0.2.2",
"react-native-paper": "^5.14.5",
"react-native-reanimated": "~4.1.3",
"react-native-reanimated-dnd": "^1.1.0",
"react-native-safe-area-context": "~5.6.1",
"react-native-screens": "~4.17.1",
"react-native-tab-view": "4.1.3",

View File

@@ -83,7 +83,7 @@ export function* openImagePickerAction({ payload: jobid }) {
yield put(mediaUploadStart({ photos: result.assets, jobid }));
}
} catch (error) {
console.log("Saga Error: open Picker", error);
// console.log("Saga Error: open Picker", error);
}
}
@@ -93,8 +93,6 @@ export function* onMediaUploadStart() {
export function* mediaUploadStartAction({ payload: { photos, jobid } }) {
try {
console.log("Starting upload for", photos.length, "photos");
const bodyshop = yield select(selectBodyshop);
if (bodyshop.uselocalmediaserver) {
@@ -130,7 +128,7 @@ export function* mediaUploadStartAction({ payload: { photos, jobid } }) {
yield delay(100);
}
}
console.log("All uploads completed. This shouldn't fire before the uploads are done.");
yield put(mediaUploadCompleted(photos));
} catch (error) {