Merge remote-tracking branch 'origin/feature/IO-2236-timetickets' into rome/1.4.1

This commit is contained in:
Patrick Fic
2023-08-02 13:44:18 -07:00
50 changed files with 4016 additions and 130 deletions

View File

@@ -0,0 +1,24 @@
import React from "react";
import { Text } from "react-native";
import { Button } from "react-native-paper";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useNavigation } from "@react-navigation/native";
export function AddTimeTicketButton() {
const navigation = useNavigation();
const { t } = useTranslation();
return (
<Button
mode="text"
compact={true}
onPress={() => {navigation.navigate("CreateTimeTicket");}}
icon="plus"
>
<Text style={{fontSize: 12}}>{t("timeticketbrowser.actions.ticket")}</Text>
</Button>
);
}
export default connect(null, null)(AddTimeTicketButton);

View File

@@ -0,0 +1,26 @@
import React from "react";
import { Text } from "react-native";
import { Button } from "react-native-paper";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { employeeSignOut } from "../../redux/employee/employee.actions";
const mapDispatchToProps = (dispatch) => ({
signOut: () => dispatch(employeeSignOut()),
});
export function SignOutButton({ signOut,style }) {
const { t } = useTranslation();
return (
<Button
mode="text"
compact={true}
onPress={() => signOut()}
icon="logout"
style={style}
>
<Text style={{fontSize: 12}}>{t("general.actions.logout")}</Text>
</Button>
);
}
export default connect(null, mapDispatchToProps)(SignOutButton);

View File

@@ -0,0 +1,220 @@
import { useLazyQuery, useQuery } from "@apollo/client";
import { Ionicons } from "@expo/vector-icons";
import React from "react";
import { useTranslation } from "react-i18next";
import { FlatList, RefreshControl, Text, View } from "react-native";
import { Button, List, Modal, Portal, Searchbar } from "react-native-paper";
import { connect } from "react-redux";
import { SEARCH_JOBS_FOR_AUTOCOMPLETE } from "../../graphql/jobs.queries";
import ErrorDisplay from "../error-display/error-display.component";
import _ from "lodash";
import { useCallback,useRef, useEffect } from "react";
const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
return isMounted.current;
};
export function JobSearchAndSelectModal(props) {
const { t } = useTranslation();
const jobSrchNotExported =
props?.notExported !== undefined ? props.notExported : true;
const jobSrchNotInvoiced =
props?.notInvoiced !== undefined ? props.notInvoiced : false;
const jobSrchConvertedOnly =
props?.convertedOnly !== undefined ? props.convertedOnly : false;
const jobSrchCurrentValue =
props?.currentValue !== undefined
? props.currentValue
: { id:"temp", ro_number:t("selectjobid.labels.noselection") };
const jobSrchOnSetCurrentValue =
props?.onSetCurrentValue !== undefined
? props.onSetCurrentValue
: (e) => {
console.info("onSetCurrentValue was called", e);
};
const [visible, setVisible] = React.useState(false);
const [searchText, setSearchText] = React.useState("");
const showModal = () => setVisible(true);
const hideModal = () => setVisible(false);
const [searchQuery, { loading, error, data, refetch }] = useLazyQuery(
SEARCH_JOBS_FOR_AUTOCOMPLETE,{fetchPolicy: "cache-and-network",}
);
if (error) return <ErrorDisplay errorMessage={error.message} />;
const onRefresh = async () => {
refetch({
notInvoiced: jobSrchNotInvoiced,
notExported: jobSrchNotExported,
isConverted: jobSrchConvertedOnly,
search: searchText,
});
};
const search = (v) => {if (v && v !== "") searchQuery(v);};
const searchDebouncer = useCallback(_.debounce(search, 1000),[]);
const onChangeSearch = (query) => {
setSearchText(query);
searchDebouncer({
variables: {
search: query,
...(jobSrchConvertedOnly || jobSrchNotExported
? {
...(jobSrchConvertedOnly ? { isConverted: true } : {}),
...(jobSrchNotExported ? { notExported: true } : {}),
...(jobSrchNotInvoiced ? { notInvoiced: true } : {}),
}
: {}),
},
});
};
const inputSearch = useRef(null);
const isMounted = useIsMounted();
const setFocus = useCallback(() => {
if (inputSearch.current) {
inputSearch.current.focus();
}
}, []);
useEffect(() => {
setTimeout(() => {
if (visible) {
setFocus();
}
}, 300);
}, [visible, isMounted]);
return (
<>
<Portal>
<Modal
visible={visible}
onDismiss={hideModal}
contentContainerStyle={{
paddingTop: 20,
paddingBottom: 20,
margin: 12,
flex: 1,
backgroundColor: "white",
}}
>
<View
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
margin: 8,
}}
>
<Button onPress={() => hideModal()}>
<Ionicons name="ios-arrow-back" size={32} color="dodgerblue" />
</Button>
<Searchbar
style={{ flex: 1 }}
onChangeText={onChangeSearch}
value={searchText}
ref={inputSearch}
placeholder={t("selectjobid.labels.searchplaceholder")}
/>
</View>
<FlatList
refreshControl={
<RefreshControl refreshing={loading} onRefresh={onRefresh} />
}
data={data?.search_jobs}
keyExtractor={(item) => item.id}
renderItem={(object) => (
<List.Item
onPress={() => {
jobSrchOnSetCurrentValue(object.item);
hideModal();
setSearchText("");
}}
left={() => {
if (object.item.id !== jobSrchCurrentValue?.id) return null;
return (
<Ionicons
name="ios-checkmark-circle"
size={24}
color="dodgerblue"
style={{ alignSelf: "center" }}
/>
);
}}
titleStyle={{
...(object.item.id === jobSrchCurrentValue?.id
? { color: "dodgerblue" }
: {}),
}}
title={`${
object.item.ro_number ? `${object.item.ro_number} ` : ``
}${object.item.ownr_fn || ""} ${object.item.ownr_ln || ""}${
object.item.v_model_yr ? `- ${object.item.v_model_yr}` : ""
}${
object.item.v_make_desc ? `- ${object.item.v_make_desc}` : ""
}${
object.item.v_model_desc
? `- ${object.item.v_model_desc}`
: ""
}`}
key={object.item.id}
/>
)}
ListEmptyComponent={
<View
style={{
flex: 1,
padding: 10,
alignItems: "center",
justifyContent: "center",
}}
>
<Text>{t("selectjobid.labels.nodata")}</Text>
</View>
}
/>
</Modal>
</Portal>
<Button
mode="outlined"
style={{
margin: 8,
height: 50,
borderColor: "gray",
borderWidth: 0.5,
borderRadius: 4,
marginVertical: 4,
marginHorizontal: 16,
justifyContent: "center",
}}
onPress={showModal}
>
{jobSrchCurrentValue?.id
? jobSrchCurrentValue?.id === "temp"
? t("selectjobid.labels.noselection")
: `${
jobSrchCurrentValue.ro_number
? `${jobSrchCurrentValue.ro_number} - `
: ``
}${jobSrchCurrentValue.ownr_fn || ""} ${
jobSrchCurrentValue.ownr_ln || ""
} - ${jobSrchCurrentValue.v_model_yr || ""} ${
jobSrchCurrentValue.v_make_desc || ""
} ${jobSrchCurrentValue.v_model_desc || ""}`
: t("selectjobid.labels.placeholder")}
</Button>
</>
);
}
export default connect(null, null)(JobSearchAndSelectModal);

View File

@@ -0,0 +1,108 @@
import React, { useState } from "react";
import { StyleSheet, View } from "react-native";
import { Dropdown } from "react-native-element-dropdown";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { useEffect } from "react";
import { t } from "i18next";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
export function CostCenterSelect(props) {
const currentRatesNCostCenters = props.currentRatesNCostCenters;
const bodyshop = props.bodyshop;
// const [value, setValue] = useState(null);
const [isFocus, setIsFocus] = useState(false);
const [costCenters, setCostCenters] = useState([]);
useEffect(() => {
if (typeof currentRatesNCostCenters !== "undefined") {
var count = Object.keys(currentRatesNCostCenters).length;
let selectDataArray = [];
for (let i = 0; i < count; i++) {
selectDataArray.push({
value: currentRatesNCostCenters[i].cost_center,
label: bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber ? t(
`joblines.fields.lbr_types.${currentRatesNCostCenters[i].cost_center.toUpperCase()}`
)
: currentRatesNCostCenters[i].cost_center,
rate: currentRatesNCostCenters[i].rate,
});
}
setCostCenters(selectDataArray);
}
}, [currentRatesNCostCenters]);
return (
<View style={styles.container}>
<Dropdown
style={[styles.dropdown, isFocus && { borderColor: "blue" }]}
placeholderStyle={styles.placeholderStyle}
selectedTextStyle={styles.selectedTextStyle}
inputSearchStyle={styles.inputSearchStyle}
iconStyle={styles.iconStyle}
maxHeight={200}
labelField="label"
valueField="value"
placeholder={!isFocus ? t("selectcostcenter.labels.placeholder") : t("selectcostcenter.labels.selectedplaceholder")}
searchPlaceholder={t("selectcostcenter.labels.searchplaceholder")}
onFocus={() => setIsFocus(true)}
onBlur={() => setIsFocus(false)}
data={costCenters}
value={props.currentValue?.value}
onChange={(item) => {
props.onValueSelected(item);
setIsFocus(false);
}}
/>
</View>
);
}
export default connect(mapStateToProps, null)(CostCenterSelect);
const styles = StyleSheet.create({
container: {
marginVertical: 4,
marginHorizontal: 16,
justifyContent: "center",
alignContent: "center",
},
dropdown: {
height: 50,
borderColor: "gray",
borderWidth: 0.5,
borderRadius: 4,
paddingHorizontal: 8,
},
icon: {
marginRight: 5,
},
label: {
position: "absolute",
backgroundColor: "white",
left: 22,
top: 8,
zIndex: 999,
paddingHorizontal: 8,
fontSize: 14,
},
placeholderStyle: {
fontSize: 14,
},
selectedTextStyle: {
fontSize: 14,
},
iconStyle: {
width: 20,
height: 20,
},
inputSearchStyle: {
height: 40,
fontSize: 14,
},
});

View File

@@ -0,0 +1,227 @@
import { useLazyQuery } from "@apollo/client";
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import _ from "lodash";
import {
SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE,
SEARCH_JOBS_FOR_AUTOCOMPLETE,
} from "../../graphql/jobs.queries";
import { StyleSheet, Text, View } from "react-native";
import { Dropdown } from "react-native-element-dropdown";
import { connect } from "react-redux";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
export function JobIdSearchSelect(
props
// //currentValue,
// ...restProps
) {
// console.log("JobIdSearchSelectprops:", props);
const notExported =
props.notExported !== undefined ? props.notExported : true;
const notInvoiced =
props.notInvoiced !== undefined ? props.notInvoiced : false;
const convertedOnly =
props.convertedOnly !== undefined ? props.convertedOnly : false;
const clm_no = props.clm_no !== undefined ? props.clm_no : false;
// console.log("notExported:", notExported);
// console.log("notInvoiced:", notInvoiced);
// console.log("convertedOnly:", convertedOnly);
// console.log("clm_no:", clm_no);
const { t } = useTranslation();
const [theOptions, setTheOptions] = useState([]);
const [selectorData, setSelectorData] = useState([]);
const [selectedvalue, setSelectedValue] = useState(null);
const [isFocus, setIsFocus] = useState(false);
const [callIdSearch, { loading: idLoading, error: idError, data: idData }] =
useLazyQuery(SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE);
const [callSearch, { loading, error, data }] = useLazyQuery(
SEARCH_JOBS_FOR_AUTOCOMPLETE,
{}
);
const executeSearch = (v) => {
// console.log("executeSearchWithV:", v);
if (v && v !== "") callSearch(v);
};
const debouncedExecuteSearch = _.debounce(executeSearch, 500);
const handleSearch = (value) => {
// console.log("handleSearchWithValue:", value);
debouncedExecuteSearch({
variables: {
search: value,
...(convertedOnly || notExported
? {
...(convertedOnly ? { isConverted: true } : {}),
...(notExported ? { notExported: true } : {}),
...(notInvoiced ? { notInvoiced: true } : {}),
}
: {}),
},
});
};
// useEffect(() => {
// console.log("useEfectDependentOn: restProps.value, callIdSearch", restProps);
// if (restProps.value) {
// console.log("restPropsValue:", restProps.value);
// callIdSearch({ variables: { id: restProps.value } });
// }
// }, [restProps.value, callIdSearch]);
useEffect(() => {
// console.log("useEfectDependentOn: [data, idData]");
// console.log( "idDataValue:", idData && idData.jobs_by_pk ? [idData.jobs_by_pk] : []);
// console.log("dataValue:", data && data.search_jobs ? data.search_jobs : []);
if (data) {
setTheOptions(
_.uniqBy(
[
...(idData && idData.jobs_by_pk ? [idData.jobs_by_pk] : []),
...(data && data.search_jobs ? data.search_jobs : []),
],
"id"
)
);
}
}, [data, idData]);
useEffect(() => {
if (typeof theOptions !== "undefined") {
// console.log("useEfectDependentOn: [theOptions]");
var count = Object.keys(theOptions).length;
// console.log("useEfectDependentOn: [theOptions] count:", count);
let selectDataArray = [];
for (let i = 0; i < count; i++) {
selectDataArray.push({
value: theOptions[i].id,
label: `${
clm_no && theOptions[i].clm_no ? `${theOptions[i].clm_no} | ` : ""
}${
theOptions[i].ro_number || t("general.labels.na")
} | ${OwnerNameDisplayFunction(theOptions[i])} | ${
theOptions[i].v_model_yr || ""
} ${theOptions[i].v_make_desc || ""} ${
theOptions[i].v_model_desc || ""
}`,
});
}
setSelectorData(selectDataArray);
}
}, [theOptions]);
// useEffect(() => {
// console.log("useEffectonselectedvaluechange:", selectedvalue.value);
// if (typeof onJobSelected !== "undefined") {
// console.log("onJobSelected:", selectedvalue.value);
// onJobSelected(selectedvalue.value);
// }
// }, [selectedvalue]);
return (
<View style={styles.container}>
<Dropdown
style={[styles.dropdown, isFocus && { borderColor: "blue" }]}
placeholderStyle={styles.placeholderStyle}
selectedTextStyle={styles.selectedTextStyle}
inputSearchStyle={styles.inputSearchStyle}
iconStyle={styles.iconStyle}
search
maxHeight={200}
labelField="label"
valueField="value"
placeholder={!isFocus ? t("selectjobid.labels.placeholder") : t("selectjobid.labels.selectedplaceholder")}
searchPlaceholder={t("selectjobid.labels.searchplaceholder")}
onFocus={() => setIsFocus(true)}
onBlur={() => setIsFocus(false)}
data={selectorData}
value={props.currentValue?.value} //{selectedvalue}
onChange={(item) => {
// console.log("onChange Fired!!!!");
props.onJobSelected(item);
setIsFocus(false);
}}
//TODO: add setIsFocus(false); to this
// {
// console.log("onValueSelected!!!!");
// // (item) => {onJobSelected(item.value)};
// console.log("onChangefired!!!!");
// console.log("itemSelected", item);
// onJobSelected(item.value);
// setSelectedValue(item.value);
// console.log("onValueSelected",onValueSelected);
// if (onValueSelected){
// console.log("onValueSelected!!!!");
// }
//console.log(item);
// setSelectedValue(item.value);
// if (onValueSelected) onValueSelected(item.value);
// setIsFocus(false);
// }}
onChangeText={(search) => {
if (search && search !== "") {
// console.log("onChangeTextFired!!!!");
handleSearch(search);
}
}}
/>
{/* {theOptions ? console.log(theOptions): null} */}
</View>
);
}
export default connect(null, null)(JobIdSearchSelect);
const styles = StyleSheet.create({
container: {
marginVertical: 4,
marginHorizontal: 16,
justifyContent: "center",
alignContent: "center",
},
dropdown: {
height: 50,
borderColor: "gray",
borderWidth: 0.5,
borderRadius: 4,
paddingHorizontal: 8,
},
icon: {
marginRight: 5,
},
label: {
position: "absolute",
backgroundColor: "white",
left: 22,
top: 8,
zIndex: 999,
paddingHorizontal: 8,
fontSize: 14,
},
placeholderStyle: {
fontSize: 14,
},
selectedTextStyle: {
fontSize: 14,
},
iconStyle: {
width: 20,
height: 20,
},
inputSearchStyle: {
height: 40,
fontSize: 14,
},
});

View File

@@ -1,10 +1,20 @@
import React from "react";
import { View, Text } from "react-native";
import { StyleSheet,View, Text } from "react-native";
import { Title } from "react-native-paper";
export default function ErrorDisplay({ errorMessage }) {
return (
<View style={{ backgroundColor: "red" }}>
<Text>{errorMessage}</Text>
<View >
<Title style={localStyles.alert}>{errorMessage}</Title>
{/* <Text>{errorMessage}</Text> */}
</View>
);
}
const localStyles = StyleSheet.create({
alert: {
color: "red",
textAlign: "center",
margin: 8,
padding: 8,
},
});

