Add job status changer.
This commit is contained in:
@@ -2685,6 +2685,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</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>
|
<concept_node>
|
||||||
<name>ins_addr1</name>
|
<name>ins_addr1</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
|
|||||||
166
components/job-status-selector/JobStatusSelector.tsx
Normal file
166
components/job-status-selector/JobStatusSelector.tsx
Normal 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)}
|
||||||
|
* />
|
||||||
|
*/
|
||||||
@@ -7,15 +7,15 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { RefreshControl, ScrollView, StyleSheet, View } from "react-native";
|
import { RefreshControl, ScrollView, StyleSheet, View } from "react-native";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Button,
|
|
||||||
Card,
|
Card,
|
||||||
Menu,
|
Chip,
|
||||||
Text,
|
Text,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from "react-native-paper";
|
} from "react-native-paper";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import DataLabelComponent from "../data-label/data-label";
|
import DataLabelComponent from "../data-label/data-label";
|
||||||
|
import { JobStatusSelector } from "../job-status-selector/JobStatusSelector";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -31,10 +31,7 @@ function JobTombstone({ bodyshop }) {
|
|||||||
},
|
},
|
||||||
skip: !jobId,
|
skip: !jobId,
|
||||||
});
|
});
|
||||||
const [visible, setVisible] = useState(false);
|
console.log("JobTombstone render");
|
||||||
const openMenu = () => setVisible(true);
|
|
||||||
const closeMenu = () => setVisible(false);
|
|
||||||
console.log("JobTombstone render", visible);
|
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
@@ -44,6 +41,8 @@ function JobTombstone({ bodyshop }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const [availableStatuses, setAvailableStatuses] = useState([]);
|
const [availableStatuses, setAvailableStatuses] = useState([]);
|
||||||
|
const job = data?.jobs_by_pk;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!job || !bodyshop) return;
|
if (!job || !bodyshop) return;
|
||||||
|
|
||||||
@@ -88,7 +87,7 @@ function JobTombstone({ bodyshop }) {
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const job = data.jobs_by_pk;
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
@@ -105,23 +104,33 @@ function JobTombstone({ bodyshop }) {
|
|||||||
titleVariant="titleLarge"
|
titleVariant="titleLarge"
|
||||||
/>
|
/>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<Text>{job.status}</Text>
|
<DataLabelComponent
|
||||||
<Menu
|
label={t("objects.jobs.fields.status")}
|
||||||
visible={visible}
|
content={
|
||||||
onDismiss={closeMenu}
|
<JobStatusSelector
|
||||||
anchor={<Button onPress={openMenu}>{job.status}</Button>}
|
statuses={availableStatuses}
|
||||||
>
|
currentStatus={job.status}
|
||||||
{availableStatuses.map((status) => (
|
label={t("jobdetail.labels.jobstatus")}
|
||||||
<Menu.Item key={status} onPress={() => {}} title={status} />
|
/>
|
||||||
))}
|
}
|
||||||
</Menu>
|
/>
|
||||||
{job.inproduction && (
|
{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.inproduction &&
|
||||||
job.production_vars &&
|
job.production_vars &&
|
||||||
!!job.production_vars.note && (
|
!!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.Content>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -430,6 +430,7 @@ export const UPDATE_JOB_STATUS = gql`
|
|||||||
update_jobs(where: { id: { _eq: $jobId } }, _set: { status: $status }) {
|
update_jobs(where: { id: { _eq: $jobId } }, _set: { status: $status }) {
|
||||||
returning {
|
returning {
|
||||||
id
|
id
|
||||||
|
status
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -886,9 +887,8 @@ export const generate_UPDATE_JOB_KANBAN = (
|
|||||||
) => {
|
) => {
|
||||||
const oldChildQuery = `
|
const oldChildQuery = `
|
||||||
updateOldChild: update_jobs(where: { id: { _eq: "${oldChildId}" } },
|
updateOldChild: update_jobs(where: { id: { _eq: "${oldChildId}" } },
|
||||||
_set: {kanbanparent: ${
|
_set: {kanbanparent: ${oldChildNewParent ? `"${oldChildNewParent}"` : null
|
||||||
oldChildNewParent ? `"${oldChildNewParent}"` : null
|
}}) {
|
||||||
}}) {
|
|
||||||
returning {
|
returning {
|
||||||
id
|
id
|
||||||
kanbanparent
|
kanbanparent
|
||||||
@@ -897,9 +897,8 @@ export const generate_UPDATE_JOB_KANBAN = (
|
|||||||
|
|
||||||
const movedQuery = `
|
const movedQuery = `
|
||||||
updateMovedChild: update_jobs(where: { id: { _eq: "${movedId}" } },
|
updateMovedChild: update_jobs(where: { id: { _eq: "${movedId}" } },
|
||||||
_set: {kanbanparent: ${
|
_set: {kanbanparent: ${movedNewParent ? `"${movedNewParent}"` : null
|
||||||
movedNewParent ? `"${movedNewParent}"` : null
|
} , status: "${movedNewStatus}"}) {
|
||||||
} , status: "${movedNewStatus}"}) {
|
|
||||||
returning {
|
returning {
|
||||||
id
|
id
|
||||||
status
|
status
|
||||||
|
|||||||
@@ -177,6 +177,7 @@
|
|||||||
"est_ph1": "Appraiser Phone #",
|
"est_ph1": "Appraiser Phone #",
|
||||||
"federal_tax_payable": "Federal Tax Payable",
|
"federal_tax_payable": "Federal Tax Payable",
|
||||||
"federal_tax_rate": "Federal Tax Rate",
|
"federal_tax_rate": "Federal Tax Rate",
|
||||||
|
"inproduction": "In Production",
|
||||||
"ins_addr1": "Insurance Co. Address",
|
"ins_addr1": "Insurance Co. Address",
|
||||||
"ins_city": "Insurance City",
|
"ins_city": "Insurance City",
|
||||||
"ins_co_id": "Insurance Co. ID",
|
"ins_co_id": "Insurance Co. ID",
|
||||||
|
|||||||
@@ -177,6 +177,7 @@
|
|||||||
"est_ph1": "Número de teléfono del tasador",
|
"est_ph1": "Número de teléfono del tasador",
|
||||||
"federal_tax_payable": "Impuesto federal por pagar",
|
"federal_tax_payable": "Impuesto federal por pagar",
|
||||||
"federal_tax_rate": "",
|
"federal_tax_rate": "",
|
||||||
|
"inproduction": "",
|
||||||
"ins_addr1": "Dirección de Insurance Co.",
|
"ins_addr1": "Dirección de Insurance Co.",
|
||||||
"ins_city": "Ciudad de seguros",
|
"ins_city": "Ciudad de seguros",
|
||||||
"ins_co_id": "ID de la compañía de seguros",
|
"ins_co_id": "ID de la compañía de seguros",
|
||||||
|
|||||||
@@ -177,6 +177,7 @@
|
|||||||
"est_ph1": "Numéro de téléphone de l'évaluateur",
|
"est_ph1": "Numéro de téléphone de l'évaluateur",
|
||||||
"federal_tax_payable": "Impôt fédéral à payer",
|
"federal_tax_payable": "Impôt fédéral à payer",
|
||||||
"federal_tax_rate": "",
|
"federal_tax_rate": "",
|
||||||
|
"inproduction": "",
|
||||||
"ins_addr1": "Adresse Insurance Co.",
|
"ins_addr1": "Adresse Insurance Co.",
|
||||||
"ins_city": "Insurance City",
|
"ins_city": "Insurance City",
|
||||||
"ins_co_id": "ID de la compagnie d'assurance",
|
"ins_co_id": "ID de la compagnie d'assurance",
|
||||||
|
|||||||
Reference in New Issue
Block a user