Add error handling and notes page.
This commit is contained in:
@@ -30,7 +30,7 @@ function JobTabLayout(props) {
|
||||
options={{
|
||||
title: t("jobdetail.labels.lines"),
|
||||
tabBarIcon: ({ color }) => (
|
||||
<FontAwesome size={28} name="cog" color={color} />
|
||||
<FontAwesome size={28} name="list" color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
@@ -39,7 +39,7 @@ function JobTabLayout(props) {
|
||||
options={{
|
||||
title: t("jobdetail.labels.documents"),
|
||||
tabBarIcon: ({ color }) => (
|
||||
<FontAwesome size={28} name="cog" color={color} />
|
||||
<FontAwesome size={28} name="photo" color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
@@ -48,7 +48,7 @@ function JobTabLayout(props) {
|
||||
options={{
|
||||
title: t("jobdetail.labels.notes"),
|
||||
tabBarIcon: ({ color }) => (
|
||||
<FontAwesome size={28} name="cog" color={color} />
|
||||
<FontAwesome size={28} name="sticky-note" color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
import JobDocuments from "../../../components/job-documents/job-documents";
|
||||
function Documents() {
|
||||
return (
|
||||
<View>
|
||||
<Text>Documents</Text>
|
||||
</View>
|
||||
);
|
||||
return <JobDocuments />;
|
||||
}
|
||||
export default Documents;
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
import JobNotes from "../../../components/job-notes/job-notes";
|
||||
function Notes() {
|
||||
return (
|
||||
<View>
|
||||
<Text>Notes</Text>
|
||||
</View>
|
||||
);
|
||||
return <JobNotes />;
|
||||
}
|
||||
export default Notes;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function JobsStack() {
|
||||
const router = useRouter();
|
||||
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Stack
|
||||
screenOptions={{
|
||||
@@ -15,6 +16,7 @@ function JobsStack() {
|
||||
name="index"
|
||||
options={{
|
||||
headerShown: false,
|
||||
title: t("joblist.titles.jobtab"),
|
||||
// headerSearchBarOptions: {
|
||||
// placement: "automatic",
|
||||
// placeholder: "Search",
|
||||
|
||||
@@ -25,58 +25,3 @@ export default function MediaCacheOverlay({
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
||||
// return (
|
||||
// <Modal
|
||||
// onDismiss={() => setPreviewVisible(false)}
|
||||
// onRequestClose={() => setPreviewVisible(false)}
|
||||
// visible={previewVisible}
|
||||
// transparent={false}
|
||||
// >
|
||||
// <SafeAreaView style={{ flex: 1, backgroundColor: "black" }}>
|
||||
// <Gallery
|
||||
// initialPage={imgIndex}
|
||||
|
||||
// images={photos}
|
||||
// onPageScroll={({ position }) => setcurrentIndex(position)}
|
||||
// onPageScrollStateChanged={(state) =>
|
||||
// state === "idle" ? setDragging(false) : setDragging(true)
|
||||
// }
|
||||
// />
|
||||
// <TouchableOpacity
|
||||
// style={{ position: "absolute" }}
|
||||
// onPress={() => setPreviewVisible(false)}
|
||||
// >
|
||||
// <Ionicons
|
||||
// name="ios-close"
|
||||
// size={64}
|
||||
// color="dodgerblue"
|
||||
// style={{ margin: 20 }}
|
||||
// />
|
||||
// </TouchableOpacity>
|
||||
// {!dragging && photos[currentIndex] && photos[currentIndex].videoUrl && (
|
||||
// <TouchableOpacity
|
||||
// style={{
|
||||
// position: "absolute",
|
||||
// left: Dimensions.get("window").width / 2 - 32,
|
||||
// top: Dimensions.get("window").height / 2 - 32,
|
||||
// justifyContent: "center",
|
||||
// alignItems: "center",
|
||||
// }}
|
||||
// onPress={async () => {
|
||||
// await videoRef.current.loadAsync(
|
||||
// { uri: photos[currentIndex].videoUrl },
|
||||
// {},
|
||||
// false
|
||||
// );
|
||||
// videoRef.current.presentFullscreenPlayer();
|
||||
// }}
|
||||
// >
|
||||
// <Ionicons name="play" size={64} color="white" />
|
||||
// </TouchableOpacity>
|
||||
// )}
|
||||
// </SafeAreaView>
|
||||
// <Video ref={videoRef} useNativeControls />
|
||||
// </Modal>
|
||||
// );
|
||||
}
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
import { Text, View } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Text } from "react-native";
|
||||
import { Card } from "react-native-paper";
|
||||
|
||||
export default function ErrorDisplay({ errorMessage }) {
|
||||
export default function ErrorDisplay({ errorMessage, error }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<View style={{ backgroundColor: "red" }}>
|
||||
<Text>{errorMessage}</Text>
|
||||
</View>
|
||||
<Card style={{ margin: 8, backgroundColor: "#ffdddd" }}>
|
||||
<Card.Title title={t("general.labels.error")} titleVariant="titleLarge" />
|
||||
<Card.Content>
|
||||
<Text>
|
||||
{errorMessage ||
|
||||
error?.message ||
|
||||
error ||
|
||||
"An unknown error has occured."}
|
||||
</Text>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
170
components/job-documents/job-documents.jsx
Normal file
170
components/job-documents/job-documents.jsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import cleanAxios from "@/util/CleanAxios";
|
||||
import axios from "axios";
|
||||
import { useGlobalSearchParams } from "expo-router";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
FlatList,
|
||||
Image,
|
||||
RefreshControl,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import ImageView from "react-native-image-viewing";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import env from "../../env";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { DetermineFileType } from "../../util/document-upload.utility";
|
||||
import ErrorDisplay from "../error/error-display";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(JobDocumentsComponent);
|
||||
|
||||
export function JobDocumentsComponent({ bodyshop }) {
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
const [fullphotos, setFullPhotos] = useState([]);
|
||||
const [imgIndex, setImgIndex] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const { jobId } = useGlobalSearchParams();
|
||||
|
||||
const isLms = bodyshop.uselocalmediaserver;
|
||||
const onRefresh = async () => {
|
||||
return getPhotos();
|
||||
};
|
||||
|
||||
const getPhotos = useCallback(async () => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
try {
|
||||
if (!isLms) {
|
||||
const result = await axios.post(
|
||||
`${env.API_URL}/media/imgproxy/thumbnails`,
|
||||
{
|
||||
jobid: jobId,
|
||||
}
|
||||
);
|
||||
|
||||
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 {
|
||||
let localmediaserverhttp = bodyshop.localmediaserverhttp.trim();
|
||||
if (localmediaserverhttp.endsWith("/")) {
|
||||
localmediaserverhttp = localmediaserverhttp.slice(0, -1);
|
||||
}
|
||||
|
||||
const imagesFetch = await cleanAxios.post(
|
||||
`${localmediaserverhttp}/jobs/list`,
|
||||
{
|
||||
jobid: 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,
|
||||
};
|
||||
});
|
||||
|
||||
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);
|
||||
}, [isLms, jobId, bodyshop]);
|
||||
|
||||
useEffect(() => {
|
||||
getPhotos();
|
||||
}, [getPhotos]);
|
||||
|
||||
if (error) {
|
||||
return <ErrorDisplay message={JSON.stringify(error)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<FlatList
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={loading} onRefresh={onRefresh} />
|
||||
}
|
||||
data={fullphotos}
|
||||
ListFooterComponent={
|
||||
<Text style={{ textAlign: "center", padding: 8 }}>{`${
|
||||
fullphotos.length
|
||||
} document${fullphotos.length === 1 ? "" : "s"}`}</Text>
|
||||
}
|
||||
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>
|
||||
)}
|
||||
/>
|
||||
|
||||
<ImageView
|
||||
onRequestClose={() => setPreviewVisible(false)}
|
||||
visible={previewVisible}
|
||||
images={fullphotos}
|
||||
imageIndex={imgIndex}
|
||||
swipeToCloseEnabled={true}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -3,10 +3,11 @@ import { useQuery } from "@apollo/client";
|
||||
import { useGlobalSearchParams } from "expo-router";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ScrollView, Text } from "react-native";
|
||||
import { ActivityIndicator, Card, DataTable } from "react-native-paper";
|
||||
import { ScrollView } from "react-native";
|
||||
import { ActivityIndicator, DataTable } from "react-native-paper";
|
||||
import ErrorDisplay from "../error/error-display";
|
||||
|
||||
export default function JobLines(props) {
|
||||
export default function JobLines() {
|
||||
const { jobId } = useGlobalSearchParams();
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(GET_JOB_BY_PK, {
|
||||
@@ -16,22 +17,25 @@ export default function JobLines(props) {
|
||||
skip: !jobId,
|
||||
});
|
||||
|
||||
console.log("*** ~ JobLines ~ error:", error);
|
||||
const { t } = useTranslation();
|
||||
const onRefresh = async () => {
|
||||
return refetch();
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
if (!data?.jobs_by_pk) {
|
||||
return (
|
||||
<Card>
|
||||
<Text>Job is not defined.</Text>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
const job = data.jobs_by_pk;
|
||||
|
||||
if (error) {
|
||||
return <ErrorDisplay message={JSON.stringify(error)} />;
|
||||
}
|
||||
|
||||
if (!data?.jobs_by_pk) {
|
||||
return <ErrorDisplay message={"Job is not defined."} />;
|
||||
}
|
||||
|
||||
const job = data.jobs_by_pk;
|
||||
return (
|
||||
<ScrollView>
|
||||
<DataTable>
|
||||
|
||||
100
components/job-notes/job-notes.jsx
Normal file
100
components/job-notes/job-notes.jsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { GET_JOB_BY_PK } from "@/graphql/jobs.queries";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { AntDesign } from "@expo/vector-icons";
|
||||
import { useGlobalSearchParams } from "expo-router";
|
||||
import { DateTime } from "luxon";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FlatList, RefreshControl, Text, View } from "react-native";
|
||||
import { ActivityIndicator, Card } from "react-native-paper";
|
||||
import ErrorDisplay from "../error/error-display";
|
||||
|
||||
export default function JobNotes() {
|
||||
const { jobId } = useGlobalSearchParams();
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(GET_JOB_BY_PK, {
|
||||
variables: {
|
||||
id: jobId,
|
||||
},
|
||||
skip: !jobId,
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
const onRefresh = async () => {
|
||||
return refetch();
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
if (error) {
|
||||
return <ErrorDisplay message={JSON.stringify(error?.message)} />;
|
||||
}
|
||||
|
||||
if (!data?.jobs_by_pk) {
|
||||
return <ErrorDisplay errorMessage={"Job is not defined."} />;
|
||||
}
|
||||
const job = data.jobs_by_pk;
|
||||
|
||||
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) => <NoteListItem item={object.item} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -48,7 +48,10 @@ export default function JobTombstone() {
|
||||
}
|
||||
>
|
||||
<Card>
|
||||
<Card.Title title={t("jobdetail.labels.jobinfo")} />
|
||||
<Card.Title
|
||||
title={t("jobdetail.labels.jobinfo")}
|
||||
titleVariant="titleLarge"
|
||||
/>
|
||||
<Card.Content>
|
||||
<Text>{job.status}</Text>
|
||||
{job.inproduction && (
|
||||
@@ -63,7 +66,10 @@ export default function JobTombstone() {
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Card.Title title={t("jobdetail.labels.claiminformation")} />
|
||||
<Card.Title
|
||||
title={t("jobdetail.labels.claiminformation")}
|
||||
titleVariant="titleLarge"
|
||||
/>
|
||||
<Card.Content style={localStyles.twoColumnCard}>
|
||||
<View style={localStyles.twoColumnCardColumn}>
|
||||
<DataLabelComponent
|
||||
@@ -97,7 +103,10 @@ export default function JobTombstone() {
|
||||
</Card.Content>
|
||||
</Card>
|
||||
<Card>
|
||||
<Card.Title title={t("jobdetail.labels.employeeassignments")} />
|
||||
<Card.Title
|
||||
title={t("jobdetail.labels.employeeassignments")}
|
||||
titleVariant="titleLarge"
|
||||
/>
|
||||
<Card.Content>
|
||||
<DataLabelComponent
|
||||
label={t("objects.jobs.fields.employee_body")}
|
||||
@@ -138,7 +147,10 @@ export default function JobTombstone() {
|
||||
</Card.Content>
|
||||
</Card>
|
||||
<Card>
|
||||
<Card.Title title={t("jobdetail.labels.dates")} />
|
||||
<Card.Title
|
||||
title={t("jobdetail.labels.dates")}
|
||||
titleVariant="titleLarge"
|
||||
/>
|
||||
<Card.Content style={localStyles.twoColumnCard}>
|
||||
<View style={localStyles.twoColumnCardColumn}>
|
||||
<DataLabelComponent
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"signout": "Sign Out"
|
||||
},
|
||||
"labels": {
|
||||
"na": "N/A"
|
||||
"na": "N/A",
|
||||
"error": "Error"
|
||||
}
|
||||
},
|
||||
"jobdetail": {
|
||||
|
||||
Reference in New Issue
Block a user