View File

@@ -12,6 +12,7 @@ const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
setCameraJobId: (id) => dispatch(setCameraJobId(id)),
setCameraJob: (job) => dispatch(setCameraJob(job)),
// setTmTicketJobId:(id) => dispatch(setTmTicketJobId(id)),
});
export function JobListItem({ setCameraJob, setCameraJobId, item }) {
@@ -71,6 +72,7 @@ export function JobListItem({ setCameraJob, setCameraJobId, item }) {
onPress={() => {
logImEXEvent("imexmobile_setcamerajobid_row");
setCameraJobId(item.id);
// setTmTicketJobId(item.id);
setCameraJob(item);
navigation.navigate("MediaBrowserTab");
}}

View File

@@ -0,0 +1,27 @@
import { View, Text, Platform } from "react-native";
import React from "react";
import { KeyboardAvoidingView } from "react-native";
import { StyleSheet } from "react-native";
const KeyboardAvoidingComponent = ({ children }) => {
return (
<KeyboardAvoidingView
enabled
behavior={Platform.OS === "ios" ? "padding":"height" }
style={styles.container}
>
<View style={styles.inner}>{children}</View>
</KeyboardAvoidingView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
inner: {
flex: 1,
},
});
export default KeyboardAvoidingComponent;

View File

@@ -0,0 +1,190 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { FlatList, StyleSheet, Text, View } from "react-native";
import _ from "lodash";
import { ActivityIndicator, Card, DataTable, Divider } from "react-native-paper";
import { GET_LINE_TICKET_BY_PK } from "../../graphql/jobs.queries";
import ErrorDisplay from "../error-display/error-display.component";
import { useQuery } from "@apollo/client";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { selectCurrentEmployee } from "../../redux/employee/employee.selectors";
import { CalculateAllocationsTotals } from "../../util/labor-allocations-table.utility";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
technician: selectCurrentEmployee,
});
export function LaborAllocationsTable({ jobId, bodyshop, technician,costCenterDiff,selectedCostCenter, style, shouldRefresh}) {
const { t } = useTranslation();
const onRefresh = async () => { return refetch(); };
const { loading, error, data, refetch } = useQuery(GET_LINE_TICKET_BY_PK, {
variables: { id: jobId },
skip: !!!jobId,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
if (error) return <ErrorDisplay errorMessage={error.message} />;
const [totals, setTotals] = useState([]);
useEffect(() => {
if (!!data?.joblines && !!data?.timetickets && !!bodyshop){
let temptotals = CalculateAllocationsTotals(
bodyshop,
data?.joblines,
data?.timetickets,
data?.adjustments
);
if(!!selectedCostCenter){
let tempCostCenterDiff = Math.round(
temptotals.find(
(total) =>
total["cost_center"] === selectedCostCenter.label
)?.difference * 10
) / 10;
costCenterDiff.current= tempCostCenterDiff;
}
setTotals(
temptotals
);
}
if (!jobId) setTotals([]);
}, [data?.joblines, data?.timetickets, bodyshop, data?.adjustments, jobId, selectedCostCenter]);
useEffect(() => {
!!shouldRefresh && shouldRefresh ? onRefresh() : null
},[shouldRefresh]);
const summary =
totals &&
totals.reduce(
(acc, val) => {
acc.hrs_total += val.total;
acc.hrs_claimed += val.claimed;
acc.adjustments += val.adjustments;
acc.difference += val.difference;
return acc;
},
{ hrs_total: 0, hrs_claimed: 0, adjustments: 0, difference: 0 }
);
if (loading) return <ActivityIndicator color="dodgerblue" size="large" />;
return (
<View style={style}>
{typeof data !== "undefined" ? (
<Card style={{ flex: 1 }}>
<Card.Title title={t("laborallocations.labels.laborallocations")} />
<Card.Content>
<View style={localStyles.headerArea}>
<Text style={localStyles.headertext}>{t("laborallocations.labels.costcenter")}</Text>
<Text style={localStyles.headertext}>{t("laborallocations.labels.hourstotal")}</Text>
<Text style={localStyles.headertext}>{t("laborallocations.labels.hoursclaimed")}</Text>
{/* <Text numberOfLines={2} style={{ flex: 1, flexWrap:'wrap' }}>Hours Claimed</Text> */}
<Text style={localStyles.headertextAdjusts}>{t("laborallocations.labels.adjustments")}</Text>
<Text style={localStyles.headertext}>{t("laborallocations.labels.difference")}</Text>
</View>
<Divider orientation="vertical" />
<DataTable>
<FlatList
data={totals}
keyExtractor={(item) => item.cost_center}
ItemSeparatorComponent={<Divider orientation="vertical" />}
renderItem={(object) => (
<DataTable.Row style={{ flex: 1, justifyContent:"space-evenly" }}>
<View style={{ flex: 1, alignItems: "flex-start" }}>
<Text>
{object.item.cost_center} {" ("}
{object.item.mod_lbr_ty}
{")"}
</Text>
</View>
<View style={{ flex: 1, alignItems: "center" }}>
<Text>
{object.item.total && object.item.total.toFixed(1)}
</Text>
</View>
<View style={{ flex: 1, alignItems: "center" }}>
<Text>
{object.item.claimed && object.item.claimed.toFixed(1)}
</Text>
</View>
<View style={{ flex: 1, alignItems: "center" }}>
<Text>
{object.item.adjustments &&
object.item.adjustments.toFixed(1)}
</Text>
</View>
<View style={{ flex: 1, alignItems: "center" }}>
<Text
style={{
color: object.item.difference >= 0 ? "green" : "red",
}}
>
{_.round(object.item.difference, 1)}
</Text>
</View>
</DataTable.Row>
)}
/>
{summary && (
<View style={localStyles.headerArea}>
<Text style={localStyles.footertext}>{t("laborallocations.labels.totals")}</Text>
<Text style={localStyles.footertext}>
{summary.hrs_total.toFixed(1)}
</Text>
<Text style={localStyles.footertext}>
{summary.hrs_claimed.toFixed(1)}
</Text>
<Text style={localStyles.footertext}>
{summary.adjustments.toFixed(1)}
</Text>
<Text style={localStyles.footertext}>
{summary.difference.toFixed(1)}
</Text>
</View>
)}
</DataTable>
</Card.Content>
</Card>
) : null}
{/* use "totals" for the rows in the table */}
{/* use "summary" for the totals at the bottom */}
</View>
);
}
const localStyles = StyleSheet.create({
headerArea: {
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
margin: 1,
},
footertext: {
flex: 1,
textAlign: "center",
textAlignVertical: "center",
margin: 1,
paddingBottom: 8,
},
headertext: {
flex: 1,
textAlign: "center",
textAlignVertical: "center",
margin: 1,
paddingBottom: 8,
},
headertextAdjusts: {
flex: 1,
textAlign: "center",
textAlignVertical: "center",
margin: 1,
paddingBottom: 8,
},
});
export default connect(mapStateToProps, null)(LaborAllocationsTable);

View File

@@ -0,0 +1,47 @@
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { store } from "../../redux/store";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(OwnerNameDisplay);
export function OwnerNameDisplay({ bodyshop, ownerObject }) {
const emptyTest =
ownerObject?.ownr_fn + ownerObject?.ownr_ln + ownerObject?.ownr_co_nm;
if (!emptyTest || emptyTest === "null" || emptyTest.trim() === "")
return "N/A";
if (bodyshop.last_name_first)
return `${ownerObject?.ownr_ln || ""}, ${ownerObject?.ownr_fn || ""} ${
ownerObject?.ownr_co_nm || ""
}`.trim();
return `${ownerObject?.ownr_fn || ""} ${ownerObject?.ownr_ln || ""} ${
ownerObject.ownr_co_nm || ""
}`.trim();
}
export function OwnerNameDisplayFunction(ownerObject, forceFirstLast = false) {
const emptyTest =
ownerObject?.ownr_fn + ownerObject?.ownr_ln + ownerObject?.ownr_co_nm;
if (!emptyTest || emptyTest === "null" || emptyTest.trim() === "")
return "N/A";
const rdxStore = store.getState();
if (rdxStore.user.bodyshop.last_name_first && !forceFirstLast)
return `${ownerObject?.ownr_ln || ""}, ${ownerObject?.ownr_fn || ""} ${
ownerObject?.ownr_co_nm || ""
}`.trim();
return `${ownerObject?.ownr_fn || ""} ${ownerObject?.ownr_ln || ""} ${
ownerObject?.ownr_co_nm || ""
}`.trim();
}

View File

@@ -0,0 +1,102 @@
import { Formik } from "formik";
import React from "react";
import { View, StyleSheet, Text } from "react-native";
import { Button, TextInput } from "react-native-paper";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { employeeSignInStart } from "../../redux/employee/employee.actions";
import { createStructuredSelector } from "reselect";
import {
selectCurrentEmployee,
selectSigningIn,
selectSignInError,
} from "../../redux/employee/employee.selectors";
import ErrorDisplay from "../error-display/error-display.component";
const mapStateToProps = createStructuredSelector({
currentEmployee: selectCurrentEmployee,
signingIn: selectSigningIn,
signingError: selectSignInError,
});
const mapDispatchToProps = (dispatch) => ({
employeeSignInStart: (employeeId, pin) =>
dispatch(employeeSignInStart({ employeeId, pin })),
});
export function EmployeeSignIn({
signingError,
signingIn,
employeeSignInStart,
}) {
const { t } = useTranslation();
const formSubmit = (values) => {
const { employeeId, pin } = values;
employeeSignInStart(employeeId, pin);
};
return (
<View style={localStyles.content}>
<View style={localStyles.signInContainer}>
<Formik
initialValues={{ employeeId: "", pin: "" }}
onSubmit={formSubmit}
>
{({ handleChange, handleBlur, handleSubmit, values }) => {
const signingErrorMsg = signingError ? (<ErrorDisplay errorMessage={signingError} />) : null;
return(
<View>
<TextInput
label={t("employeesignin.fields.employeeid")}
mode="outlined"
autoCapitalize="none"
keyboardType="default"
onChangeText={handleChange("employeeId")}
onBlur={handleBlur("employeeId")}
value={values.employeeId}
style={[localStyles.input]}
/>
<TextInput
label={t("employeesignin.fields.pin")}
mode="outlined"
secureTextEntry={true}
onChangeText={handleChange("pin")}
onBlur={handleBlur("pin")}
value={values.pin}
style={[localStyles.input]}
textContentType={"none"}
/>
{signingErrorMsg}
<Button
mode="outlined"
loading={signingIn}
onPress={handleSubmit}
>
<Text>{t("employeesignin.actions.employeesignin")}</Text>
</Button>
</View>
)
}}
</Formik>
</View>
</View>
);
}
const localStyles = StyleSheet.create({
content: {
display: "flex",
flex: 1,
},
signInContainer: {
flex: 1,
},
input: {
margin: 12,
},
});
export default connect(mapStateToProps, mapDispatchToProps)(EmployeeSignIn);

View File

@@ -1,14 +1,17 @@
import { Ionicons } from "@expo/vector-icons";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import React, { useEffect, useRef } from "react";
import i18n from "i18next";
import moment from "moment";
import React, { useEffect } from "react";
import { AppState } from "react-native";
import { Button } from "react-native-paper";
import { Ionicons } from "@expo/vector-icons";
import { NavigationContainer } from "@react-navigation/native";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { logImEXEvent } from "../../firebase/firebase.analytics";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.analytics";
import { setCameraJob, setCameraJobId } from "../../redux/app/app.actions";
import {
checkUserSession,
@@ -19,6 +22,9 @@ import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { employeeSignOut } from "../../redux/employee/employee.actions";
import { selectCurrentEmployee } from "../../redux/employee/employee.selectors";
import ScreenJobDetail from "../screen-job-detail/screen-job-detail.component";
import ScreenJobList from "../screen-job-list/screen-job-list.component";
import ScreenMediaBrowser from "../screen-media-browser/screen-media-browser.component";
@@ -26,16 +32,23 @@ import ScreenSettingsComponent from "../screen-settings/screen-settings.componen
import ScreenSignIn from "../screen-sign-in/screen-sign-in.component";
import ScreenSplash from "../screen-splash/screen-splash.component";
import EmployeeSignIn from "../screen-employee-sign-in/screen-employee-sign-in.component";
import ScreenTimeTicketBrowser from "../screen-time-ticket-browser/screen-time-ticket-browser.component";
import SignOutButton from "../Buttons/employee-sign-out-button.component";
import AddTimeTicketButton from "../Buttons/create-time-ticket-button.component";
import ScreenTimeTicketCreate from "../time-ticket/screen-time-ticket-create.component";
import ScreenTimeTicketClockoffComponent from "../time-ticket/screen-time-ticket-clockoff.component";
const ActiveJobStack = createNativeStackNavigator();
const MoreStack = createNativeStackNavigator();
const BottomTabs = createBottomTabNavigator();
const MediaBrowserStack = createNativeStackNavigator();
const TimeTicketBrowserStack = createNativeStackNavigator();
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
checkUserSession: () => dispatch(checkUserSession()),
emailSignInStart: (email, password) =>
@@ -44,6 +57,12 @@ const mapDispatchToProps = (dispatch) => ({
setCameraJobId: (id) => dispatch(setCameraJobId(id)),
setCameraJob: (job) => dispatch(setCameraJob(job)),
});
const mapStateToProps2 = createStructuredSelector({
currentEmployee: selectCurrentEmployee,
});
const mapDispatchToProps2 = (dispatch) => ({
signOut: () => dispatch(employeeSignOut()),
});
const JobsTabNavigator = connect(
mapStateToProps,
@@ -104,6 +123,74 @@ const MoreStackNavigator = () => (
</MoreStack.Navigator>
);
const TimeTicketBrowserStackNavigator = connect(
mapStateToProps2,
mapDispatchToProps2
)(({ currentEmployee, signOut }) => {
const appState = useRef(AppState.currentState);
useEffect(() => {
const subscription = AppState.addEventListener("change", (nextAppState) => {
if (
appState.current.match(/active|inactive/) &&
nextAppState === "background"
) {
//App is about to be background
signOut();
}
// if ( appState.current.match(/inactive/)) { console.log("App has come to the inactive"); }
// if (appState.current.match(/background/)) { console.log("App has come to the background");}
appState.current = nextAppState;
});
return () => {
subscription.remove();
};
}, []);
return (
<TimeTicketBrowserStack.Navigator>
{currentEmployee === null ? (
<TimeTicketBrowserStack.Screen
name="EmployeeSignIn"
options={() => ({ title: i18n.t("employeesignin.titles.signin") })}
component={EmployeeSignIn}
/>
) : currentEmployee.authorized ? (
<>
<TimeTicketBrowserStack.Screen
name="TimeTicketBrowser"
options={{
title: i18n.t("timeticketbrowser.titles.timeticketbrowsertab"),
}}
component={ScreenTimeTicketBrowser}
/>
<TimeTicketBrowserStack.Screen
name="CreateTimeTicket"
options={() => ({
title: i18n.t("createtimeticket.titles.createtimeticket"),
})}
component={ScreenTimeTicketCreate}
/>
<TimeTicketBrowserStack.Screen
name="TimeTicketClockOff"
options={() => ({
title: i18n.t("timeticketclockoff.titles.clockoff"),
})}
component={ScreenTimeTicketClockoffComponent}
/>
</>
) : (
<TimeTicketBrowserStack.Screen
name="EmployeeSignIn"
options={() => ({
title: i18n.t("employeesignin.titles.signin"),
})}
component={EmployeeSignIn}
/>
)}
</TimeTicketBrowserStack.Navigator>
);
});
const BottomTabsNavigator = () => (
<BottomTabs.Navigator
screenOptions={({ route }) => ({
@@ -116,6 +203,8 @@ const BottomTabsNavigator = () => (
iconName = "ios-settings";
} else if (route.name === "MediaBrowserTab") {
iconName = "ios-camera";
} else if (route.name === "TimeTicketBrowserTab") {
iconName = "ios-stopwatch-outline";
} else {
//iconName = "customerservice";
}
@@ -140,6 +229,14 @@ const BottomTabsNavigator = () => (
}}
component={MediaBrowserStackNavigator}
/>
<BottomTabs.Screen
name="TimeTicketBrowserTab"
options={{
title: i18n.t("timeticketbrowser.titles.timeticketbrowsertab"),
headerShown: false,
}}
component={TimeTicketBrowserStackNavigator}
/>
<BottomTabs.Screen
name="MoreTab"
options={{ title: i18n.t("more.titles.moretab"), headerShown: false }}

View File

@@ -0,0 +1,330 @@
import React, { useCallback, useState, useRef } from "react";
import moment from "moment";
import {
View,
Text,
StyleSheet,
ScrollView,
RefreshControl,
FlatList,
} from "react-native";
import {
ActivityIndicator,
Button,
Card,
Headline,
Subheading,
} from "react-native-paper";
import styles from "../styles";
import axios from "axios";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { employeeGetRatesStart } from "../../redux/employee/employee.actions";
import {
selectCurrentEmployee,
selectRates,
selectGettingRates,
selectSignInError,
selectEmployeeFullName,
} from "../../redux/employee/employee.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CostCenterSelect from "../Selects/select-cost-center";
import ErrorDisplay from "../error-display/error-display.component";
import {
selectCurrentTimeTicketJob,
selectCurrentTimeTicketJobId,
} from "../../redux/timetickets/timetickets.selectors";
import { INSERT_NEW_TIME_TICKET } from "../../graphql/timetickets.queries";
import { useMutation, useQuery } from "@apollo/client";
import { QUERY_ACTIVE_TIME_TICKETS } from "../../graphql/timetickets.queries";
// import EmployeeClockedInList from "../time-ticket-lists/employee-clockedin-list.component";
import { useTranslation } from "react-i18next";
import ClockedinListItem from "../time-ticket-items/clockedin-list-item.component";
import SignOutButton from "../Buttons/employee-sign-out-button.component";
import AddTimeTicketButton from "../Buttons/create-time-ticket-button.component";
import KeyboardAvoidingComponent from "../keyboards/KeyboardAvoidingComponent";
import JobSearchAndSelectModal from "../Modals/JobSearchAndSelectModal";
import { useNavigation } from "@react-navigation/native";
const mapStateToProps = createStructuredSelector({
currentEmployee: selectCurrentEmployee,
loaderGettingRates: selectGettingRates,
signingError: selectSignInError,
currentBodyshop: selectBodyshop,
currentRatesNCostCenters: selectRates,
currentSelectedTimeTicketJobId: selectCurrentTimeTicketJobId,
currentSelectedTimeTicketJob: selectCurrentTimeTicketJob,
currentEmployeeFullName: selectEmployeeFullName,
});
const mapDispatchToProps = (dispatch) => ({
employeeGetRatesStart: (employeeId) =>
dispatch(employeeGetRatesStart(employeeId)),
});
export function ScreenTimeTicketBrowser({
loaderGettingRates,
currentEmployee,
employeeGetRatesStart,
signingError,
currentBodyshop,
currentRatesNCostCenters,
currentSelectedTimeTicketJob,
currentSelectedTimeTicketJobId,
currentEmployeeFullName,
}) {
const { t } = useTranslation();
const [currentSCC, setCurrentSCC] = useState(null);
const [loadingClockIn, setLoadingClockIn] = useState(false);
const [error, setError] = useState(null);
const [refreshing, setRefreshing] = useState(false);
const [insertTimeTicket] = useMutation(INSERT_NEW_TIME_TICKET, {
refetchQueries: ["QUERY_ACTIVE_TIME_TICKETS"],
});
const navigation = useNavigation();
const [curSelClockIntoJob, setCurSelClockIntoJob] = useState(null);
const handleFinish = async (values) => {
setLoadingClockIn(true);
setError(null);
const theTime = (await axios.post("/utils/time")).data;
if (!!currentSCC?.value && !!curSelClockIntoJob?.id) {
setError(null);
// console.log("have all values");
} else {
// console.log("missing values!");
setLoadingClockIn(false);
setError({ message: t("timeticketbrowser.errors.missingvalues") });
return;
}
const tempVariablesObj = {
variables: {
timeTicketInput: [
{
bodyshopid: currentBodyshop.id,
employeeid: currentEmployee?.technician?.id,
date: moment(theTime).format("YYYY-MM-DD"),
clockon: moment(theTime),
jobid: curSelClockIntoJob?.id,
cost_center: currentSCC?.value,
ciecacode:
currentBodyshop?.cdk_dealerid || currentBodyshop?.pbs_serialnumber
? currentSCC?.value
: Object.keys(
currentBodyshop.md_responsibility_centers.defaults.costs
).find((key) => {
return (
currentBodyshop.md_responsibility_centers.defaults.costs[
key
] === currentSCC?.value
);
}),
},
],
},
};
const result = await insertTimeTicket(tempVariablesObj);
setLoadingClockIn(false);
if (!!result.errors) {
setError(JSON.stringify(result.errors));
} else {
setCurSelClockIntoJob(null);
setCurrentSCC(null);
}
};
const onRefresh = useCallback(() => {
setRefreshing(true);
refetch();
setTimeout(() => {
setRefreshing(false);
}, 500);
}, []);
const [itemState, setItemState] = useState({
title: t("employeeclockedinlist.labels.alreadyclockedon"),
data: null,
content: null,
});
const { loadingATT, errorATT, dataATT, refetch } = useQuery(
QUERY_ACTIVE_TIME_TICKETS,
{
variables: {
employeeId: currentEmployee?.technician?.id,
},
skip: !currentEmployee?.technician,
onCompleted: (dataATT) => {
if (!!dataATT && dataATT?.timetickets) {
setItemState((itemState) => ({
...itemState,
data: dataATT?.timetickets,
}));
}
},
onRefresh: { onRefresh },
}
);
if (loadingATT) {
setItemState((itemState) => ({
...itemState,
content: <ActivityIndicator color="dodgerblue" size="large" />,
}));
return;
}
if (errorATT) {
setItemState((itemState) => ({
...itemState,
content: <ErrorDisplay errorMessage={errorATT.message} />,
}));
return;
}
return (
<View style={styles.cardBackground}>
<KeyboardAvoidingComponent>
<FlatList
ListHeaderComponent={
<Card style={localStyles.localCardStyle}>
<Card.Title
title={t("timeticketbrowser.labels.loggedinemployee")}
right={(props) => <SignOutButton style={{margin:8}}/>}
/>
<Card.Content>
{currentEmployeeFullName && (
<Subheading>{currentEmployeeFullName}</Subheading>
)}
</Card.Content>
</Card>
}
data={!!itemState ? [itemState] : null}
renderItem={({ item }) => (
<MyItem style={localStyles.localCardStyle} itemState={item} />
)}
ListFooterComponent={
<Card style={localStyles.localCardStyle}>
<Card.Title
title={t("timeticketbrowser.labels.clockintojob")}
right={(props) => (
<View style={{ flexDirection: "row" }}>
<Button
mode="text"
compact={true}
onPress={() => {
navigation.navigate("CreateTimeTicket");
}}
icon="plus"
style={{ margin: 8 }}
>
<Text >
{t("timeticketbrowser.actions.ticket")}
</Text>
</Button>
{/* <Button
mode="outlined"
compact={true}
loading={loadingClockIn}
onPress={handleFinish}
style={{ margin: 4 }}
>
<Text>Clock In</Text>
</Button> */}
</View>
)}
/>
<Card.Content>
<JobSearchAndSelectModal
currentValue={curSelClockIntoJob}
onSetCurrentValue={setCurSelClockIntoJob}
notExported={!currentBodyshop.tt_allow_post_to_invoiced}
notInvoiced={!currentBodyshop.tt_allow_post_to_invoiced}
convertedOnly={!currentBodyshop.tt_allow_post_to_invoiced}
/>
<CostCenterSelect
currentValue={currentSCC}
currentRatesNCostCenters={currentRatesNCostCenters}
onValueSelected={setCurrentSCC}
/>
{error && error?.message ? (
<ErrorDisplay errorMessage={error.message} />
) : null}
</Card.Content>
<Card.Actions
style={{
justifyContent: "center",
}}
>
<Button
mode="outlined"
style={styles.buttonBasicOutlined}
loading={loadingClockIn}
onPress={handleFinish}
>
<Text>{t("timeticketbrowser.actions.clockin")}</Text>
</Button>
</Card.Actions>
</Card>
}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
ListEmptyComponent={
<View style={styles.containerNoData}>
<Text>{t("timeticketbrowser.labels.nodata")}</Text>
</View>
}
/>
</KeyboardAvoidingComponent>
</View>
);
}
const MyItem = ({ itemState, style }) => {
const { t } = useTranslation();
const items = itemState?.data;
return (
<Card key={itemState.title} style={style}>
<Card.Title title={itemState.title} />
<Card.Content>
{!!items ? (
items.map((item) => <ClockedinListItem key={item.id} ticket={item} />)
) : !!itemState?.content ? (
itemState.content
) : (
<Text>{t("timeticketbrowser.labels.nodata")}</Text>
)}
</Card.Content>
</Card>
);
};
const localStyles = StyleSheet.create({
content: {
display: "flex",
flex: 2,
},
localCardStyle: {
margin: 4,
},
containerNoData: {
flex: 1,
padding: 10,
alignItems: "center",
justifyContent: "center",
},
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ScreenTimeTicketBrowser);

View File

@@ -4,20 +4,20 @@ import { Button } from "react-native";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { signOutStart } from "../../redux/user/user.actions";
import { employeeSignOut } from "../../redux/employee/employee.actions";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
signOutStart: () => dispatch(signOutStart()),
signOut: () => dispatch(employeeSignOut()),
});
export function SignOutButton({ signOutStart }) {
export function SignOutButton({ signOutStart, signOut }) {
const { t } = useTranslation();
return (
<Button
style={{ margin: 8 }}
onPress={() => signOutStart()}
onPress={() => (signOut(),signOutStart())}
title={t("general.actions.signout")}
/>
);

View File

@@ -9,4 +9,23 @@ export default StyleSheet.create({
display: "flex",
flex: 1,
},
buttonBasicOutlined: {
marginVertical: 4,
marginHorizontal: 16,
justifyContent: "center",
alignContent: "center",
borderColor: "gray",
borderWidth: 0.8,
borderRadius: 4,
},
buttonFatOutlined: {
marginVertical: 4,
marginHorizontal: 16,
height: 48,
justifyContent: "center",
alignContent: "center",
borderColor: "gray",
borderWidth: 0.8,
borderRadius: 4,
}
});

View File

@@ -0,0 +1,60 @@
import { useNavigation } from "@react-navigation/native";
import React from "react";
import { useTranslation } from "react-i18next";
import { Button, Card, Text } from "react-native-paper";
import { connect } from "react-redux";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import { DateTimeFormatter } from "../../util/DateFormater";
import { setTmTicketJobId } from "../../redux/app/app.actions";
import styles from "../styles";
const mapDispatchToProps = (dispatch) => ({
setTmTicketJobIdRedux: (jobId) => dispatch(setTmTicketJobId(jobId)),
});
export function ClockedinListItem({ setTmTicketJobIdRedux, ticket }) {
const { t } = useTranslation();
const navigation = useNavigation();
const makeNavToTimeTicketClockOff = () => (
setTmTicketJobIdRedux(ticket.job.id),
navigation.navigate("TimeTicketClockOff", {
timeTicketId:ticket.id,
})
);
return (
<Card style={{ margin: 8 }}>
<Card.Title
title={`${
ticket.job.ro_number || t("general.labels.na")
} ${OwnerNameDisplayFunction(ticket.job)}`}
/>
<Card.Content>
<Text>
{t("clockedinlistitem.labels.vehicle")}
{`${ticket.job.v_model_yr || ""} ${ticket.job.v_make_desc || ""} ${
ticket.job.v_model_desc || ""
}`}
</Text>
<Text>
{t("clockedinlistitem.labels.clockedin")}<DateTimeFormatter>{ticket.clockon}</DateTimeFormatter>
</Text>
<Text>
{t("clockedinlistitem.labels.costcenter")}
{ticket.cost_center === "timetickets.labels.shift"
? t(ticket.cost_center)
: ticket.cost_center}
</Text>
</Card.Content>
<Card.Actions style={{
justifyContent: 'center'}}>
<Button mode="outlined" style={styles.buttonBasicOutlined} onPress={makeNavToTimeTicketClockOff} >
{t("clockedinlistitem.actions.clockout")}
</Button>
</Card.Actions>
</Card>
);
}
export default connect(null, mapDispatchToProps)(ClockedinListItem);

View File

@@ -0,0 +1,151 @@
import { connect } from "react-redux";
import { selectCurrentEmployee } from "../../redux/employee/employee.selectors";
import { QUERY_ACTIVE_TIME_TICKETS } from "../../graphql/timetickets.queries";
import { ActivityIndicator } from "react-native-paper";
import ErrorDisplay from "../error-display/error-display.component";
import { View, Text, FlatList, RefreshControl } from "react-native";
import { useMutation, useQuery } from "@apollo/client";
import { createStructuredSelector } from "reselect";
import { useTranslation } from "react-i18next";
import ClockedinListItem from "../time-ticket-items/clockedin-list-item.component";
import { useEffect, useState } from "react";
// import { setTmTicketJobId } from "../../redux/app/app.actions";
const mapStateToProps = createStructuredSelector({
//technician: selectTechnician,
currentEmployee: selectCurrentEmployee,
// updateJobs: selectUpdateJobs
});
// const mapDispatchToProps = (dispatch) => ({
// setTmTicketJobId: (jobId) => dispatch(setTmTicketJobId(jobId)),
// });
export function EmployeeClockedInList({ currentEmployee, isRefresh }) {
const [refreshKey, setRefreshKey] = useState(isRefresh);
const [jobData, setJobData] = useState(data);
// console.info("EmployeeClockedInList, QUERY_ACTIVE_TIME_TICKETS called.",currentEmployee);
// console.info(
// "EmployeeClockedInList, isRefresh :",
// isRefresh
// );
const { t } = useTranslation();
useEffect(() => {
// async function fetchData(){
// // const result = await getUsers();
// // setData(result);
// console.log("teste: ", result);
// }
// fetchData();
if(isRefresh){
refetch();
}
// console.log("useEffect: ", isRefresh);
// setRefreshKey(isRefresh);
}, [isRefresh]);
const { loading, error, data, refetch } = useQuery(
QUERY_ACTIVE_TIME_TICKETS,
{
variables: {
employeeId: currentEmployee?.technician?.id,
},
skip: !currentEmployee?.technician,
onCompleted:setJobData,
onRefresh:{onRefresh}
}
);
if (loading) return <ActivityIndicator color="dodgerblue" size="large" />;
if (error) return <ErrorDisplay errorMessage={error.message} />;
const onRefresh = async () => {
// console.info("EmployeeClockedInList, onRefresh.");
return refetch();
};
return (
<View style={{ flex: 1, flexGrow: 1 }}>
{jobData ? (
<View style={{ flex: 1 }}>
<Text style={{ paddingLeft: 12, paddingTop: 14 }}>
{t("employeeclockedinlist.labels.alreadyclockedon")}
</Text>
<FlatList
data={jobData?.timetickets}
refreshControl={
<RefreshControl refreshing={loading} onRefresh={onRefresh} />
}
renderItem={(object) => (
<ClockedinListItem
ticket={object.item}
// handleRefresh={onRefresh}
/>
)}
// setTmTicketJobId={setTmTicketJobId}
/>
</View>
) : null}
</View>
// <div>
// {data.timetickets.length > 0 ? (
// <div>
// <Typography.Title level={2}>
// {t("timetickets.labels.alreadyclockedon")}
// </Typography.Title>
// <List
// grid={{
// gutter: 32,
// xs: 1,
// sm: 2,
// md: 3,
// lg: 4,
// xl: 5,
// xxl: 6,
// }}
// dataSource={data.timetickets || []}
// renderItem={(ticket) => (
// <List.Item>
// <Card
// title={
// <Link to={`/tech/joblookup?selected=${ticket.job.id}`}>
// {`${
// ticket.job.ro_number || t("general.labels.na")
// } ${OwnerNameDisplayFunction(ticket.job)}`}
// </Link>
// }
// actions={[
// <TechClockOffButton
// jobId={ticket.jobid}
// timeTicketId={ticket.id}
// completedCallback={refetch}
// />,
// ]}
// >
// <div>
// {`
// ${ticket.job.v_model_yr || ""} ${
// ticket.job.v_make_desc || ""
// } ${ticket.job.v_model_desc || ""}`}
// </div>
// <DataLabel label={t("timetickets.fields.clockon")}>
// <DateTimeFormatter>{ticket.clockon}</DateTimeFormatter>
// </DataLabel>
// <DataLabel label={t("timetickets.fields.cost_center")}>
// {ticket.cost_center === "timetickets.labels.shift"
// ? t(ticket.cost_center)
// : ticket.cost_center}
// </DataLabel>
// </Card>
// </List.Item>
// )}
// ></List>
// </div>
// ) : null}
// </div>
);
}
export default connect(mapStateToProps, null)(EmployeeClockedInList);

View File

@@ -0,0 +1,266 @@
import { Formik } from "formik";
import React, { useState, useRef, useCallback } from "react";
import { StyleSheet, Text, View, ScrollView, FlatList } from "react-native";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
Button,
TextInput,
Card,
} from "react-native-paper";
import CostCenterSelect from "../Selects/select-cost-center";
import {
selectCurrentEmployee,
selectRates,
} from "../../redux/employee/employee.selectors";
import {
selectBodyshop,
selectRestrictClaimableHoursFlag,
} from "../../redux/user/user.selectors";
import LaborAllocationsTable from "../labor-allocations-table/labor-allocations-table.component";
import { UPDATE_TIME_TICKET } from "../../graphql/timetickets.queries";
import { useMutation } from "@apollo/client";
import { selectCurrentTmTicketJobId } from "../../redux/app/app.selectors";
import ErrorDisplay from "../error-display/error-display.component";
import { timeTicketClockOutStart } from "../../redux/timetickets/timetickets.actions";
import axios from "axios";
import { useNavigation } from "@react-navigation/native";
import styles from "../styles";
import { RefreshControl } from "react-native";
const mapStateToProps = createStructuredSelector({
currentEmployee: selectCurrentEmployee,
currentRatesNCostCenters: selectRates,
currentBodyshop: selectBodyshop,
currentTmTicketJobId: selectCurrentTmTicketJobId,
currentRestrictClaimableHoursFlag: selectRestrictClaimableHoursFlag,
});
const mapDispatchToProps = (dispatch) => ({
timeTicketClockOutStart,
});
export function TimeTicketClockOff({
currentEmployee,
currentRatesNCostCenters,
currentBodyshop,
currentTmTicketJobId,
currentRestrictClaimableHoursFlag,
route,
}) {
const costCenterDiff = useRef(0);
const setCostCenterDiff = (value) => {
countRef.current = val;
};
const navigation = useNavigation();
const { timeTicketId } = route.params;
const { t } = useTranslation();
const [loadingClockOut, setLoadingClockOut] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [currentSCC, setCurrentSCC] = useState(null);
const [updateTimeticket] = useMutation(UPDATE_TIME_TICKET, {
refetchQueries: ["QUERY_ACTIVE_TIME_TICKETS"],
});
const handleFinish = async (values) => {
if (
!!values.actualhours &&
!!values.productivehours &&
!!currentSCC?.value
) {
if (isNaN(values.actualhours) | isNaN(values.productivehours)) {
setLoadingClockOut(false);
setError({ message: t("timeticketclockoff.errors.nan") });
return;
}
setError(null);
} else {
setLoadingClockOut(false);
setError({ message: t("timeticketclockoff.errors.missingvalues") });
return;
}
if (!!currentRestrictClaimableHoursFlag) {
if (values.productivehours > costCenterDiff.current) {
setLoadingClockOut(false);
setError({
message: t("timeticketclockoff.errors.hoursenteredmorethanavailable"),
});
return;
}
}
const tempcallobj = {
variables: {
timeticketId: timeTicketId,
timeticket: {
actualhrs: values?.actualhours,
ciecacode:
currentBodyshop?.cdk_dealerid || currentBodyshop?.pbs_serialnumber
? currentSCC?.value
: Object.keys(
currentBodyshop.md_responsibility_centers.defaults.costs
).find((key) => {
return (
currentBodyshop.md_responsibility_centers.defaults.costs[
key
] === currentSCC?.value
);
}),
clockoff: (await axios.post("/utils/time")).data,
cost_center: currentSCC?.value,
flat_rate:
currentEmployee &&
currentEmployee.technician &&
currentEmployee.technician?.flat_rate,
productivehrs: values?.productivehours,
rate:
currentRatesNCostCenters &&
currentSCC?.value &&
currentRatesNCostCenters.filter(
(r) => r.cost_center === currentSCC?.value
)[0]?.rate,
},
},
};
setLoadingClockOut(true);
const result = await updateTimeticket(tempcallobj);
setLoadingClockOut(false);
if (!!result.errors) {
setError(JSON.stringify(result.errors));
} else {
navigation.goBack();
}
};
const onRefresh = useCallback(() => {
setLoading(true);
// refetch();
setTimeout(() => {
setLoading(false);
}, 500);
}, []);
return (
<View style={styles.cardBackground}>
<FlatList
ListHeaderComponent={
<Card style={localStyles.localCardStyle}>
<Card.Content>
<Formik
initialValues={{
costcenter: { currentSCC },
productivehours: "",
actualhours: "",
}}
onSubmit={handleFinish}
>
{({ handleChange, handleBlur, handleSubmit, values }) => (
<View style={localStyles.topTimeTicketContainer}>
<TextInput
style={localStyles.inputStyle}
mode="outlined"
onChangeText={handleChange("actualhours")}
onBlur={handleBlur("actualhours")}
value={values.actualhours}
label={t(
"timeticketclockoff.labels.actualhoursplaceholder"
)}
keyboardType="numeric"
/>
<TextInput
style={localStyles.inputStyle}
mode="outlined"
onChangeText={handleChange("productivehours")}
onBlur={handleBlur("productivehours")}
value={values.productivehours}
label={t(
"timeticketclockoff.labels.productivehoursplaceholder"
)}
keyboardType="numeric"
/>
<CostCenterSelect
currentRatesNCostCenters={currentRatesNCostCenters}
currentValue={currentSCC}
onValueSelected={setCurrentSCC}
/>
{error ? (
<ErrorDisplay errorMessage={error.message} />
) : null}
<View style={{ flexDirection: "row",justifyContent: "center", paddingTop:8 }}>
<Button
mode="outlined"
style={styles.buttonBasicOutlined}
onPress={handleSubmit}
title={t("timeticketclockoff.actions.clockoff")}
loading={loadingClockOut}
>
<Text style={{ fontSize: 12 }}>
{t("timeticketclockoff.actions.clockoff")}
</Text>
</Button>
</View>
</View>
)}
</Formik>
</Card.Content>
</Card>
}
data={null}
renderItem={null}
ListFooterComponent={
<LaborAllocationsTable
jobId={currentTmTicketJobId}
costCenterDiff={costCenterDiff}
selectedCostCenter={currentSCC}
style={localStyles.localCardStyle}
shouldRefresh={loading}
/>
}
refreshControl={
<RefreshControl refreshing={loading} onRefresh={onRefresh} />
}
/>
</View>
);
}
export default connect(mapStateToProps, null)(TimeTicketClockOff);
const localStyles = StyleSheet.create({
content: {
display: "flex",
flex: 1,
},
topContainer: {},
bottomContainer: {},
input: {},
dateButton: {
marginVertical: 4,
marginHorizontal: 16,
height: 48,
justifyContent: "center",
alignContent: "center",
borderColor: "blue",
borderWidth: 0.8,
flex: 1,
},
textForButton: {
flex: 1,
justifyContent: "center",
alignContent: "center",
},
inputStyle: {
marginVertical: 4,
marginHorizontal: 16,
height: 48,
fontSize: 16,
},
localCardStyle: {
margin: 4,
},
});

View File

@@ -0,0 +1,327 @@
import { Formik } from "formik";
import React, { useRef, useState,useCallback } from "react";
import { StyleSheet, Text, View, ScrollView, RefreshControl,FlatList } from "react-native";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { Button, TextInput, Card } from "react-native-paper";
import CostCenterSelect from "../Selects/select-cost-center";
import DateTimePickerModal from "react-native-modal-datetime-picker";
import {
selectCurrentEmployee,
selectRates,
selectEmployeeFullName,
} from "../../redux/employee/employee.selectors";
import {
selectBodyshop,
selectRestrictClaimableHoursFlag,
} from "../../redux/user/user.selectors";
import LaborAllocationsTable from "../labor-allocations-table/labor-allocations-table.component";
import ErrorDisplay from "../error-display/error-display.component";
import { INSERT_NEW_TIME_TICKET } from "../../graphql/timetickets.queries";
import { useMutation } from "@apollo/client";
import moment from "moment";
import { useNavigation } from "@react-navigation/native";
import styles from "../styles";
import JobSearchAndSelectModal from "../Modals/JobSearchAndSelectModal";
const mapStateToProps = createStructuredSelector({
currentEmployee: selectCurrentEmployee,
currentRatesNCostCenters: selectRates,
currentBodyshop: selectBodyshop,
currentEmployeeFullName: selectEmployeeFullName,
currentRestrictClaimableHoursFlag: selectRestrictClaimableHoursFlag,
});
export function TimeTicketCreate({
currentEmployee,
currentRatesNCostCenters,
currentBodyshop,
currentEmployeeFullName,
currentRestrictClaimableHoursFlag,
}) {
const costCenterDiff = useRef(0);
const navigation = useNavigation();
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [loadingCreate, setLoadingCreate] = useState(false);
const [error, setError] = useState(null);
const [isDatePickerVisible, setDatePickerVisibility] = useState(false);
const [date2, setDate2] = useState(new Date());
const [currentSCC, setCurrentSCC] = useState(null);
const [curSelClockIntoJob,setCurSelClockIntoJob] = useState(null);
const showDatePicker = () => {
setDatePickerVisibility(true);
};
const hideDatePicker = () => {
setDatePickerVisibility(false);
};
const handleConfirm = (date) => {
setDate2(date);
hideDatePicker();
};
const [insertTicket] = useMutation(INSERT_NEW_TIME_TICKET);
const handleFinish = async (values) => {
setError(null);
setLoadingCreate(true);
if (
!!currentSCC?.value &&
!!curSelClockIntoJob?.id &&
!!values.productivehours &&
!!currentBodyshop.id &&
!!currentEmployee?.technician?.id &&
!!date2 &&
!!values.actualhours
) {
if (isNaN(values.actualhours) | isNaN(values.productivehours)) {
// console.log("actual hours is NAN!");
setLoadingCreate(false);
setError({ message: t("createtimeticket.errors.nan") });
return;
}
setError(null);
// console.log("have all values");
} else {
// console.log("missing values!");
setLoadingCreate(false);
setError({ message: t("createtimeticket.errors.missingvalues") });
return;
}
if (!!currentRestrictClaimableHoursFlag) {
if (values.productivehours > costCenterDiff.current) {
setLoadingCreate(false);
setError({
message: t("timeticketclockoff.errors.hoursenteredmorethanavailable"),
});
return;
}
}
const tempVariablesObj = {
variables: {
timeTicketInput: [
{
actualhrs: values?.actualhours ? values?.actualhours : null,
bodyshopid: currentBodyshop.id,
ciecacode:
currentBodyshop?.cdk_dealerid || currentBodyshop?.pbs_serialnumber
? currentSCC?.value
: Object.keys(
currentBodyshop.md_responsibility_centers.defaults.costs
).find((key) => {
return (
currentBodyshop.md_responsibility_centers.defaults.costs[
key
] === currentSCC?.value
);
}),
cost_center: currentSCC?.value,
date: moment(date2).format("YYYY-MM-DD"),
employeeid: currentEmployee?.technician?.id,
flat_rate:
currentEmployee &&
currentEmployee.technician &&
currentEmployee.technician?.flat_rate,
jobid: curSelClockIntoJob?.id,
productivehrs: values?.productivehours,
rate:
currentRatesNCostCenters && currentSCC?.value
? currentRatesNCostCenters.filter(
(r) => r.cost_center === currentSCC?.value
)[0].rate
: null,
},
],
},
};
const result = await insertTicket(tempVariablesObj);
setLoadingCreate(false);
if (!!result.errors) {
// console.log("insertTicket, result.error :", result.errors);
setError(JSON.stringify(result.errors));
} else {
// console.log("insertTicket, result. :", result.data);
navigation.goBack();
}
};
const onRefresh = useCallback(() => {
setLoading(true);
// refetch();
setTimeout(() => {
setLoading(false);
}, 500);
}, []);
return (
<View style={styles.cardBackground}>
<FlatList
ListHeaderComponent={
<Card style={localStyles.localCardStyle}>
<Card.Content>
<Formik
initialValues={{
ticketdate: date2.toLocaleDateString(),
employee: { currentEmployeeFullName },
costcenter: { currentSCC },
productivehours: "",
actualhours: "",
}}
onSubmit={handleFinish}
>
{({ handleChange, handleBlur, handleSubmit, values }) => (
<View style={localStyles.topTimeTicketContainer}>
<JobSearchAndSelectModal
currentValue={curSelClockIntoJob}
onSetCurrentValue={setCurSelClockIntoJob}
notExported={!currentBodyshop.tt_allow_post_to_invoiced}
notInvoiced={!currentBodyshop.tt_allow_post_to_invoiced}
convertedOnly={true}
/>
<Button
mode="outlined"
title="TicketDatePickerButton"
onPress={showDatePicker}
style={localStyles.dateButton}
>
<Text style={localStyles.textForButton}>
{t("createtimeticket.actions.ticketdate")}
{date2.toLocaleDateString("en-US", {
day: "2-digit",
month: "2-digit",
year: "numeric",
})}
</Text>
</Button>
<DateTimePickerModal
isVisible={isDatePickerVisible}
mode="date"
date={date2}
onConfirm={handleConfirm}
onCancel={hideDatePicker}
/>
<TextInput
style={localStyles.inputStyle}
mode="outlined"
disabled={true}
value={currentEmployeeFullName}
label={t("createtimeticket.labels.employeeplaceholder")}
/>
<CostCenterSelect
currentRatesNCostCenters={currentRatesNCostCenters}
currentValue={currentSCC}
onValueSelected={setCurrentSCC}
/>
<TextInput
style={localStyles.inputStyle}
mode="outlined"
onChangeText={handleChange("productivehours")}
onBlur={handleBlur("productivehours")}
value={values.productivehours}
label={t(
"createtimeticket.labels.productivehoursplaceholder"
)}
keyboardType="numeric"
/>
<TextInput
style={localStyles.inputStyle}
mode="outlined"
onChangeText={handleChange("actualhours")}
onBlur={handleBlur("actualhours")}
value={values.actualhours}
label={t(
"createtimeticket.labels.actualhoursplaceholder"
)}
keyboardType="numeric"
/>
{error ? (
<ErrorDisplay errorMessage={error.message} />
) : null}
<View style={{ flexDirection: "row",justifyContent: "center", paddingTop:8 }}>
<Button
mode="outlined"
style={styles.buttonBasicOutlined}
onPress={handleSubmit}
loading={loadingCreate}
title="Submit"
>
<Text style={{ fontSize: 12 }}>
{t("createtimeticket.actions.createticket")}
</Text>
</Button>
</View>
</View>
)}
</Formik>
</Card.Content>
</Card>
}
data={null}
renderItem={null}
ListFooterComponent={
<LaborAllocationsTable
jobId={curSelClockIntoJob?.id}
costCenterDiff={costCenterDiff}
selectedCostCenter={currentSCC}
style={localStyles.localCardStyle}
shouldRefresh={loading}
/>
}
refreshControl={
<RefreshControl refreshing={loading} onRefresh={onRefresh} />
}
/>
</View>
);
}
export default connect(mapStateToProps, null)(TimeTicketCreate);
const localStyles = StyleSheet.create({
content: {
display: "flex",
flex: 1,
},
topContainer: {},
bottomContainer: {},
input: {
marginVertical: 4,
marginHorizontal: 16,
height: 48,
justifyContent: "center",
alignContent: "center",
borderWidth: 0.8,
},
dateButton: {
marginVertical: 4,
marginHorizontal: 16,
height: 48,
justifyContent: "center",
alignContent: "center",
borderColor: "gray",
borderWidth: 0.8,
},
textForButton: {
flex: 1,
justifyContent: "center",
alignContent: "center",
},
inputStyle: {
marginVertical: 4,
marginHorizontal: 16,
height: 48,
fontSize: 16,
},
localCardStyle: {
margin: 4,
},
});

View File

@@ -5,12 +5,17 @@ export const QUERY_BODYSHOP = gql`
bodyshops(where: { associations: { active: { _eq: true } } }) {
id
jobsizelimit
cdk_dealerid
pbs_serialnumber
md_ro_statuses
uselocalmediaserver
localmediaserverhttp
shopname
features
localmediatoken
tt_allow_post_to_invoiced
md_responsibility_centers
tt_enforce_hours_for_tech_console
}
}
`;

View File

@@ -0,0 +1,54 @@
import gql from "graphql-tag";
export const QUERY_EMPLOYEES = gql`
query QUERY_EMPLOYEES {
employees(order_by: { employee_number: asc }) {
last_name
id
first_name
flat_rate
employee_number
}
}
`;
export const QUERY_EMPLOYEE_BY_ID = gql`
query QUERY_EMPLOYEE_BY_ID($id: uuid!) {
employees_by_pk(id: $id) {
last_name
id
first_name
employee_number
active
termination_date
hire_date
flat_rate
rates
pin
user_email
external_id
employee_vacations(order_by: { start: desc }) {
id
start
end
}
}
}
`;
export const QUERY_ACTIVE_EMPLOYEES = gql`
query QUERY_ACTIVE_EMPLOYEES {
employees(where: { active: { _eq: true } }) {
last_name
id
first_name
employee_number
active
termination_date
hire_date
flat_rate
rates
pin
user_email
}
}
`;

View File

@@ -451,6 +451,54 @@ export const ACTIVE_JOBS_FOR_AUTOCOMPLETE = gql`
}
`;
export const SEARCH_JOBS_FOR_AUTOCOMPLETE = gql`
query SEARCH_JOBS_FOR_AUTOCOMPLETE(
$search: String
$isConverted: Boolean
$notExported: Boolean
$notInvoiced: Boolean
) {
search_jobs(
args: { search: $search }
limit: 50
where: {
_and: {
converted: { _eq: $isConverted }
date_exported: { _is_null: $notExported }
date_invoiced: { _is_null: $notInvoiced }
}
}
) {
id
ownr_fn
ownr_ln
ro_number
clm_no
vehicleid
v_make_desc
v_model_desc
v_model_yr
status
}
}
`;
export const SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE = gql`
query SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE($id: uuid!) {
jobs_by_pk(id: $id) {
id
ownr_fn
ownr_ln
ro_number
clm_no
vehicleid
v_make_desc
v_model_desc
v_model_yr
status
}
}
`;
export const SEARCH_FOR_JOBS = gql`
query SEARCH_FOR_JOBS($search: String!) {
jobs(where: { ro_number: { _ilike: $search } }) {
@@ -875,6 +923,54 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
}
`;
export const GET_LINE_TICKET_BY_PK = gql`
query GET_LINE_TICKET_BY_PK($id: uuid!) {
jobs_by_pk(id: $id) {
id
lbr_adjustments
converted
}
joblines(where: { jobid: { _eq: $id }, removed: { _eq: false } }) {
id
line_desc
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
convertedtolbr
convertedtolbr_data
}
timetickets(where: { jobid: { _eq: $id } }) {
actualhrs
ciecacode
cost_center
date
id
jobid
employeeid
memo
flat_rate
clockon
clockoff
rate
employee {
id
first_name
last_name
employee_number
}
productivehrs
}
}
`;
export const generate_UPDATE_JOB_KANBAN = (
oldChildId,
oldChildNewParent,

View File

@@ -0,0 +1,85 @@
import gql from "graphql-tag";
export const INSERT_NEW_TIME_TICKET = gql`
mutation INSERT_NEW_TIME_TICKET(
$timeTicketInput: [timetickets_insert_input!]!
) {
insert_timetickets(objects: $timeTicketInput) {
returning {
id
clockon
clockoff
employeeid
productivehrs
actualhrs
ciecacode
date
memo
flat_rate
}
}
}
`;
export const UPDATE_TIME_TICKET = gql`
mutation UPDATE_TIME_TICKET(
$timeticketId: uuid!
$timeticket: timetickets_set_input!
) {
update_timetickets(
where: { id: { _eq: $timeticketId } }
_set: $timeticket
) {
returning {
id
clockon
clockoff
employeeid
productivehrs
actualhrs
ciecacode
created_at
updated_at
jobid
date
flat_rate
memo
}
}
}
`;
export const QUERY_ACTIVE_TIME_TICKETS = gql`
query QUERY_ACTIVE_TIME_TICKETS($employeeId: uuid) {
timetickets(
order_by: { date: desc_nulls_first }
where: {
_and: {
clockoff: { _is_null: true }
employeeid: { _eq: $employeeId }
clockon: { _is_null: false }
jobid: { _is_null: false }
}
}
) {
id
clockon
memo
cost_center
flat_rate
jobid
job {
id
ownr_fn
ownr_ln
ownr_co_nm
v_model_desc
v_make_desc
v_model_yr
ro_number
}
}
}
`;

View File

@@ -22,6 +22,7 @@
"@react-native-async-storage/async-storage": "^1.17.11",
"@react-native-community/art": "^1.2.0",
"@react-native-community/cli-debugger-ui": "^9.0.0",
"@react-native-community/datetimepicker": "6.7.3",
"@react-native-community/masked-view": "^0.1.11",
"@react-navigation/bottom-tabs": "^6.3.3",
"@react-navigation/drawer": "^6.4.4",
@@ -46,7 +47,7 @@
"expo-permissions": "^14.1.1",
"expo-status-bar": "~1.4.2",
"expo-system-ui": "^2.2.1",
"expo-updates": "^0.16.3",
"expo-updates": "^0.16.4",
"expo-video-thumbnails": "^7.2.1",
"firebase": "^9.8.3",
"formik": "^2.2.9",
@@ -62,13 +63,15 @@
"react-dom": "^18.2.0",
"react-i18next": "^11.17.2",
"react-is": ">= 16.8.0",
"react-native": "^0.71.4",
"react-native": "0.71.6",
"react-native-dnd-board": "hungga1711/react-native-dnd-board#13/head",
"react-native-draggable-flatlist": "^3.1.2",
"react-native-element-dropdown": "^2.9.0",
"react-native-gesture-handler": "^2.9.0",
"react-native-image-gallery": "^2.1.5",
"react-native-image-viewing": "^0.2.2",
"react-native-indicators": "^0.17.0",
"react-native-modal-datetime-picker": "^15.0.0",
"react-native-pager-view": "6.1.2",
"react-native-paper": "^4.12.4",
"react-native-progress": "^5.0.0",

View File

@@ -27,3 +27,8 @@ export const documentUploadFailure = (error) => ({
export const toggleDeleteAfterUpload = () => ({
type: AppActionTypes.TOGGLE_DLETE_AFTER_UPLOAD,
});
export const setTmTicketJobId = (jobId) => ({
type: AppActionTypes.SET_TM_TICKET_JOB_ID,
payload: jobId,
});

View File

@@ -6,6 +6,7 @@ const INITIAL_STATE = {
documentUploadInProgress: null,
documentUploadError: null,
deleteAfterUpload: false,
tmTicketJobId: null,
};
const appReducer = (state = INITIAL_STATE, action) => {
@@ -43,6 +44,11 @@ const appReducer = (state = INITIAL_STATE, action) => {
...state,
deleteAfterUpload: !state.deleteAfterUpload,
};
case AppActionTypes.SET_TM_TICKET_JOB_ID:
return {
...state,
tmTicketJobId: action.payload,
};
default:
return state;
}

View File

@@ -26,3 +26,8 @@ export const selectDeleteAfterUpload = createSelector(
[selectApp],
(app) => app.deleteAfterUpload
);
export const selectCurrentTmTicketJobId = createSelector(
[selectApp],
(app) => app.tmTicketJobId
);

View File

@@ -5,5 +5,6 @@ const AppActionTypes = {
DOCUMENT_UPLOAD_SUCCESS: "DOCUMENT_UPLOAD_SUCCESS",
DOCUMENT_UPLOAD_FAILURE: "DOCUMENT_UPLOAD_FAILURE",
TOGGLE_DLETE_AFTER_UPLOAD: "TOGGLE_DLETE_AFTER_UPLOAD",
SET_TM_TICKET_JOB_ID: "SET_TM_TICKET_JOB_ID"
};
export default AppActionTypes;

View File

@@ -0,0 +1,34 @@
import EmployeeActionTypes from "./employee.types";
export const employeeSignInStart = (employeeIdAndPin) => ({
type: EmployeeActionTypes.EMPLOYEE_SIGN_IN_START,
payload: employeeIdAndPin,
});
export const employeeSignInSuccess = (technician) => ({
type: EmployeeActionTypes.EMPLOYEE_SIGN_IN_SUCCESS,
payload: technician,
});
export const employeeSignInFailure = (error) => ({
type: EmployeeActionTypes.EMPLOYEE_SIGN_IN_FAILURE,
payload: error,
});
export const employeeSignOut = () => ({
type: EmployeeActionTypes.EMPLOYEE_SIGN_OUT,
});
export const employeeGetRatesStart = (employeeId) => ({
type: EmployeeActionTypes.EMPLOYEE_GET_RATES_START,
payload: employeeId,
});
export const employeeGetRatesSuccess = (employees_by_pk) => ({
type: EmployeeActionTypes.EMPLOYEE_GET_RATES_SUCCESS,
payload: employees_by_pk,
});
export const employeeGetRatesFailure = (error) => ({
type: EmployeeActionTypes.EMPLOYEE_GET_RATES_FAILURE,
payload: error,
});

View File

@@ -0,0 +1,63 @@
import EmployeeActionTypes from "./employee.types";
const INITIAL_STATE = {
currentEmployee: {
authorized: null,
technician: null,
},
signingIn: false,
gettingRates: false,
error: null,
};
const employeeReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case EmployeeActionTypes.EMPLOYEE_SIGN_IN_START:
return {
...state,
signingIn: true,
};
case EmployeeActionTypes.EMPLOYEE_SIGN_IN_SUCCESS:
return {
...state,
currentEmployee: { authorized: true, technician:action.payload },
signingIn: false,
error: null,
};
case EmployeeActionTypes.EMPLOYEE_SIGN_IN_FAILURE:
return {
...state,
signingIn: false,
error: action.payload,
};
case EmployeeActionTypes.EMPLOYEE_SIGN_OUT:
return {
...state,
currentEmployee: { authorized: false, technician:null },//TODO check if this needs to do clean up like set loaders all to false
error: null,
};
case EmployeeActionTypes.EMPLOYEE_GET_RATES_START:
return {
...state,
gettingRates: true,
};
case EmployeeActionTypes.EMPLOYEE_GET_RATES_SUCCESS:
return {
...state,
currentEmployee: { ...state.currentEmployee, technician:action.payload },
gettingRates: false,
error: null,
};
case EmployeeActionTypes.EMPLOYEE_GET_RATES_FAILURE:
return {
...state,
gettingRates: false,
error: action.payload,
};
default:
return state;
}
};
export default employeeReducer;

View File

@@ -0,0 +1,114 @@
import EmployeeActionTypes from "./employee.types";
import {
employeeSignInStart,
employeeSignInSuccess,
employeeSignInFailure,
employeeGetRatesStart,
employeeGetRatesSuccess,
employeeGetRatesFailure
} from "./employee.actions";
import { all, call, put, select, takeLatest } from "redux-saga/effects";
import { logImEXEvent } from "../../firebase/firebase.analytics";
import { selectBodyshop } from "../user/user.selectors";
import axios from "axios";
import { client } from "../../graphql/client";
import { QUERY_EMPLOYEE_BY_ID } from "../../graphql/employees.queries";
import { selectCurrentEmployee } from "./employee.selectors";
export function* onSignInEmployeeStart() {
yield takeLatest(
EmployeeActionTypes.EMPLOYEE_SIGN_IN_START,
signInWithEmployeeId
);
}
export function* signInWithEmployeeId({ payload: { employeeId, pin } }) {
try {
logImEXEvent("redux_sign_in_employee_attempt");
//console.loging
// console.log("Saga", employeeId, pin, pin);
const bodyshop = yield select(selectBodyshop);
const response = yield call(axios.post, "/tech/login", {
shopid: bodyshop.id,
employeeid: employeeId,
pin: pin,
});
const { valid, technician, error } = response.data;
if (valid) {
yield put(employeeSignInSuccess(technician));
} else {
yield put(employeeSignInFailure(error));
}
} catch (error) {
yield put(employeeSignInFailure(error));
}
}
/*
//Waits for EMPLOYEE_SIGN_IN_SUCCESS when logging in to then call QUERY_EMPLOYEE_BY_ID with the id in payload to override technician with more data like rates and flat_rate
*/
export function* onEmployeeSignInSuccessSaga() {
yield takeLatest(
EmployeeActionTypes.EMPLOYEE_SIGN_IN_SUCCESS,
updateEmployeeWithEmployeeId
);
}
export function* updateEmployeeWithEmployeeId({ payload }) {
try {
const employeeId = payload.id;
// console.log("updateEmployeeWithEmployeeId",employeeId);
// logImEXEvent("redux_update_employee_with_employee_id_attempt", employeeId);
const result = yield client.query({ query: QUERY_EMPLOYEE_BY_ID,
variables: {
id: employeeId,
},
skip: !employeeId
});
const { employees_by_pk } = result?.data;
if (employees_by_pk) {
yield put(employeeGetRatesSuccess(employees_by_pk));
} else {
yield put(employeeGetRatesFailure(result?.error));
}
} catch (error) {
yield put(employeeGetRatesFailure(error));
// console.log("Error while getting employee rates.", error);
Sentry.Native.captureException(error);
}
}
export function* onEmployeeGetRatesStart() {
yield takeLatest(
EmployeeActionTypes.EMPLOYEE_GET_RATES_START,
getRatesWithEmployeeId
);
}
export function* getRatesWithEmployeeId({ payload: employeeId }) {
try {
logImEXEvent("redux_employee_get_rates_attempt", employeeId);
const result = yield client.query({ query: QUERY_EMPLOYEE_BY_ID,
variables: {
id: employeeId,
}
});
const { employees_by_pk } = result.data;
if (employees_by_pk) {
yield put(employeeGetRatesSuccess(employees_by_pk));
} else {
yield put(employeeGetRatesFailure(error));
}
} catch (error) {
yield put(employeeGetRatesFailure(error));
// console.log("Error while getting employee rates.", error);
Sentry.Native.captureException(error);
}
}
export function* employeeSagas() {
yield all([call(onSignInEmployeeStart), call(onEmployeeSignInSuccessSaga), call(onEmployeeGetRatesStart)]);
}

View File

@@ -0,0 +1,75 @@
import { createSelector } from "reselect";
const selectEmployee = (state) => state.employee;
export const selectCurrentEmployee = createSelector(
[selectEmployee],
(employee) => employee.currentEmployee
);
export const selectSigningIn = createSelector(
[selectEmployee],
(employee) => employee.signingIn
);
export const selectSignInError = createSelector(
[selectEmployee],
(employee) => employee.error
);
export const selectRates = createSelector(
[selectEmployee],
(employee) => employee.currentEmployee?.technician?.rates?.filter((v) => (v?.cost_center !== "timetickets.labels.shift"))
);
export const selectGettingRates = createSelector(
[selectEmployee],
(employee) => employee.gettingRates
);
export const selectTechnician = createSelector(
[selectEmployee],
(employee) => {
if (!employee.currentEmployee || !employee.currentEmployee.technician) {
// console.info("selectTechnician returning null");
return null;
}
// console.info("selectTechnician returning :", employee.currentEmployee.technician);
return employee.currentEmployee.technician;
}
);
export const selectEmployeeFirstName = createSelector(
[selectTechnician],
(techResults) => {
if (!techResults || !techResults.first_name) {
// console.info("selectEmployeeFirstName returning null");
return null;
}
// console.info("selectEmployeeFirstName returning :", techResults.first_name);
return techResults.first_name;
}
);
export const selectEmployeeLastName = createSelector(
[selectTechnician],
(techResults) => {
if (!techResults || !techResults.last_name) {
// console.info("selectEmployeeLastName returning null");
return null;
}
// console.info("selectEmployeeLastName returning :", techResults.last_name);
return techResults.last_name;
}
);
export const selectEmployeeFullName = createSelector(
[selectEmployeeFirstName,selectEmployeeLastName],
(fName,lName) => {
if (!fName && !lName) {
// console.warn("selectEmployeeFullName returning null");
return null;
}
if (fName) {
if(lName){
return fName + " " + lName;
} else {
return fName;
}
} else {
return lName;
}
}
);

View File

@@ -0,0 +1,16 @@
const EmployeeActionTypes = {
EMPLOYEE_SIGN_IN_START: "EMPLOYEE_SIGN_IN_START",
EMPLOYEE_SIGN_IN_SUCCESS: "EMPLOYEE_SIGN_IN_SUCCESS",
EMPLOYEE_SIGN_IN_FAILURE: "EMPLOYEE_SIGN_IN_FAILURE",
EMPLOYEE_AUTHORIZING_REQUEST: "EMPLOYEE_AUTHORIZING_REQUEST",
EMPLOYEE_AUTHORIZING_SUCCESS: "EMPLOYEE_AUTHORIZING_SUCCESS",
EMPLOYEE_AUTHORIZING_FAILURE: "EMPLOYEE_AUTHORIZING_FAILURE",
EMPLOYEE_SIGN_OUT: "EMPLOYEE_SIGN_OUT",
EMPLOYEE_GET_RATES_START: "EMPLOYEE_GET_RATES_START",
EMPLOYEE_GET_RATES_SUCCESS: "EMPLOYEE_GET_RATES_SUCCESS",
EMPLOYEE_GET_RATES_FAILURE: "EMPLOYEE_GET_RATES_FAILURE",
EMPLOYEE_CHECK_SESSION: "EMPLOYEE_CHECK_SESSION",
EMPLOYEE_SET_CURRENT: "EMPLOYEE_SET_CURRENT"
};
export default EmployeeActionTypes;

View File

@@ -4,18 +4,22 @@ import { persistReducer } from "redux-persist";
import appReducer from "./app/app.reducer";
import photosReducer from "./photos/photos.reducer";
import userReducer from "./user/user.reducer";
import employeeReducer from './employee/employee.reducer';
import timeTicketsReducer from './timetickets/timetickets.reducer';
const persistConfig = {
key: "root",
storage: AsyncStorage,
// whitelist: ["photos"],
blacklist: ["user"],
blacklist: ["user","employee","timeTickets"],
};
const rootReducer = combineReducers({
user: userReducer,
app: appReducer,
photos: photosReducer,
employee: employeeReducer,
timeTickets: timeTicketsReducer,
});
export default persistReducer(persistConfig, rootReducer);

View File

@@ -2,7 +2,9 @@ import { all, call } from "redux-saga/effects";
import { appSagas } from "./app/app.sagas";
import { photosSagas } from "./photos/photos.sagas";
import { userSagas } from "./user/user.sagas";
import { employeeSagas } from "./employee/employee.sagas";
import { timeTicketsSagas } from "./timetickets/timetickets.sagas";
export default function* rootSaga() {
yield all([call(userSagas), call(appSagas), call(photosSagas)]);
yield all([call(userSagas), call(appSagas), call(photosSagas), call(employeeSagas), call(timeTicketsSagas),]);
}

View File

@@ -1,6 +1,6 @@
import { applyMiddleware, compose, createStore } from "redux";
import { persistStore } from "redux-persist";
//import { createLogger } from "redux-logger";
import { createLogger } from "redux-logger";
import createSagaMiddleware from "redux-saga";
import rootReducer from "./root.reducer";
import rootSaga from "./root.saga";
@@ -9,11 +9,11 @@ const sagaMiddleWare = createSagaMiddleware();
const middlewares = [sagaMiddleWare];
// if (process.env.NODE_ENV === "development") {
// middlewares.push(
// createLogger({
// collapsed: true,
// })
// );
middlewares.push(
createLogger({
collapsed: true,
})
);
// }
//Add in for React Native Debugger.

View File

@@ -0,0 +1,52 @@
import TimeTicketsActionTypes from "./timetickets.types";
export const setTimeTicket = (timeTicket) => ({
type: TimeTicketsActionTypes.SET_TIME_TICKET,
payload: timeTicket,
});
export const setTimeTicketJobId = (jobId) => ({
type: TimeTicketsActionTypes.SET_TIME_TICKET_JOB_ID,
payload: jobId,
});
export const setTimeTicketJob = (job) => ({
type: TimeTicketsActionTypes.SET_TIME_TICKET_JOB,
payload: job,
});
export const timeTicketCreateStart = (timeTicket) => ({
type: TimeTicketsActionTypes.TIME_TICKET_CREATE_START,
payload: timeTicket,
});
export const timeTicketCreateSuccess = (insertTimeTickets) => ({
type: TimeTicketsActionTypes.TIME_TICKET_CREATE_SUCCESS,
payload: insertTimeTickets,
});
export const timeTicketCreateFailure = (error) => ({
type: TimeTicketsActionTypes.TIME_TICKET_CREATE_FAILURE,
payload: error,
});
export const timeTicketClockInStart = (timeTicket) => ({
type: TimeTicketsActionTypes.TIME_TICKET_CLOCKIN_START,
payload: timeTicket,
});
export const timeTicketClockInSuccess = (insertTimeTickets) => ({
type: TimeTicketsActionTypes.TIME_TICKET_CLOCKIN_SUCCESS,
payload: insertTimeTickets,
});
export const timeTicketClockInFailure = (error) => ({
type: TimeTicketsActionTypes.TIME_TICKET_CLOCKIN_FAILURE,
payload: error,
});
export const timeTicketClockOutStart = (timeTicket) => ({
type: TimeTicketsActionTypes.TIME_TICKET_CLOCKOUT_START,
payload: timeTicket,
});
export const timeTicketClockOutSuccess = (insertTimeTickets) => ({
type: TimeTicketsActionTypes.TIME_TICKET_CLOCKOUT_SUCCESS,
payload: insertTimeTickets,
});
export const timeTicketClockOutFailure = (error) => ({
type: TimeTicketsActionTypes.TIME_TICKET_CLOCKOUT_FAILURE,
payload: error,
});

View File

@@ -0,0 +1,70 @@
import TimeTicketsActionTypes from "./timetickets.types";
const INITIAL_STATE = {
ttjobid:null,
timeticketjobid: null,
timeTicketJob: null,
uploadTimeTicketInProgress: false,
uploadTimeTicketError: null,
clockingIn: false,
clockingInError: null,
clockingOut: false,
clockingOutError: null,
};
const timeTicketsReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case TimeTicketsActionTypes.SET_TIME_TICKET:
return {
...state,
timeTicket: action.payload
};
case TimeTicketsActionTypes.SET_TIME_TICKET_JOB_ID:
return { ...state,timeticketjobid: action.payload };
case TimeTicketsActionTypes.SET_TIME_TICKET_JOB:
return {
...state,
timeTicketJob: action.payload
};
case TimeTicketsActionTypes.TIME_TICKET_CREATE_START:
return {...state,uploadTimeTicketInProgress: true};
case TimeTicketsActionTypes.TIME_TICKET_CREATE_SUCCESS:
return {...state,
uploadTimeTicketInProgress: false,
uploadTimeTicketError: null,
};
case TimeTicketsActionTypes.TIME_TICKET_CREATE_FAILURE:
return {...state,
uploadTimeTicketInProgress: false,
uploadTimeTicketError: action.payload,
};
case TimeTicketsActionTypes.TIME_TICKET_CLOCKIN_START:
return {...state,clockingIn: true};
case TimeTicketsActionTypes.TIME_TICKET_CLOCKIN_SUCCESS:
return {...state,
clockingIn: false,
clockingInError: null,
};
case TimeTicketsActionTypes.TIME_TICKET_CLOCKIN_FAILURE:
return {...state,
clockingIn: false,
clockingInError: action.payload,
};
case TimeTicketsActionTypes.TIME_TICKET_CLOCKOUT_START:
return {...state,clockingOut: true};
case TimeTicketsActionTypes.TIME_TICKET_CLOCKOUT_SUCCESS:
return {...state,
clockingOut: false,
clockingOutError: null,
};
case TimeTicketsActionTypes.TIME_TICKET_CLOCKOUT_FAILURE:
return {...state,
clockingOut: false,
clockingOutError: action.payload,
};
default:
return state;
}
};
export default timeTicketsReducer;

View File

@@ -0,0 +1,152 @@
import {
timeTicketCreateFailure,
timeTicketCreateSuccess,
timeTicketClockInSuccess,
timeTicketClockInFailure,
timeTicketClockOutSuccess,
timeTicketClockOutFailure,
} from "./timetickets.actions";
import TimeTicketsActionTypes from "./timetickets.types";
import { client } from "../../graphql/client";
import { all, call, put, select, takeLatest } from "redux-saga/effects";
import { logImEXEvent } from "../../firebase/firebase.analytics";
import { selectCurrentTimeTicket } from "./timetickets.selectors";
import { INSERT_NEW_TIME_TICKET } from "../../graphql/timetickets.queries";
export function* onCreateTimeTicketStart() {
yield takeLatest(
TimeTicketsActionTypes.TIME_TICKET_CREATE_START,
insertNewTimeTicket
);
}
export function* insertNewTimeTicket({ payload: { timeTicketInput } }) {
try {
logImEXEvent("redux_insertnewtimeticket_attempt");
// console.log("Saga, TIME_TICKET_CREATE_START :", timeTicketInput);
//console.loging
// console.log("Saga", employeeId, pin, pin);
const timeTicket = yield select(selectCurrentTimeTicket);
// const response = yield call(axios.post, "/tech/login", {
// shopid: bodyshop.id,
// employeeid: employeeId,
// pin: pin,
// });
// const { valid, data, error } = response.data;
// const result = yield client.mutate({
// mutation: INSERT_NEW_TIME_TICKET,
// variables: {
// timeTicketInput: [
// // {
// // bodyshopid: bodyshop.id,
// // employeeid: technician.id,
// // date: moment(theTime).format("YYYY-MM-DD"),
// // clockon: moment(theTime),
// // jobid: values.jobid,
// // cost_center: values.cost_center,
// // ciecacode:
// // bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
// // ? values.cost_center
// // : Object.keys(
// // bodyshop.md_responsibility_centers.defaults.costs
// // ).find((key) => {
// // return (
// // bodyshop.md_responsibility_centers.defaults.costs[key] ===
// // values.cost_center
// // );
// // }),
// // },
// ],
// },
// });
// console.log(result);
// const { valid, data, error } = result.data;
// if (valid) {
// yield put(timeTicketCreateSuccess(data));
// } else {
// yield put(timeTicketCreateFailure(error));
// }
} catch (error) {
yield put(timeTicketCreateFailure(error));
}
}
export function* onClockOutStart() {
yield takeLatest(
TimeTicketsActionTypes.TIME_TICKET_CLOCKOUT_START,
clockOutStart
);
}
export function* clockOutStart({ payload: { timeTicketInput } }) {
try {
logImEXEvent("redux_clockOutStart_attempt");
//console.loging
// console.log("Saga, clockOutStart :", timeTicketInput);
// const timeTicket = yield select(selectCurrentTimeTicket);
// const response = yield call(axios.post, "/tech/login", {
// shopid: bodyshop.id,
// employeeid: employeeId,
// pin: pin,
// });
// const { valid, data, error } = response.data;
// const result = yield client.mutate({
// mutation: INSERT_NEW_TIME_TICKET,
// variables: {
// timeTicketInput: [
// // {
// // bodyshopid: bodyshop.id,
// // employeeid: technician.id,
// // date: moment(theTime).format("YYYY-MM-DD"),
// // clockon: moment(theTime),
// // jobid: values.jobid,
// // cost_center: values.cost_center,
// // ciecacode:
// // bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
// // ? values.cost_center
// // : Object.keys(
// // bodyshop.md_responsibility_centers.defaults.costs
// // ).find((key) => {
// // return (
// // bodyshop.md_responsibility_centers.defaults.costs[key] ===
// // values.cost_center
// // );
// // }),
// // },
// ],
// },
// });
// console.log(result);
// const { valid, data, error } = result.data;
// if (valid) {
// yield put(timeTicketCreateSuccess(data));
// } else {
// yield put(timeTicketCreateFailure(error));
// }
} catch (error) {
yield put(timeTicketClockOutFailure(error));
}
}
export function* onClockInStart() {
yield takeLatest(
TimeTicketsActionTypes.TIME_TICKET_CLOCKIN_START,
clockInStart
);
}
export function* clockInStart({ payload: { timeTicketInput } }) {
try {
logImEXEvent("redux_clockInStart_attempt");
// console.log("Saga, clockInStart :", timeTicketInput);
} catch (error) {
yield put(timeTicketClockInFailure(error));
}
}
export function* timeTicketsSagas() {
yield all([
call(onCreateTimeTicketStart),
call(onClockOutStart),
call(onClockInStart),
]);
}

View File

@@ -0,0 +1,17 @@
import { createSelector } from "reselect";
const selectTimeTicketsState = (state) => state.timeTickets;
export const selectCurrentTimeTicketJobId = createSelector(
[selectTimeTicketsState],
(timeTickets) => timeTickets.timeTicketJobId
);
export const selectCurrentTimeTicketJob = createSelector(
[selectTimeTicketsState],
(timeTickets) => timeTickets.timeTicketJob
);
export const selectCurrentTimeTicket = createSelector(
[selectTimeTicketsState],
(timeTickets) => timeTickets.timeTicket
);

View File

@@ -0,0 +1,15 @@
const TimeTicketsActionTypes = {
SET_TIME_TICKET: "SET_TIME_TICKET",
SET_TIME_TICKET_JOB: "SET_TIME_TICKET_JOB",
SET_TIME_TICKET_JOB_ID: "SET_TIME_TICKET_JOB_ID",
TIME_TICKET_CREATE_START: "TIME_TICKET_CREATE_START",
TIME_TICKET_CREATE_SUCCESS: "TIME_TICKET_CREATE_SUCCESS",
TIME_TICKET_CREATE_FAILURE: "TIME_TICKET_CREATE_FAILURE",
TIME_TICKET_CLOCKIN_START: "TIME_TICKET_CLOCKIN_START",
TIME_TICKET_CLOCKIN_SUCCESS: "TIME_TICKET_CLOCKIN_SUCCESS",
TIME_TICKET_CLOCKIN_FAILURE: "TIME_TICKET_CLOCKIN_FAILURE",
TIME_TICKET_CLOCKOUT_START: "TIME_TICKET_CLOCKOUT_START",
TIME_TICKET_CLOCKOUT_SUCCESS: "TIME_TICKET_CLOCKOUT_SUCCESS",
TIME_TICKET_CLOCKOUT_FAILURE: "TIME_TICKET_CLOCKOUT_FAILURE",
};
export default TimeTicketsActionTypes;

View File

@@ -31,3 +31,15 @@ export const selectSigningIn = createSelector(
[selectUser],
(user) => user.signingIn
);
export const selectRestrictClaimableHoursFlag = createSelector(
[selectUser],
(user) => {
if (!user.bodyshop || !user.bodyshop.tt_enforce_hours_for_tech_console) {
// console.info("selectRestrictClaimableHoursFlag returning null");
return null;
}
// console.info("selectRestrictClaimableHoursFlag returning :", user.bodyshop.tt_enforce_hours_for_tech_console);
return user.bodyshop.tt_enforce_hours_for_tech_console;
}
);

View File

@@ -11,7 +11,8 @@
},
"general": {
"actions": {
"signout": "Sign Out"
"signout": "Sign Out",
"logout": "Logout"
},
"labels": {
"na": "N/A"
@@ -305,6 +306,222 @@
"email": "Email",
"password": "Password"
}
},
"employeesignin": {
"actions": {
"employeesignin": "Sign In"
},
"errors": {
"employeeidnotfound": "No employee ID found.",
"wrongpin": "The pin you entered is not correct."
},
"fields": {
"employeeid": "Employee ID",
"pin": "PIN"
},
"titles": {
"signin": "Employee Sign In"
}
},
"timeticketbrowser": {
"actions": {
"refresh": "Refresh",
"upload": "Upload",
"ticket":"Ticket",
"timetickets": "Time Tickets",
"detail": "Time Ticket Details",
"notickets": "There are no active tickets.",
"clockin": "Clock In"
},
"labels": {
"loggedinemployee": "Logged in Employee",
"clockintojob": "Clock In",
"nodata":"No Data"
},
"errors": {
"missingvalues": "Please make sure all fields have a value."
},
"titles": {
"timeticketbrowsertab": "Time Tickets"
}
},
"createtimeticket": {
"actions": {
"refresh": "Refresh",
"upload": "Upload",
"ticket":"Ticket",
"timetickets": "Time Tickets",
"detail": "Time Ticket Details",
"notickets": "There are no active tickets.",
"ticketdate": "Ticket Date: ",
"createticket": "Create Ticket"
},
"labels": {
"converting": "Converting",
"selectjob": "--- Select a ticket ---",
"selectticketassetselector": "Please select a ticket to update. ",
"uploading": "Uploading",
"employeeplaceholder": "Employee",
"actualhoursplaceholder": "Actual Hours",
"productivehoursplaceholder": "Productive Hours"
},
"errors": {
"nan": "Please make sure all fields have valid values.",
"missingvalues": "Please make sure all fields have a value."
},
"titles": {
"createtimeticket": "New Time Ticket"
}
},
"joblines": {
"actions": {
"converttolabor": "Convert amount to Labor.",
"new": "New Line"
},
"errors": {
"creating": "Error encountered while creating job line. {{message}}",
"updating": "Error encountered updating job line. {{message}}"
},
"fields": {
"act_price": "Retail Price",
"ah_detail_line": "Mark as Detail Labor Line (Autohouse Only)",
"db_price": "List Price",
"lbr_types": {
"LA1": "LA1",
"LA2": "LA2",
"LA3": "LA3",
"LA4": "LA4",
"LAA": "Aluminum",
"LAB": "Body",
"LAD": "Diagnostic",
"LAE": "Electrical",
"LAF": "Frame",
"LAG": "Glass",
"LAM": "Mechanical",
"LAR": "Refinish",
"LAS": "Structural",
"LAU": "User Defined"
},
"line_desc": "Line Desc.",
"line_ind": "S#",
"line_no": "Line #",
"location": "Location",
"mod_lb_hrs": "Hrs",
"mod_lbr_ty": "Labor Type",
"notes": "Notes",
"oem_partno": "OEM Part #",
"op_code_desc": "Op Code Description",
"part_qty": "Qty.",
"part_type": "Part Type",
"part_types": {
"CCC": "CC Cleaning",
"CCD": "CC Damage Waiver",
"CCDR": "CC Daily Rate",
"CCF": "CC Refuel",
"CCM": "CC Mileage",
"PAA": "Aftermarket",
"PAC": "Rechromed",
"PAE": "Existing",
"PAG": "Glass",
"PAL": "LKQ",
"PAM": "Remanufactured",
"PAN": "New/OEM",
"PAO": "Other",
"PAP": "OEM Partial",
"PAR": "Recored",
"PAS": "Sublet",
"PASL": "Sublet (L)"
},
"profitcenter_labor": "Profit Center: Labor",
"profitcenter_part": "Profit Center: Part",
"prt_dsmk_m": "Line Discount/Markup $",
"prt_dsmk_p": "Line Discount/Markup %",
"status": "Status",
"tax_part": "Tax Part",
"total": "Total",
"unq_seq": "Seq #"
},
"labels": {
"adjustmenttobeadded": "Adjustment to be added: {{adjustment}}",
"billref": "Latest Bill",
"convertedtolabor": "This line has been converted to labor. Ensure you adjust the profit center for the amount accordingly.",
"edit": "Edit Line",
"ioucreated": "IOU",
"new": "New Line",
"nostatus": "No Status",
"presets": "Jobline Presets"
},
"successes": {
"created": "Job line created successfully.",
"saved": "Job line saved.",
"updated": "Job line updated successfully."
},
"validations": {
"ahdetailonlyonuserdefinedtypes": "Detail line indicator can only be set for LA1, LA2, LA3, LA4, and LAU labor types.",
"hrsrequirediflbrtyp": "Labor hours are required if a labor type is selected. Clear the labor type if there are no labor hours.",
"requiredifparttype": "Required if a part type has been specified.",
"zeropriceexistingpart": "This line cannot have any price since it uses an existing part."
}
},
"timeticketclockoff":{
"actions": {
"clockoff": "Clock Off",
"noinfo": "There is no info to display."
},
"labels": {
"actualhoursplaceholder": "Actual Hours",
"productivehoursplaceholder": "Productive Hours"
},
"errors": {
"nan": "Please make sure all fields have valid values.",
"missingvalues": "Please make sure all fields have a value.",
"hoursenteredmorethanavailable": "The number of hours entered is more than what is available for this cost center."
},
"titles": {
"clockoff": "Clock Off"
}
},
"selectjobid":{
"labels": {
"placeholder": "RO #",
"selectedplaceholder": "...",
"searchplaceholder": "Search...",
"noselection":"No Selection",
"nodata":"No Data"
}
},
"selectcostcenter":{
"labels": {
"placeholder": "Select Cost Center",
"selectedplaceholder": "...",
"searchplaceholder": "Search..."
}
},
"employeeclockedinlist": {
"labels": {
"alreadyclockedon": "Clocked into job(s)"
}
},
"clockedinlistitem": {
"labels": {
"vehicle": "Vehicle : ",
"clockedin": "Clocked In : ",
"costcenter": "Cost Center : "
},
"actions": {
"clockout": "Clock Out"
}
},
"laborallocations": {
"labels": {
"laborallocations": "Labor Allocations",
"costcenter": "Cost Center",
"hourstotal": "Hours Total",
"hoursclaimed": "Hours Claimed",
"adjustments": "Adjustments",
"difference": "Difference",
"totals": "Totals"
}
}
}
}

View File

@@ -11,7 +11,8 @@
},
"general": {
"actions": {
"signout": ""
"signout": "",
"logout": ""
},
"labels": {
"na": ""
@@ -305,6 +306,222 @@
"email": "Email",
"password": ""
}
},
"employeesignin": {
"actions": {
"employeesignin": ""
},
"errors": {
"employeeidnotfound": "",
"wrongpin": ""
},
"fields": {
"employeeid": "",
"pin": ""
},
"titles": {
"signin": ""
}
},
"timeticketbrowser": {
"actions": {
"refresh": "",
"upload": "",
"ticket": "",
"timetickets": "",
"detail": "",
"notickets": "",
"clockin": ""
},
"labels": {
"loggedinemployee": "",
"clockintojob": "",
"nodata":""
},
"errors": {
"missingvalues": ""
},
"titles": {
"timeticketbrowsertab": ""
}
},
"createtimeticket": {
"actions": {
"refresh": "",
"upload": " ",
"ticket":" ",
"timetickets": "",
"detail": "",
"notickets": "",
"ticketdate": "",
"createticket": ""
},
"labels": {
"converting": "",
"selectjob": "",
"selectticketassetselector": "",
"uploading": "",
"employeeplaceholder": "",
"actualhoursplaceholder": "",
"productivehoursplaceholder": ""
},
"errors": {
"nan": "",
"missingvalues": ""
},
"titles": {
"createtimeticket": ""
}
},
"joblines": {
"actions": {
"converttolabor": "",
"new": ""
},
"errors": {
"creating": "",
"updating": ""
},
"fields": {
"act_price": "Precio actual",
"ah_detail_line": "",
"db_price": "Precio de base de datos",
"lbr_types": {
"LA1": "",
"LA2": "",
"LA3": "",
"LA4": "",
"LAA": "",
"LAB": "",
"LAD": "",
"LAE": "",
"LAF": "",
"LAG": "",
"LAM": "",
"LAR": "",
"LAS": "",
"LAU": ""
},
"line_desc": "Descripción de línea",
"line_ind": "S#",
"line_no": "",
"location": "",
"mod_lb_hrs": "Horas laborales",
"mod_lbr_ty": "Tipo de trabajo",
"notes": "",
"oem_partno": "OEM parte #",
"op_code_desc": "",
"part_qty": "",
"part_type": "Tipo de parte",
"part_types": {
"CCC": "",
"CCD": "",
"CCDR": "",
"CCF": "",
"CCM": "",
"PAA": "",
"PAC": "",
"PAE": "",
"PAG": "",
"PAL": "",
"PAM": "",
"PAN": "",
"PAO": "",
"PAP": "",
"PAR": "",
"PAS": "",
"PASL": ""
},
"profitcenter_labor": "",
"profitcenter_part": "",
"prt_dsmk_m": "",
"prt_dsmk_p": "",
"status": "Estado",
"tax_part": "",
"total": "",
"unq_seq": "Seq #"
},
"labels": {
"adjustmenttobeadded": "",
"billref": "",
"convertedtolabor": "",
"edit": "Línea de edición",
"ioucreated": "",
"new": "Nueva línea",
"nostatus": "",
"presets": ""
},
"successes": {
"created": "",
"saved": "",
"updated": ""
},
"validations": {
"ahdetailonlyonuserdefinedtypes": "",
"hrsrequirediflbrtyp": "",
"requiredifparttype": "",
"zeropriceexistingpart": ""
}
},
"timeticketclockoff":{
"actions": {
"clockoff": "",
"noinfo": ""
},
"labels": {
"actualhoursplaceholder": "",
"productivehoursplaceholder": ""
},
"errors": {
"nan": "",
"missingvalues": "",
"hoursenteredmorethanavailable": ""
},
"titles": {
"clockoff": ""
}
},
"selectjobid":{
"labels": {
"placeholder": "",
"selectedplaceholder": "...",
"searchplaceholder": "",
"noselection":"",
"nodata":""
}
},
"selectcostcenter":{
"labels": {
"placeholder": "",
"selectedplaceholder": "...",
"searchplaceholder": ""
}
},
"employeeclockedinlist": {
"labels": {
"alreadyclockedon": ""
}
},
"clockedinlistitem": {
"labels": {
"vehicle": "",
"clockedin": "",
"costcenter": ""
},
"actions": {
"clockout": ""
}
},
"laborallocations": {
"labels": {
"laborallocations": "",
"costcenter": "",
"hourstotal": "",
"hoursclaimed": "",
"adjustments": "",
"difference": "",
"totals": ""
}
}
}
}

