diff --git a/components/Buttons/create-time-ticket-button.component.jsx b/components/Buttons/create-time-ticket-button.component.jsx new file mode 100644 index 0000000..2c5da22 --- /dev/null +++ b/components/Buttons/create-time-ticket-button.component.jsx @@ -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 ( + + ); +} +export default connect(null, null)(AddTimeTicketButton); \ No newline at end of file diff --git a/components/Buttons/employee-sign-out-button.component.jsx b/components/Buttons/employee-sign-out-button.component.jsx new file mode 100644 index 0000000..33661be --- /dev/null +++ b/components/Buttons/employee-sign-out-button.component.jsx @@ -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 ( + + ); +} +export default connect(null, mapDispatchToProps)(SignOutButton); diff --git a/components/Modals/JobSearchAndSelectModal.jsx b/components/Modals/JobSearchAndSelectModal.jsx new file mode 100644 index 0000000..2b55159 --- /dev/null +++ b/components/Modals/JobSearchAndSelectModal.jsx @@ -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 ; + 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 ( + <> + + + + + + + + } + data={data?.search_jobs} + keyExtractor={(item) => item.id} + renderItem={(object) => ( + { + jobSrchOnSetCurrentValue(object.item); + hideModal(); + setSearchText(""); + }} + left={() => { + if (object.item.id !== jobSrchCurrentValue?.id) return null; + return ( + + ); + }} + 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={ + + {t("selectjobid.labels.nodata")} + + } + /> + + + + + ); +} + +export default connect(null, null)(JobSearchAndSelectModal); diff --git a/components/Selects/select-cost-center.jsx b/components/Selects/select-cost-center.jsx new file mode 100644 index 0000000..d22e95e --- /dev/null +++ b/components/Selects/select-cost-center.jsx @@ -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 ( + + setIsFocus(true)} + onBlur={() => setIsFocus(false)} + data={costCenters} + value={props.currentValue?.value} + onChange={(item) => { + props.onValueSelected(item); + setIsFocus(false); + }} + /> + + ); +} + +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, + }, +}); diff --git a/components/Selects/select-job-id.jsx b/components/Selects/select-job-id.jsx new file mode 100644 index 0000000..92451ce --- /dev/null +++ b/components/Selects/select-job-id.jsx @@ -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 ( + + 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} */} + + ); +} + +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, + }, +}); diff --git a/components/error-display/error-display.component.jsx b/components/error-display/error-display.component.jsx index 84f41b6..725aac9 100644 --- a/components/error-display/error-display.component.jsx +++ b/components/error-display/error-display.component.jsx @@ -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 ( - - {errorMessage} + + {errorMessage} + {/* {errorMessage} */} ); } +const localStyles = StyleSheet.create({ + alert: { + color: "red", + textAlign: "center", + margin: 8, + padding: 8, + }, +}); diff --git a/components/job-list-item/job-list-item.component.jsx b/components/job-list-item/job-list-item.component.jsx index cb3b4e5..a91856f 100644 --- a/components/job-list-item/job-list-item.component.jsx +++ b/components/job-list-item/job-list-item.component.jsx @@ -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"); }} diff --git a/components/keyboards/KeyboardAvoidingComponent.js b/components/keyboards/KeyboardAvoidingComponent.js new file mode 100644 index 0000000..d7e1107 --- /dev/null +++ b/components/keyboards/KeyboardAvoidingComponent.js @@ -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 ( + + {children} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + inner: { + flex: 1, + }, +}); + +export default KeyboardAvoidingComponent; diff --git a/components/labor-allocations-table/labor-allocations-table.component.jsx b/components/labor-allocations-table/labor-allocations-table.component.jsx new file mode 100644 index 0000000..08bca5b --- /dev/null +++ b/components/labor-allocations-table/labor-allocations-table.component.jsx @@ -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 ; + + 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 ; + return ( + + {typeof data !== "undefined" ? ( + + + + + {t("laborallocations.labels.costcenter")} + {t("laborallocations.labels.hourstotal")} + {t("laborallocations.labels.hoursclaimed")} + {/* Hours Claimed */} + {t("laborallocations.labels.adjustments")} + {t("laborallocations.labels.difference")} + + + + item.cost_center} + ItemSeparatorComponent={} + renderItem={(object) => ( + + + + {object.item.cost_center} {" ("} + {object.item.mod_lbr_ty} + {")"} + + + + + {object.item.total && object.item.total.toFixed(1)} + + + + + {object.item.claimed && object.item.claimed.toFixed(1)} + + + + + {object.item.adjustments && + object.item.adjustments.toFixed(1)} + + + + = 0 ? "green" : "red", + }} + > + {_.round(object.item.difference, 1)} + + + + )} + /> + {summary && ( + + {t("laborallocations.labels.totals")} + + {summary.hrs_total.toFixed(1)} + + + {summary.hrs_claimed.toFixed(1)} + + + {summary.adjustments.toFixed(1)} + + + {summary.difference.toFixed(1)} + + + )} + + + + + ) : null} + {/* use "totals" for the rows in the table */} + {/* use "summary" for the totals at the bottom */} + + ); +} + +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); diff --git a/components/owner-name-display/owner-name-display.component.jsx b/components/owner-name-display/owner-name-display.component.jsx new file mode 100644 index 0000000..da79657 --- /dev/null +++ b/components/owner-name-display/owner-name-display.component.jsx @@ -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(); +} diff --git a/components/screen-employee-sign-in/screen-employee-sign-in.component.jsx b/components/screen-employee-sign-in/screen-employee-sign-in.component.jsx new file mode 100644 index 0000000..4f29785 --- /dev/null +++ b/components/screen-employee-sign-in/screen-employee-sign-in.component.jsx @@ -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 ( + + + + {({ handleChange, handleBlur, handleSubmit, values }) => { +const signingErrorMsg = signingError ? () : null; + return( + + + + {signingErrorMsg} + + + ) +}} + + + + ); +} + +const localStyles = StyleSheet.create({ + content: { + display: "flex", + flex: 1, + }, + signInContainer: { + flex: 1, + }, + input: { + margin: 12, + }, +}); + +export default connect(mapStateToProps, mapDispatchToProps)(EmployeeSignIn); diff --git a/components/screen-main/screen-main.component.jsx b/components/screen-main/screen-main.component.jsx index 46822e3..8b97379 100644 --- a/components/screen-main/screen-main.component.jsx +++ b/components/screen-main/screen-main.component.jsx @@ -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 = () => ( ); +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 ( + + {currentEmployee === null ? ( + ({ title: i18n.t("employeesignin.titles.signin") })} + component={EmployeeSignIn} + /> + ) : currentEmployee.authorized ? ( + <> + + ({ + title: i18n.t("createtimeticket.titles.createtimeticket"), + })} + component={ScreenTimeTicketCreate} + /> + ({ + title: i18n.t("timeticketclockoff.titles.clockoff"), + })} + component={ScreenTimeTicketClockoffComponent} + /> + + ) : ( + ({ + title: i18n.t("employeesignin.titles.signin"), + })} + component={EmployeeSignIn} + /> + )} + + ); +}); + const BottomTabsNavigator = () => ( ({ @@ -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} /> + ({ + 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: , + })); + return; + } + if (errorATT) { + setItemState((itemState) => ({ + ...itemState, + content: , + })); + return; + } + + return ( + + + + } + /> + + {currentEmployeeFullName && ( + {currentEmployeeFullName} + )} + + + } + data={!!itemState ? [itemState] : null} + renderItem={({ item }) => ( + + )} + ListFooterComponent={ + + ( + + + {/* */} + + )} + /> + + + + {error && error?.message ? ( + + ) : null} + + + + + + } + refreshControl={ + + } + ListEmptyComponent={ + + {t("timeticketbrowser.labels.nodata")} + + } + /> + + + ); +} + +const MyItem = ({ itemState, style }) => { + const { t } = useTranslation(); + const items = itemState?.data; + return ( + + + + {!!items ? ( + items.map((item) => ) + ) : !!itemState?.content ? ( + itemState.content + ) : ( + {t("timeticketbrowser.labels.nodata")} + )} + + + ); +}; + +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); diff --git a/components/sign-out-button/sign-out-button.component.jsx b/components/sign-out-button/sign-out-button.component.jsx index 0baa8b8..49c4d1b 100644 --- a/components/sign-out-button/sign-out-button.component.jsx +++ b/components/sign-out-button/sign-out-button.component.jsx @@ -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 ( + + + ); +} + +export default connect(null, mapDispatchToProps)(ClockedinListItem); diff --git a/components/time-ticket-lists/employee-clockedin-list.component.jsx b/components/time-ticket-lists/employee-clockedin-list.component.jsx new file mode 100644 index 0000000..34de696 --- /dev/null +++ b/components/time-ticket-lists/employee-clockedin-list.component.jsx @@ -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 ; + if (error) return ; + + const onRefresh = async () => { + // console.info("EmployeeClockedInList, onRefresh."); + return refetch(); + }; + + return ( + + {jobData ? ( + + + {t("employeeclockedinlist.labels.alreadyclockedon")} + + + } + renderItem={(object) => ( + + )} + // setTmTicketJobId={setTmTicketJobId} + /> + + ) : null} + + //
+ // {data.timetickets.length > 0 ? ( + //
+ // + // {t("timetickets.labels.alreadyclockedon")} + // + // ( + // + // + // {`${ + // ticket.job.ro_number || t("general.labels.na") + // } ${OwnerNameDisplayFunction(ticket.job)}`} + // + // } + // actions={[ + // , + // ]} + // > + //
+ // {` + // ${ticket.job.v_model_yr || ""} ${ + // ticket.job.v_make_desc || "" + // } ${ticket.job.v_model_desc || ""}`} + //
+ // + // {ticket.clockon} + // + // + // {ticket.cost_center === "timetickets.labels.shift" + // ? t(ticket.cost_center) + // : ticket.cost_center} + // + //
+ //
+ // )} + // >
+ //
+ // ) : null} + //
+ ); +} + +export default connect(mapStateToProps, null)(EmployeeClockedInList); diff --git a/components/time-ticket/screen-time-ticket-clockoff.component.jsx b/components/time-ticket/screen-time-ticket-clockoff.component.jsx new file mode 100644 index 0000000..ad3c424 --- /dev/null +++ b/components/time-ticket/screen-time-ticket-clockoff.component.jsx @@ -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 ( + + + + + {({ handleChange, handleBlur, handleSubmit, values }) => ( + + + + + {error ? ( + + ) : null} + + + + + )} + + + + } + data={null} + renderItem={null} + ListFooterComponent={ + + } + refreshControl={ + + } + /> + + ); +} + +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, + }, +}); diff --git a/components/time-ticket/screen-time-ticket-create.component.jsx b/components/time-ticket/screen-time-ticket-create.component.jsx new file mode 100644 index 0000000..f289e3e --- /dev/null +++ b/components/time-ticket/screen-time-ticket-create.component.jsx @@ -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 ( + + + + + {({ handleChange, handleBlur, handleSubmit, values }) => ( + + + + + + + + + {error ? ( + + ) : null} + + + + + + )} + + + + } + data={null} + renderItem={null} + ListFooterComponent={ + + } + refreshControl={ + + } + /> + + ); +} + +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, + }, +}); diff --git a/graphql/bodyshop.queries.js b/graphql/bodyshop.queries.js index eaf877f..ad6c226 100644 --- a/graphql/bodyshop.queries.js +++ b/graphql/bodyshop.queries.js @@ -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 } } `; diff --git a/graphql/employees.queries.js b/graphql/employees.queries.js new file mode 100644 index 0000000..8cd59b7 --- /dev/null +++ b/graphql/employees.queries.js @@ -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 + } + } +`; \ No newline at end of file diff --git a/graphql/jobs.queries.js b/graphql/jobs.queries.js index bd4b017..22adc17 100644 --- a/graphql/jobs.queries.js +++ b/graphql/jobs.queries.js @@ -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, diff --git a/graphql/timetickets.queries.js b/graphql/timetickets.queries.js new file mode 100644 index 0000000..1b65d26 --- /dev/null +++ b/graphql/timetickets.queries.js @@ -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 + } + } + } +`; \ No newline at end of file diff --git a/package.json b/package.json index 6ee0ba9..4fc229f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/redux/app/app.actions.js b/redux/app/app.actions.js index 1aeb536..551ad03 100644 --- a/redux/app/app.actions.js +++ b/redux/app/app.actions.js @@ -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, +}); diff --git a/redux/app/app.reducer.js b/redux/app/app.reducer.js index aa15d0c..02f5dfe 100644 --- a/redux/app/app.reducer.js +++ b/redux/app/app.reducer.js @@ -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; } diff --git a/redux/app/app.selectors.js b/redux/app/app.selectors.js index 4626679..15ec19a 100644 --- a/redux/app/app.selectors.js +++ b/redux/app/app.selectors.js @@ -26,3 +26,8 @@ export const selectDeleteAfterUpload = createSelector( [selectApp], (app) => app.deleteAfterUpload ); + +export const selectCurrentTmTicketJobId = createSelector( + [selectApp], + (app) => app.tmTicketJobId +); diff --git a/redux/app/app.types.js b/redux/app/app.types.js index 029d8e0..d2fd50b 100644 --- a/redux/app/app.types.js +++ b/redux/app/app.types.js @@ -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; diff --git a/redux/employee/employee.actions.js b/redux/employee/employee.actions.js new file mode 100644 index 0000000..4de1cde --- /dev/null +++ b/redux/employee/employee.actions.js @@ -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, +}); + diff --git a/redux/employee/employee.reducer.js b/redux/employee/employee.reducer.js new file mode 100644 index 0000000..cbb73bd --- /dev/null +++ b/redux/employee/employee.reducer.js @@ -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; diff --git a/redux/employee/employee.sagas.js b/redux/employee/employee.sagas.js new file mode 100644 index 0000000..768a543 --- /dev/null +++ b/redux/employee/employee.sagas.js @@ -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)]); +} diff --git a/redux/employee/employee.selectors.js b/redux/employee/employee.selectors.js new file mode 100644 index 0000000..1818289 --- /dev/null +++ b/redux/employee/employee.selectors.js @@ -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; + } + } +); diff --git a/redux/employee/employee.types.js b/redux/employee/employee.types.js new file mode 100644 index 0000000..44fd208 --- /dev/null +++ b/redux/employee/employee.types.js @@ -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; diff --git a/redux/root.reducer.js b/redux/root.reducer.js index 8ef298d..cb537cf 100644 --- a/redux/root.reducer.js +++ b/redux/root.reducer.js @@ -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); diff --git a/redux/root.saga.js b/redux/root.saga.js index b2e2da1..ff89eab 100644 --- a/redux/root.saga.js +++ b/redux/root.saga.js @@ -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),]); } diff --git a/redux/store.js b/redux/store.js index f4aed43..acaab0c 100644 --- a/redux/store.js +++ b/redux/store.js @@ -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. diff --git a/redux/timetickets/timetickets.actions.js b/redux/timetickets/timetickets.actions.js new file mode 100644 index 0000000..3de6263 --- /dev/null +++ b/redux/timetickets/timetickets.actions.js @@ -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, +}); diff --git a/redux/timetickets/timetickets.reducer.js b/redux/timetickets/timetickets.reducer.js new file mode 100644 index 0000000..377c355 --- /dev/null +++ b/redux/timetickets/timetickets.reducer.js @@ -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; diff --git a/redux/timetickets/timetickets.sagas.js b/redux/timetickets/timetickets.sagas.js new file mode 100644 index 0000000..16b925f --- /dev/null +++ b/redux/timetickets/timetickets.sagas.js @@ -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), + ]); +} diff --git a/redux/timetickets/timetickets.selectors.js b/redux/timetickets/timetickets.selectors.js new file mode 100644 index 0000000..335e21e --- /dev/null +++ b/redux/timetickets/timetickets.selectors.js @@ -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 +); + diff --git a/redux/timetickets/timetickets.types.js b/redux/timetickets/timetickets.types.js new file mode 100644 index 0000000..db91800 --- /dev/null +++ b/redux/timetickets/timetickets.types.js @@ -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; diff --git a/redux/user/user.selectors.js b/redux/user/user.selectors.js index b6b762a..fe3fdec 100644 --- a/redux/user/user.selectors.js +++ b/redux/user/user.selectors.js @@ -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; + } +); diff --git a/translations/en-US/common.json b/translations/en-US/common.json index 153ab06..878061d 100644 --- a/translations/en-US/common.json +++ b/translations/en-US/common.json @@ -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" + } } } } diff --git a/translations/es-MX/common.json b/translations/es-MX/common.json index f4a1a3f..606906b 100644 --- a/translations/es-MX/common.json +++ b/translations/es-MX/common.json @@ -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": "" + } } } } diff --git a/translations/fr-CA/common.json b/translations/fr-CA/common.json index 54de618..9f4c1c2 100644 --- a/translations/fr-CA/common.json +++ b/translations/fr-CA/common.json @@ -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": "" + } } } } diff --git a/util/CleanAxios.js b/util/CleanAxios.js index de36f24..4c15ca2 100644 --- a/util/CleanAxios.js +++ b/util/CleanAxios.js @@ -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) => { diff --git a/util/DateFormater.jsx b/util/DateFormater.jsx new file mode 100644 index 0000000..f5687d9 --- /dev/null +++ b/util/DateFormater.jsx @@ -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; + } \ No newline at end of file diff --git a/util/labor-allocations-table.utility.js b/util/labor-allocations-table.utility.js new file mode 100644 index 0000000..a2c4f88 --- /dev/null +++ b/util/labor-allocations-table.utility.js @@ -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; +}; diff --git a/util/timer.js b/util/timer.js new file mode 100644 index 0000000..5106e48 --- /dev/null +++ b/util/timer.js @@ -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 ( + <> + + {time} + + + + + + ); +} + + 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); diff --git a/yarn.lock b/yarn.lock index a1bd05d..7b1b330 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"