Add job status changer.

This commit is contained in:
Patrick Fic
2025-10-28 15:32:13 -07:00
parent 9f65ca3c2f
commit 22ce0a4703
7 changed files with 223 additions and 25 deletions

View File

@@ -2685,6 +2685,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>inproduction</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>ins_addr1</name>
<definition_loaded>false</definition_loaded>

View File

@@ -0,0 +1,166 @@
import { UPDATE_JOB_STATUS } from "@/graphql/jobs.queries";
import { useMutation } from "@apollo/client";
import * as Haptics from "expo-haptics";
import { useLocalSearchParams } from "expo-router";
import React, { useCallback, useState } from "react";
import { FlatList, StyleSheet, View } from "react-native";
import {
Button,
Divider,
List,
Modal,
Portal,
Text,
useTheme,
} from "react-native-paper";
/**
* JobStatusSelector component contract
* Props:
* - statuses: string[] (list of available statuses)
* - currentStatus: string (currently applied status)
* - onSelect: (status: string) => void (fires when user selects a status)
* - label?: string (optional label for trigger button)
*/
export interface JobStatusSelectorProps {
statuses: string[];
currentStatus: string | undefined;
onSelect: (status: string) => void;
label?: string;
disabled?: boolean;
}
const keyExtractor = (item: string) => item;
export const JobStatusSelector: React.FC<JobStatusSelectorProps> = ({
statuses,
currentStatus,
onSelect,
label = "Change Status",
disabled = false,
}) => {
const { jobId } = useLocalSearchParams();
const theme = useTheme();
const [visible, setVisible] = useState(false);
const show = () => setVisible(true);
const hide = () => setVisible(false);
const [updateJobStatus] = useMutation(UPDATE_JOB_STATUS);
const handleSelect = useCallback(
async (status: string) => {
Haptics.selectionAsync().catch(() => {});
hide();
await updateJobStatus({
variables: {
jobId,
status,
},
});
if (onSelect && typeof onSelect === "function") onSelect(status);
},
[onSelect, jobId, updateJobStatus]
);
return (
<View style={styles.root}>
<Button
mode="outlined"
icon="playlist-edit"
onPress={show}
disabled={disabled || statuses.length === 0}
style={styles.trigger}
>
{currentStatus || label}
</Button>
<Portal>
<Modal
visible={visible}
onDismiss={hide}
contentContainerStyle={[
{ backgroundColor: theme.colors.surface },
styles.modalContainer,
]}
>
<Text variant="titleMedium" style={styles.modalTitle}>
{label}
</Text>
<Divider />
<FlatList
data={statuses}
keyExtractor={keyExtractor}
renderItem={({ item }) => {
const selected = item === currentStatus;
return (
<List.Item
title={item}
onPress={() => handleSelect(item)}
style={selected ? styles.selectedItem : undefined}
titleStyle={selected ? { fontWeight: "600" } : undefined}
left={(props) =>
selected ? <List.Icon {...props} icon="check" /> : null
}
/>
);
}}
ItemSeparatorComponent={() => <Divider />}
style={styles.list}
keyboardShouldPersistTaps="handled"
/>
<Button
onPress={hide}
style={styles.closeBtn}
mode="text"
icon="close"
>
Close
</Button>
</Modal>
</Portal>
</View>
);
};
const styles = StyleSheet.create({
root: {
alignSelf: "flex-start",
},
trigger: {
minWidth: 140,
},
modalContainer: {
marginHorizontal: 24,
borderRadius: 16,
paddingVertical: 12,
paddingHorizontal: 8,
height: "60%",
display: "flex",
},
modalTitle: {
paddingHorizontal: 12,
paddingBottom: 8,
},
list: {
marginTop: 4,
flex: 1,
},
selectedItem: {
backgroundColor: "rgba(0,0,0,0.05)",
},
closeBtn: {
marginTop: 8,
alignSelf: "flex-end",
},
});
export default JobStatusSelector;
/**
* Usage example:
* <JobStatusSelector
* statuses={availableStatuses}
* currentStatus={job.status}
* onSelect={(newStatus) => console.log('Status changed to', newStatus)}
* />
*/

View File