View File

@@ -11,7 +11,8 @@
},
"general": {
"actions": {
"signout": ""
"signout": "",
"logout": ""
},
"labels": {
"na": ""
@@ -305,6 +306,222 @@
"email": "Email",
"password": ""
}
},
"employeesignin": {
"actions": {
"employeesignin": ""
},
"errors": {
"employeeidnotfound": "",
"wrongpin": ""
},
"fields": {
"employeeid": "",
"pin": ""
},
"titles": {
"signin": ""
}
},
"timeticketbrowser": {
"actions": {
"refresh": "",
"upload": "",
"ticket": "",
"timetickets": "",
"detail": "",
"notickets": "",
"clockin": ""
},
"labels": {
"loggedinemployee": "",
"clockintojob": "",
"nodata":""
},
"errors": {
"missingvalues": ""
},
"titles": {
"timeticketbrowsertab": ""
}
},
"createtimeticket": {
"actions": {
"refresh": "",
"upload": " ",
"ticket":" ",
"timetickets": "",
"detail": "",
"notickets": "",
"ticketdate": "",
"createticket": ""
},
"labels": {
"converting": "",
"selectjob": "",
"selectticketassetselector": "",
"uploading": "",
"employeeplaceholder": "",
"actualhoursplaceholder": "",
"productivehoursplaceholder": ""
},
"errors": {
"nan": "",
"missingvalues": ""
},
"titles": {
"createtimeticket": ""
}
},
"joblines": {
"actions": {
"converttolabor": "",
"new": ""
},
"errors": {
"creating": "",
"updating": ""
},
"fields": {
"act_price": "Prix actuel",
"ah_detail_line": "",
"db_price": "Prix de la base de données",
"lbr_types": {
"LA1": "",
"LA2": "",
"LA3": "",
"LA4": "",
"LAA": "",
"LAB": "",
"LAD": "",
"LAE": "",
"LAF": "",
"LAG": "",
"LAM": "",
"LAR": "",
"LAS": "",
"LAU": ""
},
"line_desc": "Description de la ligne",
"line_ind": "S#",
"line_no": "",
"location": "",
"mod_lb_hrs": "Heures de travail",
"mod_lbr_ty": "Type de travail",
"notes": "",
"oem_partno": "Pièce OEM #",
"op_code_desc": "",
"part_qty": "",
"part_type": "Type de pièce",
"part_types": {
"CCC": "",
"CCD": "",
"CCDR": "",
"CCF": "",
"CCM": "",
"PAA": "",
"PAC": "",
"PAE": "",
"PAG": "",
"PAL": "",
"PAM": "",
"PAN": "",
"PAO": "",
"PAP": "",
"PAR": "",
"PAS": "",
"PASL": ""
},
"profitcenter_labor": "",
"profitcenter_part": "",
"prt_dsmk_m": "",
"prt_dsmk_p": "",
"status": "Statut",
"tax_part": "",
"total": "",
"unq_seq": "Seq #"
},
"labels": {
"adjustmenttobeadded": "",
"billref": "",
"convertedtolabor": "",
"edit": "Ligne d'édition",
"ioucreated": "",
"new": "Nouvelle ligne",
"nostatus": "",
"presets": ""
},
"successes": {
"created": "",
"saved": "",
"updated": ""
},
"validations": {
"ahdetailonlyonuserdefinedtypes": "",
"hrsrequirediflbrtyp": "",
"requiredifparttype": "",
"zeropriceexistingpart": ""
}
},
"timeticketclockoff":{
"actions": {
"clockoff": "",
"noinfo": ""
},
"labels": {
"actualhoursplaceholder": "",
"productivehoursplaceholder": ""
},
"errors": {
"nan": "",
"missingvalues": "",
"hoursenteredmorethanavailable": ""
},
"titles": {
"clockoff": ""
}
},
"selectjobid":{
"labels": {
"placeholder": "",
"selectedplaceholder": "...",
"searchplaceholder": "",
"noselection":"",
"nodata":""
}
},
"selectcostcenter":{
"labels": {
"placeholder": "",
"selectedplaceholder": "...",
"searchplaceholder": ""
}
},
"employeeclockedinlist": {
"labels": {
"alreadyclockedon": ""
}
},
"clockedinlistitem": {
"labels": {
"vehicle": "",
"clockedin": "",
"costcenter": ""
},
"actions": {
"clockout": ""
}
},
"laborallocations": {
"labels": {
"laborallocations": "",
"costcenter": "",
"hourstotal": "",
"hoursclaimed": "",
"adjustments": "",
"difference": "",
"totals": ""
}
}
}
}

