Update jobline display and related jobs.

This commit is contained in:
Patrick Fic
2025-11-14 14:09:44 -08:00
parent ec7327e1fd
commit 5235519dd8
12 changed files with 326 additions and 59 deletions

View File

@@ -6,7 +6,7 @@
"scheme": "imex-mobile-scheme",
"userInterfaceStyle": "automatic",
"extra": {
"expover": "18",
"expover": "19",
"eas": {
"projectId": "ffe01f3a-d507-4698-82cd-da1f1cad450b"
}
@@ -34,7 +34,9 @@
"permissions": [
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.WRITE_EXTERNAL_STORAGE",
"android.permission.ACCESS_MEDIA_LOCATION"
"android.permission.ACCESS_MEDIA_LOCATION",
"android.permission.READ_MEDIA_IMAGES",
"android.permission.READ_MEDIA_VIDEO"
]
},
"splash": {

View File

@@ -1,6 +1,6 @@
import { openImagePicker } from "@/redux/photos/photos.actions";
import * as Haptics from "expo-haptics";
import { Stack, useLocalSearchParams } from "expo-router";
import { Stack, useGlobalSearchParams } from "expo-router";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { IconButton } from "react-native-paper";
@@ -13,7 +13,7 @@ export default connect(null, mapDispatchToProps)(JobsStack);
function JobsStack({ openImagePicker }) {
const { t } = useTranslation();
const { jobId } = useLocalSearchParams();
const { jobId } = useGlobalSearchParams();
const handleUpload = useCallback(() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);

View File

@@ -1,4 +1,4 @@
<babeledit_project be_version="2.7.1" version="1.2">
<babeledit_project version="1.2" be_version="2.7.1">
<!--
BabelEdit project file
@@ -4585,6 +4585,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>related_ros</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>repairtotal</name>
<definition_loaded>false</definition_loaded>

View File

@@ -1,4 +1,4 @@
import { GET_JOB_BY_PK } from "@/graphql/jobs.queries";
import { GET_JOB_LINES } from "@/graphql/jobs.queries";
import { useQuery } from "@apollo/client";
import { useGlobalSearchParams } from "expo-router";
import React from "react";
@@ -10,7 +10,7 @@ import ErrorDisplay from "../error/error-display";
export default function JobLines() {
const { jobId } = useGlobalSearchParams();
const { loading, error, data, refetch } = useQuery(GET_JOB_BY_PK, {
const { loading, error, data, refetch } = useQuery(GET_JOB_LINES, {
variables: {
id: jobId,
},
@@ -47,13 +47,13 @@ export default function JobLines() {
<DataTable.Title style={{ flex: 2 }}>
{t("jobdetail.labels.lines_lbr_ty")}
</DataTable.Title>
<DataTable.Title style={{ flex: 1 }} numeric>
<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 }} numeric>
<DataTable.Title style={{ flex: 1 }}>
{t("jobdetail.labels.lines_qty")}
</DataTable.Title>
</DataTable.Header>
@@ -71,15 +71,13 @@ export default function JobLines() {
<DataTable.Cell style={{ flex: 2 }}>
{item.mod_lbr_ty && t(`jobdetail.lbr_types.${item.mod_lbr_ty}`)}
</DataTable.Cell>
<DataTable.Cell style={{ flex: 1 }} numeric>
<DataTable.Cell style={{ flex: 1 }}>
{item.mod_lb_hrs}
</DataTable.Cell>
<DataTable.Cell style={{ flex: 2 }}>
{item.part_type && t(`jobdetail.part_types.${item.part_type}`)}
</DataTable.Cell>
<DataTable.Cell style={{ flex: 1 }} numeric>
{item.part_qty}
</DataTable.Cell>
<DataTable.Cell style={{ flex: 1 }}>{item.part_qty}</DataTable.Cell>
</DataTable.Row>
))}
</DataTable>

View File

@@ -1,4 +1,4 @@
import { GET_JOB_BY_PK } from "@/graphql/jobs.queries";
import { GET_JOB_NOTES } from "@/graphql/jobs.queries";
import { useQuery } from "@apollo/client";
import { AntDesign } from "@expo/vector-icons";
import { useGlobalSearchParams } from "expo-router";
@@ -22,7 +22,7 @@ export default function JobNotes() {
const showNoteModal = () => setNoteModalVisible(true);
const hideNoteModal = () => setNoteModalVisible(false);
const { loading, error, data, refetch } = useQuery(GET_JOB_BY_PK, {
const { loading, error, data, refetch } = useQuery(GET_JOB_NOTES, {
variables: {
id: jobId,
},

View File

@@ -1,4 +1,4 @@
import { GET_JOB_BY_PK } from "@/graphql/jobs.queries";
import { GET_JOB_NOTES } from "@/graphql/jobs.queries";
import { INSERT_NEW_NOTE } from "@/graphql/notes.queries";
import { selectCurrentUser } from "@/redux/user/user.selectors";
import { useMutation } from "@apollo/client";
@@ -97,7 +97,7 @@ const NewNoteModal = ({
}, []);
const [insertNote, { loading }] = useMutation(INSERT_NEW_NOTE, {
refetchQueries: [{ query: GET_JOB_BY_PK, variables: { id: jobId } }],
refetchQueries: [{ query: GET_JOB_NOTES, variables: { id: jobId } }],
awaitRefetchQueries: true,
});

View File

@@ -1,10 +1,16 @@
import { GET_JOB_BY_PK } from "@/graphql/jobs.queries";
import { GET_JOB_TOMBSTONE } from "@/graphql/jobs.queries";
import { selectBodyshop } from "@/redux/user/user.selectors";
import { useQuery } from "@apollo/client";
import { useLocalSearchParams } from "expo-router";
import { useLocalSearchParams, useRouter } from "expo-router";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { RefreshControl, ScrollView, StyleSheet, View } from "react-native";
import {
RefreshControl,
ScrollView,
StyleSheet,
TouchableOpacity,
View
} from "react-native";
import { ActivityIndicator, Card, Chip, Text } from "react-native-paper";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -20,7 +26,8 @@ export default connect(mapStateToProps, mapDispatchToProps)(JobTombstone);
function JobTombstone({ bodyshop }) {
const { jobId } = useLocalSearchParams();
const { loading, error, data, refetch } = useQuery(GET_JOB_BY_PK, {
const router = useRouter();
const { loading, error, data, refetch } = useQuery(GET_JOB_TOMBSTONE, {
variables: {
id: jobId,
},
@@ -100,35 +107,65 @@ function JobTombstone({ bodyshop }) {
title={t("jobdetail.labels.jobinfo")}
titleVariant="titleLarge"
/>
<Card.Content>
<DataLabelComponent
label={t("objects.jobs.fields.status")}
content={
<JobStatusSelector
statuses={availableStatuses}
currentStatus={job.status}
label={t("jobdetail.labels.status")}
/>
}
/>
{job.inproduction && (
<Card.Content style={localStyles.twoColumnCard}>
<View style={localStyles.twoColumnCardColumn}>
<DataLabelComponent
label={t("objects.jobs.fields.inproduction")}
label={t("objects.jobs.fields.status")}
content={
<Chip mode="outlined">
{t("objects.jobs.labels.inproduction")}
</Chip>
<JobStatusSelector
statuses={availableStatuses}
currentStatus={job.status}
label={t("jobdetail.labels.status")}
/>
}
/>
)}
{job.inproduction &&
job.production_vars &&
!!job.production_vars.note && (
{job.inproduction && (
<DataLabelComponent
label={t("objects.jobs.fields.production_note")}
content={<Text>{job.production_vars.note}</Text>}
label={t("objects.jobs.fields.inproduction")}
content={
<Chip mode="outlined">
{t("objects.jobs.labels.inproduction")}
</Chip>
}
/>
)}
{job.inproduction &&
job.production_vars &&
!!job.production_vars.note && (
<DataLabelComponent
label={t("objects.jobs.fields.production_note")}
content={<Text>{job.production_vars.note}</Text>}
/>
)}
</View>
<View style={localStyles.twoColumnCardColumn}>
<DataLabelComponent
label={t("objects.jobs.fields.related_ros")}
content={
<View
style={{ flexDirection: "row", flexWrap: "wrap", gap: 8 }}
>
{job.vehicle?.jobs
?.filter((ro) => ro.id !== job.id)
.map((ro) => (
<TouchableOpacity
onPress={() => {
router.navigate({
pathname: `/jobs/${ro.id}`,
params: {
title: ro.ro_number || t("general.labels.na"),
},
});
}}
key={ro.id}
>
<Chip mode="outlined">{ro.ro_number || "N/A"}</Chip>
</TouchableOpacity>
))}
</View>
}
/>
</View>
</Card.Content>
</Card>

View File

@@ -924,3 +924,173 @@ export const generate_UPDATE_JOB_KANBAN = (
}
`;
};
export const GET_JOB_TOMBSTONE = gql`
query GET_JOB_TOMBSTONE($id: uuid!) {
jobs_by_pk(id: $id) {
updated_at
employee_body_rel {
id
first_name
last_name
}
employee_refinish_rel {
id
first_name
last_name
}
employee_prep_rel {
id
first_name
last_name
}
employee_csr_rel {
id
first_name
last_name
}
loss_desc
kmin
kmout
referral_source
unit_number
po_number
special_coverage_policy
scheduled_delivery
converted
ro_number
clm_total
inproduction
vehicleid
plate_no
v_vin
v_model_yr
v_model_desc
v_make_desc
v_color
clm_no
area_of_damage
ins_co_nm
ins_addr1
ins_city
ins_ct_ln
ins_ct_fn
ins_ea
ins_ph1
est_co_nm
est_ct_fn
est_ct_ln
pay_date
est_ph1
est_ea
regie_number
scheduled_completion
id
ded_amt
ded_status
depreciation_taxes
production_vars
other_amount_payable
towing_payable
storage_payable
adjustment_bottom_line
job_totals
ownr_fn
ownr_ln
ownr_ea
ownr_addr1
ownr_addr2
ownr_city
ownr_st
ownr_zip
ownr_ctry
ownr_ph1
actual_in
scheduled_completion
scheduled_in
actual_completion
scheduled_delivery
actual_delivery
date_estimated
date_open
date_scheduled
date_invoiced
date_exported
status
owner_owing
vehicle {
id
jobs {
id
ro_number
status
}
}
}
}
`;
export const GET_JOB_LINES = gql`
query GET_JOB_LINES($id: uuid!) {
jobs_by_pk(id: $id) {
id
joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) {
id
unq_seq
line_ind
tax_part
line_desc
prt_dsmk_p
prt_dsmk_m
part_type
oem_partno
db_price
act_price
part_qty
mod_lbr_ty
db_hrs
mod_lb_hrs
lbr_op
lbr_amt
op_code_desc
}
}
}
`;
export const GET_JOB_NOTES = gql`
query GET_JOB_NOTES($id: uuid!) {
jobs_by_pk(id: $id) {
id
notes(order_by: { created_at: desc }) {
id
text
critical
private
pinned
created_at
updated_at
created_by
}
}
}
`;
export const GET_JOB_MEDIA = gql`
query GET_JOB_MEDIA($id: uuid!) {
jobs_by_pk(id: $id) {
id
documents(order_by: { takenat: desc }) {
id
name
key
created_at
type
extension
}
}
}
`;

View File

@@ -1,5 +1,4 @@
import axios from "axios";
import Constants from "expo-constants";
import * as FileSystem from "expo-file-system/legacy";
import * as ImagePicker from "expo-image-picker";
import * as MediaLibrary from "expo-media-library";
@@ -63,23 +62,24 @@ export function* onOpenImagePicker() {
export function* openImagePickerAction({ payload: jobid }) {
try {
if (Constants.platform.ios) {
const cameraRollStatus =
yield ImagePicker.requestMediaLibraryPermissionsAsync();
const cameraStatus = yield 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.");
return;
}
// if (Constants.platform.ios) {
const cameraRollStatus =
yield ImagePicker.requestMediaLibraryPermissionsAsync();
const cameraStatus = yield 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.");
return;
}
// }
let result = yield ImagePicker.launchImageLibraryAsync({
mediaTypes: ["images", "videos"],
aspect: [4, 3],
quality: 1,
allowsMultipleSelection: true,
allowsEditing: false,
exif: true,
});
if (!(result.canceled)) {
@@ -411,19 +411,55 @@ function* mediaUploadCompletedAction({ payload: photos }) {
const filesToDelete = Object.keys(progress).filter((key) => progress[key].status === 'completed').map((key) => progress[key]);
if (Platform.OS === "android") {
yield MediaLibrary.getPermissionsAsync(false);
const asset = filesToDelete[0];
let assetIdToDelete = asset.assetId;
// 2. ANDROID FIX: Find the original asset ID
if (!assetIdToDelete && Platform.OS === 'android') {
// Fetch the last 50 images from the gallery
const recentAssets = yield call(MediaLibrary.getAssetsAsync, {
first: 50,
sortBy: [MediaLibrary.SortBy.creationTime],
mediaType: MediaLibrary.MediaType.photo,
});
// Try to match based on width, height, and proximity of creation time
// Note: The cache file timestamp might differ slightly from the original
const foundAsset = recentAssets.assets.find(libraryItem => {
console.log("Comparing library item:", moment(asset.exif.DateTime, "YYYY:MM:DD HH:mm:ss").valueOf(), libraryItem.creationTime);
return (
libraryItem.width === asset.exif.ImageWidth &&
libraryItem.height === asset.exif.ImageLength &&
Math.abs(moment(asset.exif.DateTimeOriginal, "YYYY:MM:DD HH:mm:ss").valueOf() - libraryItem.creationTime) < 1000
);
});
if (foundAsset) {
assetIdToDelete = foundAsset.id;
}
}
// 3. Upload and Delete
if (assetIdToDelete) {
// await uploadFunction(asset.uri);
yield call(MediaLibrary.deleteAssetsAsync, assetIdToDelete);
}
//Create a new asset with the first file to delete.
// console.log('Trying new delete.');
yield MediaLibrary.getPermissionsAsync(false);
const album = yield call(MediaLibrary.createAlbumAsync,
"ImEX Mobile Deleted",
filesToDelete.pop(),
filesToDelete.pop().assetId,
false
);
//Move the rest.
if (filesToDelete.length > 0) {
const moveResult = yield call(MediaLibrary.addAssetsToAlbumAsync,
filesToDelete,
filesToDelete.map(f => f.assetId),
album,
false
);
@@ -447,7 +483,7 @@ function* onMediaUploadFailure() {
}
function* mediaUploadFailureAction({ payload: errorMessage }) {
Alert.alert("Upload Error", `An error occurred during upload: ${errorMessage}`);
Alert.alert("Upload Error", `An error occurred during upload: ${JSON.stringify(errorMessage)}`);
}

View File

@@ -271,6 +271,7 @@
"rate_matd": "Tire Disposal",
"referralsource": "Referral Source",
"regie_number": "Registration #",
"related_ros": "Related Jobs",
"repairtotal": "Repair Total",
"ro_number": "RO #",
"scheduled_completion": "Scheduled Completion",

View File

@@ -271,6 +271,7 @@
"rate_matd": "Tasa de eliminación de neumáticos",
"referralsource": "Fuente de referencia",
"regie_number": "N. ° de registro",
"related_ros": "",
"repairtotal": "Reparación total",
"ro_number": "RO #",
"scheduled_completion": "Finalización programada",

View File

@@ -271,6 +271,7 @@
"rate_matd": "Taux d'élimination des pneus",
"referralsource": "Source de référence",
"regie_number": "Enregistrement #",
"related_ros": "",
"repairtotal": "Réparation totale",
"ro_number": "RO #",
"scheduled_completion": "Achèvement planifié",