Add job status changer.
This commit is contained in:
@@ -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>
|
||||
|
||||
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 {
|
||||
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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user