View File

@@ -1,10 +1,10 @@
import axios from "axios";
import { auth } from "../firebase/firebase.utils";
if (process.env.NODE_ENV === "production") {
axios.defaults.baseURL =
process.env.REACT_APP_AXIOS_BASE_API_URL || "https://api.imex.online/";
}
import env from "../env";
//if (process.env.NODE_ENV === "production") {
axios.defaults.baseURL = "https://api.test.imex.online/";
//env.REACT_APP_AXIOS_BASE_API_URL || "https://api.imex.online/";
//}
export const axiosAuthInterceptorId = axios.interceptors.request.use(
async (config) => {

24
util/DateFormater.jsx Normal file
View File

@@ -0,0 +1,24 @@
import moment from "moment";
import React from "react";
export function DateFormatter(props) {
return props.children
? moment(props.children).format(
props.includeDay ? "ddd MM/DD/YYYY" : "MM/DD/YYYY"
)
: null;
}
export function DateTimeFormatter(props) {
return props.children
? moment(props.children).format(
props.format ? props.format : "MM/DD/YYYY hh:mm a"
)
: null;
}
export function TimeFormatter(props) {
return props.children
? moment(props.children).format(props.format ? props.format : "hh:mm a")
: null;
}

View File

@@ -0,0 +1,54 @@
import i18next from "i18next";
import React from "react";
export const CalculateAllocationsTotals = (
bodyshop,
joblines,
timetickets,
adjustments = []
) => {
// console.log("🚀 ~ file: adjustments", adjustments);
// console.log("🚀 ~ file: bodyshop", bodyshop);
// console.log("🚀 ~ file: joblines", joblines);
// console.log("🚀 ~ file: timetickets", timetickets);
const responsibilitycenters = bodyshop.md_responsibility_centers;
const jobCodes = joblines.map((item) => item.mod_lbr_ty);
// console.log("jobCodes :", jobCodes);
//.filter((value, index, self) => self.indexOf(value) === index && !!value);
const ticketCodes = timetickets.map((item) => item.ciecacode);
// console.log("ticketCodes :", ticketCodes);
//.filter((value, index, self) => self.indexOf(value) === index && !!value);
const adjustmentCodes = Object.keys(adjustments);
// console.log("adjustmentCodes :", adjustmentCodes);
const allCodes = [...jobCodes, ...ticketCodes, ...adjustmentCodes].filter(
(value, index, self) => self.indexOf(value) === index && !!value
);
// console.log("allCodes :", allCodes);
const r = allCodes.reduce((acc, value) => {
const r = {
opcode: value,
cost_center:
bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
? i18next.t(
`joblines.fields.lbr_types.${value && value.toUpperCase()}`
)
: responsibilitycenters.defaults.costs[value],
mod_lbr_ty: value,
total: joblines.reduce((acc2, val2) => {
return val2.mod_lbr_ty === value ? acc2 + val2.mod_lb_hrs : acc2;
}, 0),
adjustments: adjustments[value] || 0,
claimed: timetickets.reduce((acc3, val3) => {
return val3.ciecacode === value ? acc3 + val3.productivehrs : acc3;
}, 0),
};
r.difference = r.total + r.adjustments - r.claimed;
acc.push(r);
return acc;
}, []);
//console.log(" r is :", r);
return r;
};

91
util/timer.js Normal file
View File

@@ -0,0 +1,91 @@
import { useEffect } from "react";
import { useState } from "react";
import React from "react";
import { View, StyleSheet, Text } from "react-native";
import { Button, TextInput } from "react-native-paper";
export default function Timer() {
const [time, setTime] = useState(300000);
const [timerOn, setTimerOn] = useState(false);
useEffect(() => {
let interval = null;
if (timerOn) {
interval = setInterval(() => {
setTime((prevTime) => prevTime - 1000);
}, 10);
if (time == 0) {
//triger logout();
}
} else {
clearInterval(interval);
}
return () => clearInterval(interval);
// if (counter > 0) {
// setTimeout(() => setCounter(counter - 1), 1000);
// } else {
// //triger logout();
// }
}, [timerOn]);
return (
<>
<View>
<Text>{time}</Text>
</View>
<Button mode="outlined" onPress={start}>
<Text>{start}</Text>
</Button>
<Button mode="outlined" onPress={reset}>
<Text>{reset}</Text>
</Button>
<Button mode="outlined" onPress={stop}>
<Text>{stop}</Text>
</Button>
</>
);
}
function start() {
setTimerOn(true);
}
function stop() {
setTimerOn(false);
}
function reset() {
setTime(300000);
}
// The data/time we want to countdown to
// var countDownDate = new Date(moment.).getTime();
// // Run myfunc every second
// var myfunc = setInterval(function() {
// var now = new Date().getTime();
// var timeleft = countDownDate - now;
// // Calculating the days, hours, minutes and seconds left
// var days = Math.floor(timeleft / (1000 * 60 * 60 * 24));
// var hours = Math.floor((timeleft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
// var minutes = Math.floor((timeleft % (1000 * 60 * 60)) / (1000 * 60));
// var seconds = Math.floor((timeleft % (1000 * 60)) / 1000);
// // Result is output to the specific element
// document.getElementById("days").innerHTML = days + "d "
// document.getElementById("hours").innerHTML = hours + "h "
// document.getElementById("mins").innerHTML = minutes + "m "
// document.getElementById("secs").innerHTML = seconds + "s "
// // Display the message when countdown is over
// if (timeleft < 0) {
// clearInterval(myfunc);
// document.getElementById("days").innerHTML = ""
// document.getElementById("hours").innerHTML = ""
// document.getElementById("mins").innerHTML = ""
// document.getElementById("secs").innerHTML = ""
// document.getElementById("end").innerHTML = "TIME UP!!";
// }
// }, 1000);

143
yarn.lock
View File

@@ -2233,7 +2233,7 @@
dependencies:
serve-static "^1.13.1"
"@react-native-community/cli-doctor@^10.2.0":
"@react-native-community/cli-doctor@^10.2.2":
version "10.2.2"
resolved "https://registry.yarnpkg.com/@react-native-community/cli-doctor/-/cli-doctor-10.2.2.tgz#b1893604fa9fc8971064e7c00042350f96868bfe"
integrity sha512-49Ep2aQOF0PkbAR/TcyMjOm9XwBa8VQr+/Zzf4SJeYwiYLCT1NZRAVAVjYRXl0xqvq5S5mAGZZShS4AQl4WsZw==
@@ -2277,19 +2277,7 @@
glob "^7.1.3"
logkitty "^0.7.1"
"@react-native-community/cli-platform-ios@10.2.0":
version "10.2.0"
resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-ios/-/cli-platform-ios-10.2.0.tgz#be21c0e3bbf17358d540cc23e5556bf679f6322e"
integrity sha512-hIPK3iL/mL+0ChXmQ9uqqzNOKA48H+TAzg+hrxQLll/6dNMxDeK9/wZpktcsh8w+CyhqzKqVernGcQs7tPeKGw==
dependencies:
"@react-native-community/cli-tools" "^10.1.1"
chalk "^4.1.2"
execa "^1.0.0"
fast-xml-parser "^4.0.12"
glob "^7.1.3"
ora "^5.4.1"
"@react-native-community/cli-platform-ios@^10.2.1":
"@react-native-community/cli-platform-ios@10.2.1", "@react-native-community/cli-platform-ios@^10.2.1":
version "10.2.1"
resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-ios/-/cli-platform-ios-10.2.1.tgz#2e6bd2cb6d48cbb8720d7b7265bb1bab80745f72"
integrity sha512-hz4zu4Y6eyj7D0lnZx8Mf2c2si8y+zh/zUTgCTaPPLzQD8jSZNNBtUUiA1cARm2razpe8marCZ1QbTMAGbf3mg==
@@ -2301,7 +2289,7 @@
glob "^7.1.3"
ora "^5.4.1"
"@react-native-community/cli-plugin-metro@^10.2.0":
"@react-native-community/cli-plugin-metro@^10.2.2":
version "10.2.2"
resolved "https://registry.yarnpkg.com/@react-native-community/cli-plugin-metro/-/cli-plugin-metro-10.2.2.tgz#766914e3c8007dfe52b253544c4f6cd8549919ac"
integrity sha512-sTGjZlD3OGqbF9v1ajwUIXhGmjw9NyJ/14Lo0sg7xH8Pv4qUd5ZvQ6+DWYrQn3IKFUMfGFWYyL81ovLuPylrpw==
@@ -2355,17 +2343,17 @@
dependencies:
joi "^17.2.1"
"@react-native-community/cli@10.2.0":
version "10.2.0"
resolved "https://registry.yarnpkg.com/@react-native-community/cli/-/cli-10.2.0.tgz#bcb65bb3dcb03b0fc4e49619d51e12d23396b301"
integrity sha512-QH7AFBz5FX2zTZRH/o3XehHrZ0aZZEL5Sh+23nSEFgSj3bLFfvjjZhuoiRSAo7iiBdvAoXrfxQ8TXgg4Xf/7fw==
"@react-native-community/cli@10.2.2":
version "10.2.2"
resolved "https://registry.yarnpkg.com/@react-native-community/cli/-/cli-10.2.2.tgz#3fa438ba7f19f83e07bc337765fc1cabdcf2cac2"
integrity sha512-aZVcVIqj+OG6CrliR/Yn8wHxrvyzbFBY9cj7n0MvRw/P54QUru2nNqUTSSbqv0Qaa297yHJbe6kFDojDMSTM8Q==
dependencies:
"@react-native-community/cli-clean" "^10.1.1"
"@react-native-community/cli-config" "^10.1.1"
"@react-native-community/cli-debugger-ui" "^10.0.0"
"@react-native-community/cli-doctor" "^10.2.0"
"@react-native-community/cli-doctor" "^10.2.2"
"@react-native-community/cli-hermes" "^10.2.0"
"@react-native-community/cli-plugin-metro" "^10.2.0"
"@react-native-community/cli-plugin-metro" "^10.2.2"
"@react-native-community/cli-server-api" "^10.1.1"
"@react-native-community/cli-tools" "^10.1.1"
"@react-native-community/cli-types" "^10.0.0"
@@ -2378,6 +2366,13 @@
prompts "^2.4.0"
semver "^6.3.0"
"@react-native-community/datetimepicker@6.7.3":
version "6.7.3"
resolved "https://registry.yarnpkg.com/@react-native-community/datetimepicker/-/datetimepicker-6.7.3.tgz#e6d75a42729265d8404d1d668c86926564abca2f"
integrity sha512-fXWbEdHMLW/e8cts3snEsbOTbnFXfUHeO2pkiDFX3fWpFoDtUrRWvn50xbY13IJUUKHDhoJ+mj24nMRVIXfX1A==
dependencies:
invariant "^2.2.4"
"@react-native-community/masked-view@^0.1.11":
version "0.1.11"
resolved "https://registry.yarnpkg.com/@react-native-community/masked-view/-/masked-view-0.1.11.tgz#2f4c6e10bee0786abff4604e39a37ded6f3980ce"
@@ -4777,10 +4772,10 @@ expo-updates-interface@~0.9.0:
resolved "https://registry.yarnpkg.com/expo-updates-interface/-/expo-updates-interface-0.9.1.tgz#e81308d551ed5a4c35c8770ac61434f6ca749610"
integrity sha512-wk88LLhseQ7LJvxdN7BTKiryyqALxnrvr+lyHK3/prg76Yy0EGi2Q/oE/rtFyyZ1JmQDRbO/5pdX0EE6QqVQXQ==
expo-updates@^0.16.3:
version "0.16.3"
resolved "https://registry.yarnpkg.com/expo-updates/-/expo-updates-0.16.3.tgz#190f5896f98db2e130b608d61c8359ee4b2c2125"
integrity sha512-uFr2Fvq7IbKwz9xEqWE9GNEs0sBAd6uiUI9McTCKw4BzKhjylRbPAN3zewc7MGLOvhTwBASva79VLQVgzdoBRw==
expo-updates@^0.16.4:
version "0.16.4"
resolved "https://registry.yarnpkg.com/expo-updates/-/expo-updates-0.16.4.tgz#6d05438cf7304add03645a598211ac4ef3cc4f64"
integrity sha512-hEUotP10sBiYn6dvkYC2rIa+kAmsBuaMp32sIVNAYEwKMQJqEwqNAKTU6CpJ4Aoc//BYL2Hv8qNo/UsT4rATRg==
dependencies:
"@expo/code-signing-certificates" "0.0.5"
"@expo/config" "~8.0.0"
@@ -6604,16 +6599,6 @@ merge2@^1.3.0, merge2@^1.4.1:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
metro-babel-transformer@0.73.8:
version "0.73.8"
resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.73.8.tgz#521374cb9234ba126f3f8d63588db5901308b4ed"
integrity sha512-GO6H/W2RjZ0/gm1pIvdO9EP34s3XN6kzoeyxqmfqKfYhJmYZf1SzXbyiIHyMbJNwJVrsKuHqu32+GopTlKscWw==
dependencies:
"@babel/core" "^7.20.0"
hermes-parser "0.8.0"
metro-source-map "0.73.8"
nullthrows "^1.1.1"
metro-babel-transformer@0.73.9:
version "0.73.9"
resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.73.9.tgz#bec8aaaf1bbdc2e469fde586fde455f8b2a83073"
@@ -6795,19 +6780,6 @@ metro-react-native-babel-preset@0.73.9:
"@babel/template" "^7.0.0"
react-refresh "^0.4.0"
metro-react-native-babel-transformer@0.73.8:
version "0.73.8"
resolved "https://registry.yarnpkg.com/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.73.8.tgz#cbcd4b243216878431dc4311ce46f02a928e3991"
integrity sha512-oH/LCCJPauteAE28c0KJAiSrkV+1VJbU0PwA9UwaWnle+qevs/clpKQ8LrIr33YbBj4CiI1kFoVRuNRt5h4NFg==
dependencies:
"@babel/core" "^7.20.0"
babel-preset-fbjs "^3.4.0"
hermes-parser "0.8.0"
metro-babel-transformer "0.73.8"
metro-react-native-babel-preset "0.73.8"
metro-source-map "0.73.8"
nullthrows "^1.1.1"
metro-react-native-babel-transformer@0.73.9:
version "0.73.9"
resolved "https://registry.yarnpkg.com/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.73.9.tgz#4f4f0cfa5119bab8b53e722fabaf90687d0cbff0"
@@ -6828,14 +6800,6 @@ metro-resolver@0.73.9:
dependencies:
absolute-path "^0.0.0"
metro-runtime@0.73.8:
version "0.73.8"
resolved "https://registry.yarnpkg.com/metro-runtime/-/metro-runtime-0.73.8.tgz#dadae7c154fbbde24390cf7f7e7d934a2768cd18"
integrity sha512-M+Bg9M4EN5AEpJ8NkiUsawD75ifYvYfHi05w6QzHXaqOrsTeaRbbeLuOGCYxU2f/tPg17wQV97/rqUQzs9qEtA==
dependencies:
"@babel/runtime" "^7.0.0"
react-refresh "^0.4.0"
metro-runtime@0.73.9:
version "0.73.9"
resolved "https://registry.yarnpkg.com/metro-runtime/-/metro-runtime-0.73.9.tgz#0b24c0b066b8629ee855a6e5035b65061fef60d5"
@@ -6844,20 +6808,6 @@ metro-runtime@0.73.9:
"@babel/runtime" "^7.0.0"
react-refresh "^0.4.0"
metro-source-map@0.73.8:
version "0.73.8"
resolved "https://registry.yarnpkg.com/metro-source-map/-/metro-source-map-0.73.8.tgz#5134174e3d43de26ad331b95f637944c6547d441"
integrity sha512-wozFXuBYMAy7b8BCYwC+qoXsvayVJBHWtSTlSLva99t+CoUSG9JO9kg1umzbOz28YYPxKmvb/wbnLMkHdas2cA==
dependencies:
"@babel/traverse" "^7.20.0"
"@babel/types" "^7.20.0"
invariant "^2.2.4"
metro-symbolicate "0.73.8"
nullthrows "^1.1.1"
ob1 "0.73.8"
source-map "^0.5.6"
vlq "^1.0.0"
metro-source-map@0.73.9:
version "0.73.9"
resolved "https://registry.yarnpkg.com/metro-source-map/-/metro-source-map-0.73.9.tgz#89ca41f6346aeb12f7f23496fa363e520adafebe"
@@ -6872,18 +6822,6 @@ metro-source-map@0.73.9:
source-map "^0.5.6"
vlq "^1.0.0"
metro-symbolicate@0.73.8:
version "0.73.8"
resolved "https://registry.yarnpkg.com/metro-symbolicate/-/metro-symbolicate-0.73.8.tgz#96920f607bce484283d822ee5fe18d932f69c03d"
integrity sha512-xkBAcceYYp0GGdCCuMzkCF1ejHsd0lYlbKBkjSRgM0Nlj80VapPaSwumYoAvSaDxcbkvS7/sCjURGp5DsSFgRQ==
dependencies:
invariant "^2.2.4"
metro-source-map "0.73.8"
nullthrows "^1.1.1"
source-map "^0.5.6"
through2 "^2.0.1"
vlq "^1.0.0"
metro-symbolicate@0.73.9:
version "0.73.9"
resolved "https://registry.yarnpkg.com/metro-symbolicate/-/metro-symbolicate-0.73.9.tgz#cb452299a36e5b86b2826e7426d51221635c48bf"
@@ -7326,11 +7264,6 @@ number-is-nan@^1.0.0:
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
integrity sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==
ob1@0.73.8:
version "0.73.8"
resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.73.8.tgz#c569f1a15ce2d04da6fd70293ad44b5a93b11978"
integrity sha512-1F7j+jzD+edS6ohQP7Vg5f3yiIk5i3x1uLrNIHOmLHWzWK1t3zrDpjnoXghccdVlsU+UjbyURnDynm4p0GgXeA==
ob1@0.73.9:
version "0.73.9"
resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.73.9.tgz#d5677a0dd3e2f16ad84231278d79424436c38c59"
@@ -8011,6 +7944,13 @@ react-native-draggable-flatlist@^3.1.2:
resolved "https://registry.yarnpkg.com/react-native-draggable-flatlist/-/react-native-draggable-flatlist-3.1.2.tgz#b66538007f645ccd851c729d4b8a8f7e07666d3f"
integrity sha512-cqBR+lZktTqHY/J7V0aq+TZNkuqeILxwZ5bBXKndokwH4qvZl7ifijM7LlfWhfhU1/pzjketTsX7P2zyojfAaQ==
react-native-element-dropdown@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/react-native-element-dropdown/-/react-native-element-dropdown-2.9.0.tgz#c218ab512bd8be69fa0ac70462293863f8cfd527"
integrity sha512-2Y/bmoFPTdKOUXbYloEKpMTWYE2VHrvva/5tgEuHqAlfAtGGr3r/ob0L0YYH1am5N0n1PKo3+eLf0c79QUSzYw==
dependencies:
lodash "^4.17.21"
react-native-gesture-handler@^2.4.0, react-native-gesture-handler@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.9.0.tgz#2f63812e523c646f25b9ad660fc6f75948e51241"
@@ -8022,7 +7962,7 @@ react-native-gesture-handler@^2.4.0, react-native-gesture-handler@^2.9.0:
lodash "^4.17.21"
prop-types "^15.7.2"
react-native-gradle-plugin@^0.71.16:
react-native-gradle-plugin@^0.71.17:
version "0.71.17"
resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.71.17.tgz#cf780a27270f0a32dca8184eff91555d7627dd00"
integrity sha512-OXXYgpISEqERwjSlaCiaQY6cTY5CH6j73gdkWpK0hedxtiWMWgH+i5TOi4hIGYitm9kQBeyDu+wim9fA8ROFJA==
@@ -8053,6 +7993,13 @@ react-native-iphone-x-helper@^1.3.1:
resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010"
integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg==
react-native-modal-datetime-picker@^15.0.0:
version "15.0.0"
resolved "https://registry.yarnpkg.com/react-native-modal-datetime-picker/-/react-native-modal-datetime-picker-15.0.0.tgz#3c2b0a63467a3391dbc202871aa2807bc1a0d8d0"
integrity sha512-cHeFEYHUhyIk+Mt9C6RVseg/VMGR4XcxdU9SibF5RMCXiXhrwMkFy7203xg1S331pzCF/Oqhvi4Jh0pYMrTFtQ==
dependencies:
prop-types "^15.7.2"
react-native-pager-view@6.1.2:
version "6.1.2"
resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.1.2.tgz#3522079b9a9d6634ca5e8d153bc0b4d660254552"
@@ -8139,15 +8086,15 @@ react-native-web@~0.18.9:
postcss-value-parser "^4.2.0"
styleq "^0.1.2"
react-native@^0.71.4:
version "0.71.4"
resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.71.4.tgz#f03f600efe68f745d19454ab17f9c1a9ef304790"
integrity sha512-3hSYqvWrOdKhpV3HpEKp1/CkWx8Sr/N/miCrmUIAsVTSJUR7JW0VvIsrV9urDhUj/s6v2WF4n7qIEEJsmTCrPw==
react-native@0.71.6:
version "0.71.6"
resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.71.6.tgz#e8f07baf55abd1015eaa7040ceaa4aa632c2c04f"
integrity sha512-gHrDj7qaAaiE41JwaFCh3AtvOqOLuRgZtHKzNiwxakG/wvPAYmG73ECfWHGxjxIx/QT17Hp37Da3ipCei/CayQ==
dependencies:
"@jest/create-cache-key-function" "^29.2.1"
"@react-native-community/cli" "10.2.0"
"@react-native-community/cli" "10.2.2"
"@react-native-community/cli-platform-android" "10.2.0"
"@react-native-community/cli-platform-ios" "10.2.0"
"@react-native-community/cli-platform-ios" "10.2.1"
"@react-native/assets" "1.0.0"
"@react-native/normalize-color" "2.1.0"
"@react-native/polyfills" "2.0.0"
@@ -8160,16 +8107,16 @@ react-native@^0.71.4:
jest-environment-node "^29.2.1"
jsc-android "^250231.0.0"
memoize-one "^5.0.0"
metro-react-native-babel-transformer "0.73.8"
metro-runtime "0.73.8"
metro-source-map "0.73.8"
metro-react-native-babel-transformer "0.73.9"
metro-runtime "0.73.9"
metro-source-map "0.73.9"
mkdirp "^0.5.1"
nullthrows "^1.1.1"
pretty-format "^26.5.2"
promise "^8.3.0"
react-devtools-core "^4.26.1"
react-native-codegen "^0.71.5"
react-native-gradle-plugin "^0.71.16"
react-native-gradle-plugin "^0.71.17"
react-refresh "^0.4.0"
react-shallow-renderer "^16.15.0"
regenerator-runtime "^0.13.2"