@@ -7,15 +7,15 @@ import { useTranslation } from "react-i18next";
import { RefreshControl, ScrollView, StyleSheet, View } from "react-native";
import {
ActivityIndicator,
Button,
Card,
Menu,
Chip,
Text,
useTheme,
} from "react-native-paper";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import DataLabelComponent from "../data-label/data-label";
import { JobStatusSelector } from "../job-status-selector/JobStatusSelector";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -31,10 +31,7 @@ function JobTombstone({ bodyshop }) {
},
skip: !jobId,
});
const [visible, setVisible] = useState(false);
const openMenu = () => setVisible(true);
const closeMenu = () => setVisible(false);
console.log("JobTombstone render", visible);
console.log("JobTombstone render");
const theme = useTheme();
@@ -44,6 +41,8 @@ function JobTombstone({ bodyshop }) {
};
const [availableStatuses, setAvailableStatuses] = useState([]);
const job = data?.jobs_by_pk;
useEffect(() => {
if (!job || !bodyshop) return;
@@ -88,7 +87,7 @@ function JobTombstone({ bodyshop }) {
</Card>
);
}
const job = data.jobs_by_pk;
return (
<ScrollView
contentContainerStyle={{
@@ -105,23 +104,33 @@ function JobTombstone({ bodyshop }) {
titleVariant="titleLarge"
/>
<Card.Content>
<Text>{job.status}</Text>
<Menu
visible={visible}
onDismiss={closeMenu}
anchor={<Button onPress={openMenu}>{job.status}</Button>}
>
{availableStatuses.map((status) => (
<Menu.Item key={status} onPress={() => {}} title={status} />
))}
</Menu>
<DataLabelComponent
label={t("objects.jobs.fields.status")}
content={
<JobStatusSelector
statuses={availableStatuses}
currentStatus={job.status}
label={t("jobdetail.labels.jobstatus")}
/>
}
/>
{job.inproduction && (
<Text>{t("objects.jobs.labels.inproduction")}</Text>
<DataLabelComponent
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 && (
<Text>{job.production_vars.note}</Text>
<DataLabelComponent
label={t("objects.jobs.fields.production_note")}
content={<Text>{job.production_vars.note}</Text>}
/>
)}
</Card.Content>
</Card>

View File

@@ -430,6 +430,7 @@ export const UPDATE_JOB_STATUS = gql`
update_jobs(where: { id: { _eq: $jobId } }, _set: { status: $status }) {
returning {
id
status
}
}
}
@@ -886,9 +887,8 @@ export const generate_UPDATE_JOB_KANBAN = (
) => {
const oldChildQuery = `
updateOldChild: update_jobs(where: { id: { _eq: "${oldChildId}" } },
_set: {kanbanparent: ${
oldChildNewParent ? `"${oldChildNewParent}"` : null
}}) {
_set: {kanbanparent: ${oldChildNewParent ? `"${oldChildNewParent}"` : null
}}) {
returning {
id
kanbanparent
@@ -897,9 +897,8 @@ export const generate_UPDATE_JOB_KANBAN = (
const movedQuery = `
updateMovedChild: update_jobs(where: { id: { _eq: "${movedId}" } },
_set: {kanbanparent: ${
movedNewParent ? `"${movedNewParent}"` : null
} , status: "${movedNewStatus}"}) {
_set: {kanbanparent: ${movedNewParent ? `"${movedNewParent}"` : null
} , status: "${movedNewStatus}"}) {
returning {
id
status

View File

@@ -177,6 +177,7 @@
"est_ph1": "Appraiser Phone #",
"federal_tax_payable": "Federal Tax Payable",
"federal_tax_rate": "Federal Tax Rate",
"inproduction": "In Production",
"ins_addr1": "Insurance Co. Address",
"ins_city": "Insurance City",
"ins_co_id": "Insurance Co. ID",

View File

@@ -177,6 +177,7 @@
"est_ph1": "Número de teléfono del tasador",
"federal_tax_payable": "Impuesto federal por pagar",
"federal_tax_rate": "",
"inproduction": "",
"ins_addr1": "Dirección de Insurance Co.",
"ins_city": "Ciudad de seguros",
"ins_co_id": "ID de la compañía de seguros",

View File

@@ -177,6 +177,7 @@
"est_ph1": "Numéro de téléphone de l'évaluateur",
"federal_tax_payable": "Impôt fédéral à payer",
"federal_tax_rate": "",
"inproduction": "",
"ins_addr1": "Adresse Insurance Co.",
"ins_city": "Insurance City",
"ins_co_id": "ID de la compagnie d'assurance",