Merge pull request #5 from snaptsoft/dev-patrick

UNSTABLE MERGE INTO DEV - Job Lines page is not functional.
This commit is contained in:
2020-01-29 21:19:17 -08:00
committed by GitHub
306 changed files with 21323 additions and 994 deletions

View File

@@ -7,3 +7,6 @@ ALL CHANGES MUST BE MADE USING LOCAL CONSOLE TO ENSURE DATABASE MIGRATION FILES
To Start Hasura CLI:
npx hasura console --admin-secret Dev-BodyShopAppBySnaptSoftware!
Migrating to Staging:
npx hasura migrate apply --up 10 --endpoint https://bodyshop-staging-db.herokuapp.com/ --admin-secret Staging-BodyShopAppBySnaptSoftware!

View File

@@ -5,4 +5,4 @@
1. Get a presigned URL by hitting our own express server with a unique key.
2. Use this presigned URL to upload an individual file.
3. Store the key + the bucket name to the documents record.
4. ???Figure out how to add thumbnails.
4.

File diff suppressed because it is too large Load Diff

View File

@@ -54,6 +54,7 @@
]
},
"devDependencies": {
"@apollo/react-testing": "^3.1.3",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2"
}

View File

@@ -109,7 +109,6 @@ class AppContainer extends Component {
});
try {
// See above for additional options, including other storage providers.
await persistCache({
cache,
storage: window.sessionStorage,
@@ -127,6 +126,8 @@ class AppContainer extends Component {
//Init local state.
}
componentWillUnmount() {}
render() {
const { client, loaded } = this.state;

View File

@@ -1 +1,27 @@
@import '~antd/dist/antd.css';
@import "~antd/dist/antd.css";
/* .ant-layout-header {
position: absolute;
top: 0px;
left: 0px;
height: 5vh;
right: 0px;
overflow: hidden;
}
.ant-layout-content {
position: absolute;
top: 5vh;
bottom: 3vh;
left: 0px;
right: 0px;
overflow: auto;
}
.ant-layout-footer {
position: absolute;
bottom: 0px;
height: 3vh;
left: 0px;
right: 0px;
overflow: hidden;
} */

View File

@@ -1,7 +1,8 @@
import React, { useEffect, Suspense, lazy } from "react";
import React, { useEffect, Suspense, lazy, useState } from "react";
import { useApolloClient, useQuery } from "@apollo/react-hooks";
import { Switch, Route, Redirect } from "react-router-dom";
import firebase from "../firebase/firebase.utils";
import i18next from "i18next";
import "./App.css";
@@ -12,7 +13,7 @@ import ErrorBoundary from "../components/error-boundary/error-boundary.component
import { auth } from "../firebase/firebase.utils";
import { UPSERT_USER } from "../graphql/user.queries";
import { GET_CURRENT_USER } from "../graphql/local.queries";
import { GET_CURRENT_USER, GET_LANGUAGE } from "../graphql/local.queries";
// import { QUERY_BODYSHOP } from "../graphql/bodyshop.queries";
import PrivateRoute from "../utils/private-route";
@@ -26,10 +27,12 @@ const Unauthorized = lazy(() =>
export default () => {
const apolloClient = useApolloClient();
const [loaded, setloaded] = useState(false);
useEffect(() => {
//Run the auth code only on the first render.
const unsubscribeFromAuth = auth.onAuthStateChanged(async user => {
console.log("Auth State Changed.");
setloaded(true);
if (user) {
let token;
token = await user.getIdToken();
@@ -86,16 +89,30 @@ export default () => {
unsubscribeFromAuth();
};
}, [apolloClient]);
const HookCurrentUser = useQuery(GET_CURRENT_USER);
if (HookCurrentUser.loading) return <LoadingSpinner />;
if (HookCurrentUser.error)
return <AlertComponent message={HookCurrentUser.error.message} />;
const HookLanguage = useQuery(GET_LANGUAGE);
if (!loaded) return <LoadingSpinner />;
if (HookCurrentUser.loading || HookLanguage.loading)
return <LoadingSpinner />;
if (HookCurrentUser.error || HookLanguage.error)
return (
<AlertComponent
message={HookCurrentUser.error.message || HookLanguage.error.message}
/>
);
if (HookLanguage.data.language)
i18next.changeLanguage(HookLanguage.data.language, (err, t) => {
if (err)
return console.log("Error encountered when changing languages.", err);
});
return (
<div>
<Switch>
<ErrorBoundary>
<Suspense fallback={<div>TODO: Suspense Loading</div>}>
<Suspense fallback={<LoadingSpinner />}>
<Route exact path='/' component={LandingPage} />
<Route exact path='/unauthorized' component={Unauthorized} />
<Route

View File

@@ -1,9 +1,18 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import React from "react";
import ReactDOM from "react-dom";
import App from "./App.container";
import { MockedProvider } from "@apollo/react-testing";
const div = document.createElement("div");
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
it("renders without crashing", () => {
ReactDOM.render(
<MockedProvider>
<App />
</MockedProvider>,
div
);
});
it("unmounts without crashing", () => {
ReactDOM.unmountComponentAtNode(div);
});

View File

@@ -0,0 +1,11 @@
import React from "react";
import ReactDOM from "react-dom";
import Alert from "./alert.component";
import { MockedProvider } from "@apollo/react-testing";
import { shallow } from "enzyme";
const div = document.createElement("div");
it("renders without crashing", () => {
shallow(<Alert type="error" />);
});

View File

@@ -0,0 +1,5 @@
import React from "react";
export default function ChatWindowComponent() {
return <div>Chat Windows and more</div>;
}

View File

@@ -0,0 +1,18 @@
import React, { useState } from "react";
import ChatWindowComponent from "./chat-window.component";
import { Button } from "antd";
export default function ChatWindowContainer() {
const [visible, setVisible] = useState(false);
return (
<div style={{ position: "absolute", zIndex: 1000 }}>
{visible ? <ChatWindowComponent /> : null}
<Button
onClick={() => {
setVisible(!visible);
}}>
Open!
</Button>
</div>
);
}

View File

@@ -1,28 +1,33 @@
import React from "react";
import { Link } from "react-router-dom";
import { Dropdown, Menu, Icon, Avatar, Row, Col } from "antd";
import { useTranslation } from "react-i18next";
import { useApolloClient, useQuery } from "@apollo/react-hooks";
import { Avatar, Col, Dropdown, Icon, Menu, Row } from "antd";
import i18next from "i18next";
import { useQuery } from "@apollo/react-hooks";
import { GET_CURRENT_USER } from "../../graphql/local.queries";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import UserImage from "../../assets/User.svg";
import { GET_CURRENT_USER } from "../../graphql/local.queries";
import AlertComponent from "../alert/alert.component";
import SignOut from "../sign-out/sign-out.component";
export default function CurrentUserDropdown() {
const { t } = useTranslation();
const { loading, error, data } = useQuery(GET_CURRENT_USER);
const client = useApolloClient();
const handleMenuClick = e => {
console.log("e", e);
if (e.item.props.actiontype === "lang-select") {
i18next.changeLanguage(e.key, (err, t) => {
if (err)
return console.log("Error encountered when changing languages.", err);
client.writeData({ data: { language: e.key } });
});
}
};
const menu = (
<Menu mode='vertical' onClick={handleMenuClick}>
<Menu.Item>
<SignOut />
</Menu.Item>
<Menu.Item>
<Link to='/manage/profile'> {t("menus.currentuser.profile")}</Link>
</Menu.Item>
@@ -47,7 +52,7 @@ export default function CurrentUserDropdown() {
);
if (loading) return null;
if (error) return <AlertComponent message={error.message} />;
if (error) return <AlertComponent message={error.message} type='error' />;
const { currentUser } = data;
@@ -57,10 +62,8 @@ export default function CurrentUserDropdown() {
<Col span={8}>
<Avatar size='large' alt='Avatar' src={UserImage} />
</Col>
<Col span={16}>
<Link to='/manage/profile'>
{currentUser?.displayName ?? t("general.labels.unknown")}
</Link>
<Col span={16} style={{ color: "white" }}>
{currentUser.displayName || t("general.labels.unknown")}
</Col>
</Row>
</Dropdown>

View File

@@ -1,5 +1,5 @@
import { Col, Row } from "antd";
import React from "react";
import { Row, Col } from "antd";
export default function FooterComponent() {
return (

View File

@@ -0,0 +1,16 @@
import { Icon, Input } from "antd";
import React, { forwardRef } from "react";
function FormItemEmail(props, ref) {
return (
<Input
{...props}
addonAfter={
<a href={`mailto:${props.email}`}>
<Icon type="mail" />
</a>
}
/>
);
}
export default forwardRef(FormItemEmail);

View File

@@ -1,47 +1,65 @@
import React from "react";
import { Link } from "react-router-dom";
import { useApolloClient } from "@apollo/react-hooks";
import { Menu, Icon, Row, Col } from "antd";
import "./header.styles.scss";
import SignOut from "../sign-out/sign-out.component";
import ManageSignInButton from "../manage-sign-in-button/manage-sign-in-button.component";
import GlobalSearch from "../global-search/global-search.component";
import { Col, Icon, Menu, Row } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import CurrentUserDropdown from "../current-user-dropdown/current-user-dropdown.component";
import GlobalSearch from "../global-search/global-search.component";
import ManageSignInButton from "../manage-sign-in-button/manage-sign-in-button.component";
import "./header.styles.scss";
export default ({ landingHeader, navItems, selectedNavItem }) => {
const apolloClient = useApolloClient();
const { t } = useTranslation();
const handleClick = e => {
apolloClient.writeData({ data: { selectedNavItem: e.key } });
};
return (
<Row type="flex" justify="space-around">
<Row type='flex' justify='space-around'>
<Col span={16}>
<Menu
theme="dark"
className="header"
theme='dark'
className='header'
onClick={handleClick}
selectedKeys={selectedNavItem}
mode="horizontal"
>
mode='horizontal'>
<Menu.Item>
<GlobalSearch />
</Menu.Item>
{navItems.map(navItem => (
<Menu.Item key={navItem.title}>
<Link to={navItem.path}>
{navItem.icontype ? <Icon type={navItem.icontype} /> : null}
{navItem.title}
<Menu.Item key='home'>
<Link to='/manage'>
<Icon type='home' />
{t("menus.header.home")}
</Link>
</Menu.Item>
<Menu.SubMenu title={t("menus.header.jobs")}>
<Menu.Item key='jobs'>
<Link to='/manage/jobs'>
<Icon type='home' />
{t("menus.header.activejobs")}
</Link>
</Menu.Item>
))}
{!landingHeader ? (
<Menu.Item>
<SignOut />
<Menu.Item key='availablejobs'>
<Link to='/manage/available'>
<Icon type='home' />
{t("menus.header.availablejobs")}
</Link>
</Menu.Item>
) : (
</Menu.SubMenu>
{
// navItems.map(navItem => (
// <Menu.Item key={navItem.title}>
// <Link to={navItem.path}>
// {navItem.icontype ? <Icon type={navItem.icontype} /> : null}
// {navItem.title}
// </Link>
// </Menu.Item>
// ))
}
{!landingHeader ? null : (
<Menu.Item>
<ManageSignInButton />
</Menu.Item>

View File

@@ -1,46 +1,45 @@
import React from "react";
import "./header.styles.scss";
import { useQuery } from "react-apollo";
import {
GET_LANDING_NAV_ITEMS,
GET_NAV_ITEMS
} from "../../graphql/metadata.queries";
// //import {
// GET_LANDING_NAV_ITEMS,
// GET_NAV_ITEMS
// } from "../../graphql/metadata.queries";
import { GET_CURRENT_SELECTED_NAV_ITEM } from "../../graphql/local.queries";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import AlertComponent from "../alert/alert.component";
//import LoadingSpinner from "../loading-spinner/loading-spinner.component";
//import AlertComponent from "../alert/alert.component";
import HeaderComponent from "./header.component";
export default ({ landingHeader, signedIn }) => {
const hookSelectedNavItem = useQuery(GET_CURRENT_SELECTED_NAV_ITEM);
let hookNavItems;
if (landingHeader) {
hookNavItems = useQuery(GET_LANDING_NAV_ITEMS, {
fetchPolicy: "network-only"
});
} else {
hookNavItems = useQuery(GET_NAV_ITEMS, {
fetchPolicy: "network-only"
});
}
// let hookNavItems;
// if (landingHeader) {
// hookNavItems = useQuery(GET_LANDING_NAV_ITEMS, {
// fetchPolicy: "network-only"
// });
// } else {
// hookNavItems = useQuery(GET_NAV_ITEMS, {
// fetchPolicy: "network-only"
// });
// }
if (hookNavItems.loading || hookSelectedNavItem.loading)
return <LoadingSpinner />;
if (hookNavItems.error)
return <AlertComponent message={hookNavItems.error.message} />;
if (hookSelectedNavItem.error)
return console.log(
"Unable to load Selected Navigation Item.",
hookSelectedNavItem.error
);
// if (hookNavItems.loading || hookSelectedNavItem.loading)
// return <LoadingSpinner />;
// if (hookNavItems.error)
// return <AlertComponent message={hookNavItems.error.message} />;
// if (hookSelectedNavItem.error)
// return console.log(
// "Unable to load Selected Navigation Item.",
// hookSelectedNavItem.error
// );
const { selectedNavItem } = hookSelectedNavItem.data;
const navItems = JSON.parse(hookNavItems.data.masterdata_by_pk.value);
// const navItems = JSON.parse(hookNavItems.data.masterdata_by_pk.value);
return (
<HeaderComponent
landingHeader={landingHeader}
navItems={navItems}
selectedNavItem={selectedNavItem}
/>
);

View File

@@ -1,26 +1,24 @@
import React, { useState } from "react";
import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useQuery } from "@apollo/react-hooks";
import AlertComponent from "../alert/alert.component";
import { Button, Icon, PageHeader, Tag } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries";
import { PageHeader, Button, Descriptions, Tag, Icon } from "antd";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container";
//import JobDetailCardsHeaderComponent from "./job-detail-cards.header.component";
import JobDetailCardsCustomerComponent from "./job-detail-cards.customer.component";
import JobDetailCardsVehicleComponent from "./job-detail-cards.vehicle.component";
import JobDetailCardsInsuranceComponent from "./job-detail-cards.insurance.component";
import JobDetailCardsDatesComponent from "./job-detail-cards.dates.component";
import JobDetailCardsPartsComponent from "./job-detail-cards.parts.component";
import JobDetailCardsNotesComponent from "./job-detail-cards.notes.component";
import JobDetailCardsDamageComponent from "./job-detail-cards.damage.component";
import JobDetailCardsTotalsComponent from "./job-detail-cards.totals.component";
import JobDetailCardsDatesComponent from "./job-detail-cards.dates.component";
import JobDetailCardsDocumentsComponent from "./job-detail-cards.documents.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import JobDetailCardsInsuranceComponent from "./job-detail-cards.insurance.component";
import JobDetailCardsNotesComponent from "./job-detail-cards.notes.component";
import JobDetailCardsPartsComponent from "./job-detail-cards.parts.component";
import "./job-detail-cards.styles.scss";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container";
import JobDetailCardsTotalsComponent from "./job-detail-cards.totals.component";
export default function JobDetailCards({ selectedJob }) {
const { loading, error, data, refetch } = useQuery(QUERY_JOB_CARD_DETAILS, {
@@ -56,11 +54,17 @@ export default function JobDetailCards({ selectedJob }) {
</span>
}
title={
loading
? t("general.labels.loading")
: data.jobs_by_pk.ro_number
? `${t("jobs.fields.ro_number")} ${data.jobs_by_pk.ro_number}`
: `${t("jobs.fields.est_number")} ${data.jobs_by_pk.est_number}`
loading ? (
t("general.labels.loading")
) : (
<Link to={`/manage/jobs/${data.jobs_by_pk.id}`}>
{data.jobs_by_pk.ro_number
? `${t("jobs.fields.ro_number")} ${data.jobs_by_pk.ro_number}`
: `${t("jobs.fields.est_number")} ${
data.jobs_by_pk.est_number
}`}{" "}
</Link>
)
}
extra={[
<Link
@@ -89,33 +93,38 @@ export default function JobDetailCards({ selectedJob }) {
{t("jobs.actions.postInvoices")}
</Button>
]}>
{loading ? (
<LoadingSkeleton />
) : (
<Descriptions size='small' column={3}>
<Descriptions.Item label='Created'>Lili Qu</Descriptions.Item>
<Descriptions.Item label='Association'>421421</Descriptions.Item>
<Descriptions.Item label='Creation Time'>
2017-01-10
</Descriptions.Item>
<Descriptions.Item label='Effective Time'>
2017-10-10
</Descriptions.Item>
<Descriptions.Item label='Remarks'>
Gonghu Road, Xihu District, Hangzhou, Zhejiang, China
</Descriptions.Item>
</Descriptions>
)}
{
// loading ? (
// <LoadingSkeleton />
// ) : (
// <Descriptions size='small' column={3}>
// <Descriptions.Item label='Created'>Lili Qu</Descriptions.Item>
// <Descriptions.Item label='Association'>421421</Descriptions.Item>
// <Descriptions.Item label='Creation Time'>
// 2017-01-10
// </Descriptions.Item>
// <Descriptions.Item label='Effective Time'>
// 2017-10-10
// </Descriptions.Item>
// <Descriptions.Item label='Remarks'>
// Gonghu Road, Xihu District, Hangzhou, Zhejiang, China
// </Descriptions.Item>
// </Descriptions>
// )
}
<section className='job-cards'>
<JobDetailCardsCustomerComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
<JobDetailCardsVehicleComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
{
// <JobDetailCardsVehicleComponent
// loading={loading}
// data={data ? data.jobs_by_pk : null}
// />
}
<JobDetailCardsInsuranceComponent
loading={loading}
data={data ? data.jobs_by_pk : null}

View File

@@ -1,7 +1,8 @@
import React from "react";
import { useTranslation } from "react-i18next";
import CardTemplate from "./job-detail-cards.template.component";
import { Link } from "react-router-dom";
import PhoneFormatter from "../../utils/PhoneFormatter";
import CardTemplate from "./job-detail-cards.template.component";
export default function JobDetailCardsCustomerComponent({ loading, data }) {
const { t } = useTranslation();
@@ -10,21 +11,34 @@ export default function JobDetailCardsCustomerComponent({ loading, data }) {
<CardTemplate
loading={loading}
title={t("jobs.labels.cards.customer")}
extraLink={data?.owner ? `/manage/owners/${data?.owner?.id}` : null}>
extraLink={data && data.owner ? `/manage/owners/${data.owner.id}` : null}>
{data ? (
<span>
<div>{`${data?.ownr_fn ??
""} ${data.ownr_ln ?? ""}`}</div>
<div>{`${data.ownr_fn || ""} ${data.ownr_ln || ""}`}</div>
<div>
<PhoneFormatter>{`${data?.ownr_ph1 ?? ""}`}</PhoneFormatter>
{t("jobs.fields.phoneshort")}:
<PhoneFormatter>{`${data.ownr_ph1 ||
t("general.labels.na")}`}</PhoneFormatter>
</div>
{data?.ownr_ea ? (
<a href={`mailto:${data.ownr_ea}`}>
<div>{`${data?.ownr_ea ?? ""}`}</div>
</a>
) : null}
<div>{`${data?.owner?.preferred_contact ?? ""}`}</div>
<div>
{t("jobs.fields.ownr_ea")}:
{data.ownr_ea ? (
<a href={`mailto:${data.ownr_ea}`}>
<span>{`${data.ownr_ea || ""}`}</span>
</a>
) : (
t("general.labels.na")
)}
</div>
<div>{`${(data.owner && data.owner.preferred_contact) || ""}`}</div>
{data.vehicle ? (
<Link to={`/manage/vehicles/${data.vehicle.id}`}>
{`${data.vehicle.v_model_yr || ""} ${data.vehicle.v_make_desc ||
""} ${data.vehicle.v_model_desc || ""}`}
</Link>
) : (
<span>{t("jobs.errors.novehicle")}</span>
)}
</span>
) : null}
</CardTemplate>

View File

@@ -11,13 +11,112 @@ export default function JobDetailCardsDatesComponent({ loading, data }) {
<CardTemplate loading={loading} title={t("jobs.labels.cards.dates")}>
{data ? (
<Timeline>
<Timeline.Item>
Actual In <Moment format='MM/DD/YYYY'>{data?.actual_in}</Moment>
</Timeline.Item>
<Timeline.Item>
Scheduled Completion
<Moment format='MM/DD/YYYY'>{data?.scheduled_completion}</Moment>
</Timeline.Item>
{!(
data.actual_in ||
data.scheduled_completion ||
data.scheduled_in ||
data.actual_completion ||
data.scheduled_delivery ||
data.actual_delivery ||
data.date_estimated ||
data.date_open ||
data.date_scheduled ||
data.date_invoiced ||
data.date_closed ||
data.date_exported
) ? (
<div>{t("jobs.errors.nodates")}</div>
) : null}
{data.actual_in ? (
<Timeline.Item>
{t("jobs.fields.actual_in")}
<Moment format='MM/DD/YYYY'>{data.actual_in || ""}</Moment>
</Timeline.Item>
) : null}
{data.scheduled_completion ? (
<Timeline.Item>
{t("jobs.fields.scheduled_completion")}
<Moment format='MM/DD/YYYY'>
{data.scheduled_completion || ""}
</Moment>
</Timeline.Item>
) : null}
{data.scheduled_in ? (
<Timeline.Item>
{t("jobs.fields.scheduled_in")}
<Moment format='MM/DD/YYYY'>{data.scheduled_in || ""}</Moment>
</Timeline.Item>
) : null}
{data.actual_completion ? (
<Timeline.Item>
{t("jobs.fields.actual_completion")}
<Moment format='MM/DD/YYYY'>
{data.actual_completion || ""}
</Moment>
</Timeline.Item>
) : null}
{data.scheduled_delivery ? (
<Timeline.Item>
{t("jobs.fields.scheduled_delivery")}
<Moment format='MM/DD/YYYY'>
{data.scheduled_delivery || ""}
</Moment>
</Timeline.Item>
) : null}
{data.actual_delivery ? (
<Timeline.Item>
{t("jobs.fields.actual_delivery")}
<Moment format='MM/DD/YYYY'>{data.actual_delivery || ""}</Moment>
</Timeline.Item>
) : null}
{data.date_estimated ? (
<Timeline.Item>
{t("jobs.fields.date_estimated")}
<Moment format='MM/DD/YYYY'>{data.date_estimated || ""}</Moment>
</Timeline.Item>
) : null}
{data.date_open ? (
<Timeline.Item>
{t("jobs.fields.date_open")}
<Moment format='MM/DD/YYYY'>{data.date_open || ""}</Moment>
</Timeline.Item>
) : null}
{data.date_scheduled ? (
<Timeline.Item>
{t("jobs.fields.date_scheduled")}
<Moment format='MM/DD/YYYY'>{data.date_scheduled || ""}</Moment>
</Timeline.Item>
) : null}
{data.date_invoiced ? (
<Timeline.Item>
{t("jobs.fields.date_invoiced")}
<Moment format='MM/DD/YYYY'>{data.date_invoiced || ""}</Moment>
</Timeline.Item>
) : null}
{data.date_closed ? (
<Timeline.Item>
{t("jobs.fields.date_closed")}
<Moment format='MM/DD/YYYY'>{data.date_closed || ""}</Moment>
</Timeline.Item>
) : null}
{data.date_exported ? (
<Timeline.Item>
{t("jobs.fields.date_exported")}
<Moment format='MM/DD/YYYY'>{data.date_exported || ""}</Moment>
</Timeline.Item>
) : null}
</Timeline>
) : null}
</CardTemplate>

View File

@@ -1,17 +1,35 @@
import { Carousel } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import "./job-detail-cards.styles.scss";
import CardTemplate from "./job-detail-cards.template.component";
export default function JobDetailCardsDocumentsComponent({ loading, data }) {
const { t } = useTranslation();
if (!data)
return (
<CardTemplate loading={loading} title={t("jobs.labels.cards.documents")}>
null
</CardTemplate>
);
return (
<CardTemplate loading={loading} title={t("jobs.labels.cards.documents")}>
{data ? (
<span>
Documents stuff here.
</span>
) : null}
<CardTemplate
loading={loading}
title={t("jobs.labels.cards.documents")}
extraLink={`/manage/jobs/${data.id}#documents`}>
{data.documents.count > 0 ? (
<Carousel autoplay>
{data.documents.map(item => (
<div key={item.id}>
<img src={item.thumb_url} alt={item.name} />
</div>
))}
</Carousel>
) : (
<div>{t("documents.errors.nodocuments")}</div>
)}
</CardTemplate>
);
}

View File

@@ -0,0 +1,11 @@
.ant-carousel .slick-slide {
text-align: center;
height: 160px;
line-height: 160px;
background: #364d79;
overflow: hidden;
}
.ant-carousel .slick-slide h3 {
color: #fff;
}

View File

@@ -10,16 +10,16 @@ export default function JobDetailCardsInsuranceComponent({ loading, data }) {
<CardTemplate loading={loading} title={t("jobs.labels.cards.insurance")}>
{data ? (
<span>
<div>{data?.ins_co_nm ?? t("general.labels.unknown")}</div>
<div>{data?.clm_no ?? t("general.labels.unknown")}</div>
<div>{data?.ins_co_nm || t("general.labels.unknown")}</div>
<div>{data?.clm_no || t("general.labels.unknown")}</div>
<div>
{t("jobs.labels.cards.filehandler")}
{data?.ins_ea ? (
<a href={`mailto:${data.ins_ea}`}>
<div>{`${data?.ins_ct_fn ?? ""} ${data?.ins_ct_ln ?? ""}`}</div>
<div>{`${data?.ins_ct_fn || ""} ${data?.ins_ct_ln || ""}`}</div>
</a>
) : (
<div>{`${data?.ins_ct_fn ?? ""} ${data?.ins_ct_ln ?? ""}`}</div>
<div>{`${data?.ins_ct_fn || ""} ${data?.ins_ct_ln || ""}`}</div>
)}
{data?.ins_ph1 ? (
<PhoneFormatter>{data?.ins_ph1}</PhoneFormatter>
@@ -27,14 +27,13 @@ export default function JobDetailCardsInsuranceComponent({ loading, data }) {
</div>
<div>
TODO:
{t("jobs.labels.cards.appraiser")}
{data?.est_ea ? (
<a href={`mailto:${data.est_ea}`}>
<div>{`${data?.ins_ct_fn ?? ""} ${data?.ins_ct_ln ?? ""}`}</div>
<div>{`${data?.ins_ct_fn || ""} ${data?.ins_ct_ln || ""}`}</div>
</a>
) : (
<div>{`${data?.ins_ct_fn ?? ""} ${data?.ins_ct_ln ?? ""}`}</div>
<div>{`${data?.ins_ct_fn || ""} ${data?.ins_ct_ln || ""}`}</div>
)}
</div>
@@ -42,10 +41,10 @@ export default function JobDetailCardsInsuranceComponent({ loading, data }) {
{t("jobs.labels.cards.estimator")}
{data?.est_ea ? (
<a href={`mailto:${data.est_ea}`}>
<div>{`${data?.est_ct_fn ?? ""} ${data?.est_ct_ln ?? ""}`}</div>
<div>{`${data?.est_ct_fn || ""} ${data?.est_ct_ln || ""}`}</div>
</a>
) : (
<div>{`${data?.est_ct_fn ?? ""} ${data?.est_ct_ln ?? ""}`}</div>
<div>{`${data?.est_ct_fn || ""} ${data?.est_ct_ln || ""}`}</div>
)}
{data?.est_ph1 ? (
<PhoneFormatter>{data?.est_ph1}</PhoneFormatter>

View File

@@ -13,9 +13,9 @@ export default function JobDetailCardsVehicleComponent({ loading, data }) {
>
{data ? (
<span>
{data.vehicle?.v_model_yr ?? t("general.labels.na")}{" "}
{data.vehicle?.v_make_desc ?? t("general.labels.na")}{" "}
{data.vehicle?.v_model_desc ?? t("general.labels.na")}
{data.vehicle?.v_model_yr || t("general.labels.na")}{" "}
{data.vehicle?.v_make_desc || t("general.labels.na")}{" "}
{data.vehicle?.v_model_desc || t("general.labels.na")}
</span>
) : null}
</CardTemplate>

View File

@@ -0,0 +1,52 @@
import React from "react";
import { Form, Input, InputNumber } from "antd";
import JobDetailFormContext from "../../pages/jobs-detail/jobs-detail.page.context";
export default class EditableCell extends React.Component {
getInput = () => {
if (this.props.inputType === "number") {
return <InputNumber />;
}
return <Input />;
};
renderCell = ({ getFieldDecorator }) => {
const {
editing,
dataIndex,
title,
inputType,
record,
index,
children,
...restProps
} = this.props;
return (
<td {...restProps}>
{editing ? (
<Form.Item style={{ margin: 0 }}>
{getFieldDecorator(dataIndex, {
rules: [
{
required: true,
message: `Please Input ${title}!`
}
],
initialValue: record[dataIndex]
})(this.getInput())}
</Form.Item>
) : (
children
)}
</td>
);
};
render() {
return (
<JobDetailFormContext.Consumer>
{this.renderCell}
</JobDetailFormContext.Consumer>
);
}
}

View File

@@ -0,0 +1,124 @@
import { Table, Button } from "antd";
import React, { useContext, useState } from "react";
import { useTranslation } from "react-i18next";
import JobDetailFormContext from "../../pages/jobs-detail/jobs-detail.page.context";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters";
import EditableCell from "./job-lines-cell.component";
export default function JobLinesComponent({ job }) {
//const form = useContext(JobDetailFormContext);
//const { getFieldDecorator } = form;
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" }
});
const [editingKey, setEditingKey] = useState("");
const { t } = useTranslation();
const columns = [
{
title: t("joblines.fields.unq_seq"),
dataIndex: "joblines.unq_seq",
key: "joblines.unq_seq",
// onFilter: (value, record) => record.ro_number.includes(value),
// filteredValue: state.filteredInfo.text || null,
sorter: (a, b) => alphaSort(a, b),
sortOrder:
state.sortedInfo.columnKey === "unq_seq" && state.sortedInfo.order,
//ellipsis: true,
editable: true
},
{
title: t("joblines.fields.line_desc"),
dataIndex: "line_desc",
key: "joblines.line_desc",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
sortOrder:
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
ellipsis: true,
editable: true
},
{
title: t("joblines.fields.part_type"),
dataIndex: "part_type",
key: "joblines.part_type",
sorter: (a, b) => alphaSort(a.part_type, b.part_type),
sortOrder:
state.sortedInfo.columnKey === "part_type" && state.sortedInfo.order,
ellipsis: true,
editable: true
},
{
title: t("joblines.fields.db_price"),
dataIndex: "db_price",
key: "joblines.db_price",
sorter: (a, b) => a.db_price - b.db_price,
sortOrder:
state.sortedInfo.columnKey === "db_price" && state.sortedInfo.order,
ellipsis: true,
render: (text, record) => (
<CurrencyFormatter>{record.db_price}</CurrencyFormatter>
)
},
{
title: t("joblines.fields.act_price"),
dataIndex: "act_price",
key: "joblines.act_price",
sorter: (a, b) => a.act_price - b.act_price,
sortOrder:
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
ellipsis: true,
render: (text, record) => (
<div>
{" "}
<CurrencyFormatter>{record.act_price}</CurrencyFormatter>{" "}
<Button
onClick={() => {
setEditingKey(record.id);
}}>
EDIT
</Button>
</div>
)
}
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
// const handleChange = event => {
// const { value } = event.target;
// setState({ ...state, filterinfo: { text: [value] } });
// };
return (
<Table
size='small'
pagination={{ position: "bottom" }}
columns={columns.map(col => {
if (!col.editable) {
return col;
}
return {
...col,
onCell: record => ({
record,
inputType: col.dataIndex === "age" ? "number" : "text",
dataIndex: col.dataIndex,
title: col.title,
editing: editingKey === record.id
})
};
})}
components={{
body: {
cell: EditableCell
}
}}
rowKey='id'
dataSource={job.joblines}
onChange={handleTableChange}
/>
);
}

View File

@@ -1,12 +1,11 @@
import React from "react";
import JobLinesComponent from "./job-lines.component";
import { useQuery } from "@apollo/react-hooks";
import AlertComponent from "../../components/alert/alert.component";
import AlertComponent from "../alert/alert.component";
import { GET_JOB_LINES_BY_PK } from "../../graphql/jobs-lines.queries";
export default function JobLinesContainer({ match }) {
const { jobId } = match.params;
export default function JobLinesContainer({ jobId }) {
const { loading, error, data } = useQuery(GET_JOB_LINES_BY_PK, {
variables: { id: jobId },
@@ -17,3 +16,4 @@ export default function JobLinesContainer({ match }) {
<JobLinesComponent loading={loading} joblines={data ? data.joblines : null} />
);
}

View File

@@ -1,53 +0,0 @@
import React, { useState } from "react";
import { Table } from "antd";
import { alphaSort } from "../../utils/sorters";
export default function JobLinesComponent({ loading, joblines }) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" }
});
const columns = [
{
title: "Line #",
dataIndex: "line_ind",
key: "line_ind",
// onFilter: (value, record) => record.ro_number.includes(value),
// filteredValue: state.filteredInfo.text || null,
sorter: (a, b) => alphaSort(a, b),
sortOrder:
state.sortedInfo.columnKey === "line_ind" && state.sortedInfo.order,
ellipsis: true
},
{
title: "Description",
dataIndex: "line_desc",
key: "line_desc",
sorter: (a, b) => alphaSort(a, b),
sortOrder:
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
ellipsis: true
}
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
// const handleChange = event => {
// const { value } = event.target;
// setState({ ...state, filterinfo: { text: [value] } });
// };
return (
<Table
loading={loading}
pagination={{ position: "bottom" }}
columns={columns.map(item => ({ ...item }))}
rowKey='id'
dataSource={joblines}
onChange={handleTableChange}
/>
);
}

View File

@@ -1,217 +0,0 @@
import React, { useState } from "react";
import { Link } from "react-router-dom";
import AlertComponent from "../alert/alert.component";
import {
Form,
Input,
Row,
Col,
Button,
Typography,
PageHeader,
Descriptions,
Tag,
notification,
Avatar,
Layout
} from "antd";
import { UPDATE_JOB, CONVERT_JOB_TO_RO } from "../../graphql/jobs.queries";
import { useMutation } from "@apollo/react-hooks";
import FormItemPhone from "../form-items-formatted/phone-form-item.component";
import { useTranslation } from "react-i18next";
import CarImage from "../../assets/car.svg";
const { Content } = Layout;
const formItemLayout = {
// labelCol: {
// xs: { span: 12 },
// sm: { span: 5 }
// },
// wrapperCol: {
// xs: { span: 24 },
// sm: { span: 12 }
// }
};
function JobTombstone({ job, ...otherProps }) {
const [jobContext, setJobContext] = useState(job);
const [mutationUpdateJob] = useMutation(UPDATE_JOB);
const [mutationConvertJob] = useMutation(CONVERT_JOB_TO_RO);
const { t } = useTranslation();
if (!job) {
return <AlertComponent message={t("jobs.errors.noaccess")} type='error' />;
}
const handleSubmit = e => {
e.preventDefault();
otherProps.form.validateFieldsAndScroll((err, values) => {
if (err) {
notification["error"]({
message: t("jobs.errors.validationtitle"),
description: t("jobs.errors.validation")
});
}
if (!err) {
mutationUpdateJob({
variables: { jobId: jobContext.id, job: values }
}).then(r =>
notification["success"]({
message: t("jobs.successes.savetitle")
})
);
}
});
};
const handleChange = event => {
const { name, value } = event.target ? event.target : event;
setJobContext({ ...jobContext, [name]: value });
};
const { getFieldDecorator } = otherProps.form;
const tombstoneTitle = (
<div>
<Avatar size='large' alt='Vehicle Image' src={CarImage} />
{`${t("jobs.fields.ro_number")} ${
jobContext.ro_number ? jobContext.ro_number : t("general.labels.na")
}`}
</div>
);
return (
<Content>
<Form onSubmit={handleSubmit} {...formItemLayout}>
<PageHeader
style={{
border: "1px solid rgb(235, 237, 240)"
}}
title={tombstoneTitle}
subTitle={
jobContext.owner
? (jobContext.owner?.first_name ?? "") +
" " +
(jobContext.owner?.last_name ?? "")
: t("jobs.errors.noowner")
}
tags={
<span key='job-status'>
{jobContext.job_status ? (
<Tag color='blue'>{jobContext.job_status.name}</Tag>
) : null}
</span>
}
extra={[
<Button
key='convert'
type='dashed'
disabled={jobContext.converted}
onClick={() => {
mutationConvertJob({
variables: { jobId: jobContext.id }
}).then(r => {
console.log("r", r);
setJobContext({
...jobContext,
converted: true,
ro_number: r.data.update_jobs.returning[0].ro_number
});
notification["success"]({
message: t("jobs.successes.converted")
});
});
}}>
{t("jobs.labels.convert")}
</Button>,
<Button type='primary' key='submit' htmlType='submit'>
{t("general.labels.save")}
</Button>
]}>
<Descriptions size='small' column={5}>
<Descriptions.Item label={t("jobs.fields.vehicle")}>
<Link to={`/manage/vehicles/${jobContext.vehicle?.id}`}>
{jobContext.vehicle?.v_model_yr ?? t("general.labels.na")}{" "}
{jobContext.vehicle?.v_make_desc ?? t("general.labels.na")}{" "}
{jobContext.vehicle?.v_model_desc ?? t("general.labels.na")} |{" "}
{jobContext.vehicle?.plate_no ?? t("general.labels.na")}
</Link>
</Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.est_number")}>
{jobContext.est_number}
</Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.claim_total")}>
$ {jobContext.claim_total?.toFixed(2)}
</Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.deductible")}>
$ {jobContext.deductible?.toFixed(2)}
</Descriptions.Item>
</Descriptions>
</PageHeader>
<Row>
<Typography.Title level={4}>Information</Typography.Title>
{
// <Form.Item label='Estimate #'>
// {getFieldDecorator("est_number", {
// initialValue: jobContext.est_number
// })(<Input name='est_number' readOnly onChange={handleChange} />)}
// </Form.Item>
}
</Row>
<Row>
<Typography.Title level={4}>Insurance Information</Typography.Title>
<Form.Item label='Insurance Company'>
{getFieldDecorator("est_co_nm", {
initialValue: jobContext.est_co_nm
})(<Input name='est_co_nm' onChange={handleChange} />)}
</Form.Item>
<Col span={8}>
<Form.Item label='Estimator Last Name'>
{getFieldDecorator("est_ct_ln", {
initialValue: jobContext.est_ct_ln
})(<Input name='est_ct_ln' onChange={handleChange} />)}
</Form.Item>
<Form.Item label='Estimator First Name'>
{getFieldDecorator("est_ct_fn", {
initialValue: jobContext.est_ct_fn
})(<Input name='est_ct_fn' onChange={handleChange} />)}
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label='Estimator Phone #'>
{getFieldDecorator("est_ph1", {
initialValue: jobContext.est_ph1
})(
<FormItemPhone
customInput={Input}
name='est_ph1'
onValueChange={handleChange}
/>
)}
</Form.Item>
<Form.Item label='Estimator Email'>
{getFieldDecorator("est_ea", {
initialValue: jobContext.est_ea,
rules: [
{
type: "email",
message: "This is not a valid email address."
}
]
})(<Input name='est_ea' onChange={handleChange} />)}
</Form.Item>
</Col>
</Row>
</Form>
</Content>
);
}
export default Form.create({ name: "JobTombstone" })(JobTombstone);

View File

@@ -0,0 +1,212 @@
import { Button, Icon, Input, notification, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import OwnerFindModalContainer from "../owner-find-modal/owner-find-modal.container";
export default function JobsAvailableComponent({
loading,
data,
refetch,
deleteJob,
deleteAllNewJobs,
onModalOk,
onModalCancel,
modalVisible,
setModalVisible,
selectedOwner,
setSelectedOwner,
loadEstData,
estData
}) {
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" }
});
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const columns = [
{
title: t("jobs.fields.cieca_id"),
dataIndex: "cieca_id",
key: "cieca_id",
//width: "8%",
// onFilter: (value, record) => record.ro_number.includes(value),
// filteredValue: state.filteredInfo.text || null,
sorter: (a, b) => alphaSort(a, b),
sortOrder:
state.sortedInfo.columnKey === "cieca_id" && state.sortedInfo.order
},
{
title: t("jobs.fields.owner"),
dataIndex: "ownr_name",
key: "ownr_name",
ellipsis: true,
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
//width: "25%",
sortOrder:
state.sortedInfo.columnKey === "ownr_name" && state.sortedInfo.order
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle_info",
key: "vehicle_info",
sorter: (a, b) => alphaSort(a.vehicle_info, b.vehicle_info),
sortOrder:
state.sortedInfo.columnKey === "vehicle_info" && state.sortedInfo.order
//ellipsis: true
},
{
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order
//width: "12%",
//ellipsis: true
},
{
title: t("jobs.fields.clm_total"),
dataIndex: "clm_amt",
key: "clm_amt",
sorter: (a, b) => a.clm_amt - b.clm_amt,
sortOrder:
state.sortedInfo.columnKey === "clm_amt" && state.sortedInfo.order
//width: "12%",
//ellipsis: true
},
{
title: t("jobs.fields.uploaded_by"),
dataIndex: "uploaded_by",
key: "uploaded_by",
sorter: (a, b) => alphaSort(a.uploaded_by, b.uploaded_by),
sortOrder:
state.sortedInfo.columnKey === "uploaded_by" && state.sortedInfo.order
//width: "12%",
//ellipsis: true
},
{
title: t("jobs.fields.updated_at"),
dataIndex: "updated_at",
key: "updated_at",
sorter: (a, b) => new Date(a.updated_at) - new Date(b.updated_at),
sortOrder:
state.sortedInfo.columnKey === "updated_at" && state.sortedInfo.order,
render: (text, record) => (
<DateTimeFormatter>{record.updated_at}</DateTimeFormatter>
)
//width: "12%",
//ellipsis: true
},
{
title: t("general.labels.actions"),
key: "actions",
render: (text, record) => (
<span>
<Button
onClick={() => {
deleteJob({ variables: { id: record.id } }).then(r => {
notification["success"]({
message: t("jobs.successes.deleted")
});
refetch();
});
}}
>
<Icon type="delete" />
</Button>
<Button
onClick={() => {
loadEstData({ variables: { id: record.id } });
setModalVisible(true);
}}
>
<Icon type="plus" />
</Button>
</span>
)
//width: "12%",
//ellipsis: true
}
];
const owner =
estData.data &&
estData.data.available_jobs_by_pk &&
estData.data.available_jobs_by_pk.est_data &&
estData.data.available_jobs_by_pk.est_data.owner &&
estData.data.available_jobs_by_pk.est_data.owner.data
? estData.data.available_jobs_by_pk.est_data.owner.data
: null;
return (
<div>
<OwnerFindModalContainer
loading={estData.loading}
error={estData.error}
owner={owner}
selectedOwner={selectedOwner}
setSelectedOwner={setSelectedOwner}
visible={modalVisible}
onOk={onModalOk}
onCancel={onModalCancel}
/>
<Table
loading={loading}
title={() => {
return (
<div>
<Input.Search
placeholder="Search..."
onSearch={value => {
console.log(value);
}}
enterButton
/>
<Button
onClick={() => {
refetch();
}}
>
<Icon type="sync" />
</Button>
<Button
onClick={() => {
deleteAllNewJobs()
.then(r => {
notification["success"]({
message: t("jobs.successes.all_deleted", {
count: r.data.delete_available_jobs.affected_rows
})
});
refetch();
})
.catch(r => {
notification["error"]({
message: t("jobs.errors.deleted") + " " + r.message
});
});
}}
>
Delete All
</Button>
</div>
);
}}
size="small"
pagination={{ position: "top" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={data && data.available_jobs}
onChange={handleTableChange}
/>
</div>
);
}

View File

@@ -0,0 +1,125 @@
import { notification } from "antd";
import React, { useState } from "react";
import { useMutation, useQuery } from "react-apollo";
import { useTranslation } from "react-i18next";
import { withRouter } from "react-router-dom";
import { DELETE_ALL_AVAILABLE_NEW_JOBS, QUERY_AVAILABLE_NEW_JOBS } from "../../graphql/available-jobs.queries";
import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import JobsAvailableComponent from "./jobs-available-new.component";
export default withRouter(function JobsAvailableContainer({
deleteJob,
estDataLazyLoad,
history
}) {
const { loading, error, data, refetch } = useQuery(QUERY_AVAILABLE_NEW_JOBS, {
fetchPolicy: "network-only"
});
const { t } = useTranslation();
const [modalVisible, setModalVisible] = useState(false);
const [selectedOwner, setSelectedOwner] = useState(null);
const [insertLoading, setInsertLoading] = useState(false);
const [deleteAllNewJobs] = useMutation(DELETE_ALL_AVAILABLE_NEW_JOBS);
const [insertNewJob] = useMutation(INSERT_NEW_JOB);
const [loadEstData, estData] = estDataLazyLoad;
const onModalOk = () => {
setModalVisible(false);
console.log("selectedOwner", selectedOwner);
setInsertLoading(true);
console.log(
"logitest",
estData.data &&
estData.data.available_jobs_by_pk &&
estData.data.available_jobs_by_pk.est_data
);
if (
!(
estData.data &&
estData.data.available_jobs_by_pk &&
estData.data.available_jobs_by_pk.est_data
)
) {
//We don't have the right data. Error!
setInsertLoading(false);
notification["error"]({
message: t("jobs.errors.creating", { error: "No job data present." })
});
} else {
insertNewJob({
variables: {
job: selectedOwner
? Object.assign(
{},
estData.data.available_jobs_by_pk.est_data,
{ owner: null },
{ ownerid: selectedOwner }
)
: estData.data.available_jobs_by_pk.est_data
}
})
.then(r => {
notification["success"]({
message: t("jobs.successes.created"),
onClick: () => {
console.log("r", r);
history.push(
`/manage/jobs/${r.data.insert_jobs.returning[0].id}`
);
}
});
//Job has been inserted. Clean up the available jobs record.
deleteJob({
variables: { id: estData.data.available_jobs_by_pk.id }
}).then(r => {
refetch();
setInsertLoading(false);
});
})
.catch(r => {
//error while inserting
notification["error"]({
message: t("jobs.errors.creating", { error: r.message })
});
refetch();
setInsertLoading(false);
});
}
};
const onModalCancel = () => {
setModalVisible(false);
setSelectedOwner(null);
};
if (error) return <AlertComponent type="error" message={error.message} />;
return (
<LoadingSpinner
loading={insertLoading}
message={t("jobs.labels.creating_new_job")}
>
<JobsAvailableComponent
loading={loading}
data={data}
refetch={refetch}
deleteJob={deleteJob}
deleteAllNewJobs={deleteAllNewJobs}
insertNewJob={insertNewJob}
estDataLazyLoad={estDataLazyLoad}
onModalCancel={onModalCancel}
onModalOk={onModalOk}
modalVisible={modalVisible}
setModalVisible={setModalVisible}
selectedOwner={selectedOwner}
setSelectedOwner={setSelectedOwner}
loadEstData={loadEstData}
estData={estData}
/>
</LoadingSpinner>
);
});

View File

@@ -0,0 +1,193 @@
import { Input, Table, Button, Icon, notification } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
import { DateTimeFormatter } from "../../utils/DateFormatter";
export default function JobsAvailableSupplementComponent({
loading,
data,
refetch,
deleteJob,
deleteAllNewJobs,
estDataLazyLoad
}) {
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" }
});
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const columns = [
{
title: t("jobs.fields.cieca_id"),
dataIndex: "cieca_id",
key: "cieca_id",
//width: "8%",
// onFilter: (value, record) => record.ro_number.includes(value),
// filteredValue: state.filteredInfo.text || null,
sorter: (a, b) => alphaSort(a, b),
sortOrder:
state.sortedInfo.columnKey === "cieca_id" && state.sortedInfo.order
},
{
title: t("jobs.fields.ro_number"),
dataIndex: "job_id",
key: "job_id",
//width: "8%",
// onFilter: (value, record) => record.ro_number.includes(value),
// filteredValue: state.filteredInfo.text || null,
sorter: (a, b) => alphaSort(a, b),
sortOrder:
state.sortedInfo.columnKey === "cieca_id" && state.sortedInfo.order,
render: (text, record) => <div>{record.job && record.job.ro_number}</div>
},
{
title: t("jobs.fields.owner"),
dataIndex: "ownr_name",
key: "ownr_name",
ellipsis: true,
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
//width: "25%",
sortOrder:
state.sortedInfo.columnKey === "ownr_name" && state.sortedInfo.order
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle_info",
key: "vehicle_info",
sorter: (a, b) => alphaSort(a.vehicle_info, b.vehicle_info),
sortOrder:
state.sortedInfo.columnKey === "vehicle_info" && state.sortedInfo.order
//ellipsis: true
},
{
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order
//width: "12%",
//ellipsis: true
},
{
title: t("jobs.fields.clm_total"),
dataIndex: "clm_amt",
key: "clm_amt",
sorter: (a, b) => a.clm_amt - b.clm_amt,
sortOrder:
state.sortedInfo.columnKey === "clm_amt" && state.sortedInfo.order
//width: "12%",
//ellipsis: true
},
{
title: t("jobs.fields.uploaded_by"),
dataIndex: "uploaded_by",
key: "uploaded_by",
sorter: (a, b) => alphaSort(a.uploaded_by, b.uploaded_by),
sortOrder:
state.sortedInfo.columnKey === "uploaded_by" && state.sortedInfo.order
//width: "12%",
//ellipsis: true
},
{
title: t("jobs.fields.updated_at"),
dataIndex: "updated_at",
key: "updated_at",
sorter: (a, b) => new Date(a.updated_at) - new Date(b.updated_at),
sortOrder:
state.sortedInfo.columnKey === "updated_at" && state.sortedInfo.order,
render: (text, record) => (
<DateTimeFormatter>{record.updated_at}</DateTimeFormatter>
)
//width: "12%",
//ellipsis: true
},
{
title: t("general.labels.actions"),
key: "actions",
render: (text, record) => (
<span>
<Button
onClick={() => {
deleteJob({ variables: { id: record.id } }).then(r => {
notification["success"]({
message: t("jobs.successes.deleted")
});
refetch();
});
}}
>
<Icon type="delete" />
</Button>
<Button
onClick={() => {
alert("Add");
}}
>
<Icon type="plus" />
</Button>
</span>
)
//width: "12%",
//ellipsis: true
}
];
return (
<Table
loading={loading}
title={() => {
return (
<div>
<Input.Search
placeholder="Search..."
onSearch={value => {
console.log(value);
}}
enterButton
/>
<Button
onClick={() => {
refetch();
}}
>
<Icon type="sync" />
</Button>
<Button
onClick={() => {
deleteAllNewJobs()
.then(r => {
notification["success"]({
message: t("jobs.successes.all_deleted", {
count: r.data.delete_available_jobs.affected_rows
})
});
refetch();
})
.catch(r => {
notification["error"]({
message: t("jobs.errors.deleted") + " " + r.message
});
});
}}
>
Delete All
</Button>
</div>
);
}}
size="small"
pagination={{ position: "top" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={data && data.available_jobs}
onChange={handleTableChange}
/>
);
}

View File

@@ -0,0 +1,30 @@
import React from "react";
import { useMutation, useQuery } from "react-apollo";
import { DELETE_ALL_AVAILABLE_SUPPLEMENT_JOBS, QUERY_AVAILABLE_SUPPLEMENT_JOBS } from "../../graphql/available-jobs.queries";
import AlertComponent from "../alert/alert.component";
import JobsAvailableSupplementComponent from "./jobs-available-supplement.component";
export default function JobsAvailableSupplementContainer({
deleteJob,
estDataLazyLoad
}) {
const { loading, error, data, refetch } = useQuery(
QUERY_AVAILABLE_SUPPLEMENT_JOBS,
{
fetchPolicy: "network-only"
}
);
const [deleteAllNewJobs] = useMutation(DELETE_ALL_AVAILABLE_SUPPLEMENT_JOBS);
if (error) return <AlertComponent type="error" message={error.message} />;
return (
<JobsAvailableSupplementComponent
loading={loading}
data={data}
refetch={refetch}
deleteJob={deleteJob}
deleteAllNewJobs={deleteAllNewJobs}
estDataLazyLoad={estDataLazyLoad}
/>
);
}

View File

@@ -0,0 +1,64 @@
import { Form, Input, Switch } from "antd";
import React, { useContext } from "react";
import { useTranslation } from "react-i18next";
import JobDetailFormContext from "../../pages/jobs-detail/jobs-detail.page.context";
export default function JobsDetailClaims({ job }) {
const form = useContext(JobDetailFormContext);
const { getFieldDecorator } = form;
const { t } = useTranslation();
return (
<div>
<Form.Item label={t("jobs.fields.csr")}>
{getFieldDecorator("csr", {
initialValue: job.csr
})(<Input name='csr' />)}
</Form.Item>
<Form.Item label={t("jobs.fields.loss_desc")}>
{getFieldDecorator("loss_desc", {
initialValue: job.loss_desc
})(<Input name='loss_desc' />)}
</Form.Item>
TODO: How to handle different taxes and marking them as exempt?
{
// <Form.Item label={t("jobs.fields.exempt")}>
// {getFieldDecorator("exempt", {
// initialValue: job.exempt
// })(<Input name='exempt' />)}
// </Form.Item>
}
<Form.Item label={t("jobs.fields.ponumber")}>
{getFieldDecorator("po_number", {
initialValue: job.po_number
})(<Input name='po_number' />)}
</Form.Item>
<Form.Item label={t("jobs.fields.unitnumber")}>
{getFieldDecorator("unit_number", {
initialValue: job.unit_number
})(<Input name='unit_number' />)}
</Form.Item>
<Form.Item label={t("jobs.fields.specialcoveragepolicy")}>
{getFieldDecorator("special_coverage_policy", {
initialValue: job.special_coverage_policy,
valuePropName: "checked"
})(<Switch name='special_coverage_policy' />)}
</Form.Item>
<Form.Item label={t("jobs.fields.kmin")}>
{getFieldDecorator("kmin", {
initialValue: job.kmin
})(<Input name='kmin' />)}
</Form.Item>
<Form.Item label={t("jobs.fields.kmout")}>
{getFieldDecorator("kmout", {
initialValue: job.kmout
})(<Input name='kmout' />)}
</Form.Item>
<Form.Item label={t("jobs.fields.referralsource")}>
{getFieldDecorator("referral_source", {
initialValue: job.referral_source
})(<Input name='referral_source' />)}
</Form.Item>
</div>
);
}

View File

@@ -0,0 +1,57 @@
import { Form, Input, InputNumber } from "antd";
import React, { useContext } from "react";
import { useTranslation } from "react-i18next";
import JobDetailFormContext from "../../pages/jobs-detail/jobs-detail.page.context";
export default function JobsDetailFinancials({ job }) {
const form = useContext(JobDetailFormContext);
const { getFieldDecorator } = form;
const { t } = useTranslation();
return (
<div>
<Form.Item label={t("jobs.fields.ded_amt")}>
{getFieldDecorator("ded_amt", {
initialValue: job.ded_amt
})(<InputNumber name="ded_amt" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.ded_status")}>
{getFieldDecorator("ded_status", {
initialValue: job.ded_status
})(<Input name="ded_status" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.depreciation_taxes")}>
{getFieldDecorator("depreciation_taxes", {
initialValue: job.depreciation_taxes
})(<InputNumber name="depreciation_taxes" />)}
</Form.Item>
TODO: This is equivalent of GST payable.
<Form.Item label={t("jobs.fields.federal_tax_payable")}>
{getFieldDecorator("federal_tax_payable", {
initialValue: job.federal_tax_payable
})(<InputNumber name="federal_tax_payable" />)}
</Form.Item>
TODO: equivalent of other customer amount
<Form.Item label={t("jobs.fields.other_amount_payable")}>
{getFieldDecorator("other_amount_payable", {
initialValue: job.other_amount_payable
})(<InputNumber name="other_amount_payable" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.towing_payable")}>
{getFieldDecorator("towing_payable", {
initialValue: job.towing_payable
})(<InputNumber name="towing_payable" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.storage_payable")}>
{getFieldDecorator("storage_payable", {
initialValue: job.storage_payable
})(<InputNumber name="storage_payable" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.adjustment_bottom_line")}>
{getFieldDecorator("adjustment_bottom_line", {
initialValue: job.adjustment_bottom_line
})(<InputNumber name="adjustment_bottom_line" />)}
</Form.Item>
</div>
);
}

View File

@@ -0,0 +1,125 @@
import {
Avatar,
Button,
Checkbox,
Descriptions,
notification,
PageHeader,
Tag
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import Moment from "react-moment";
import { Link } from "react-router-dom";
import CarImage from "../../assets/car.svg";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
export default function JobsDetailHeader({
job,
mutationConvertJob,
refetch,
getFieldDecorator
}) {
const { t } = useTranslation();
const tombstoneTitle = (
<div>
<Avatar size="large" alt="Vehicle Image" src={CarImage} />
{`${t("jobs.fields.ro_number")} ${
job.ro_number ? job.ro_number : t("general.labels.na")
}`}
</div>
);
const tombstoneSubtitle = (
<div>
<Tag color="red">
{job.owner ? (
<Link to={`/manage/owners/${job.owner.id}`}>
{`${job.ownr_co_nm || ""}${job.ownr_fn || ""} ${job.ownr_ln || ""}`}
</Link>
) : (
t("jobs.errors.noowner")
)}
</Tag>
<Tag color="green">
{job.vehicle ? (
<Link to={`/manage/vehicles/${job.vehicle.id}`}>
{job.vehicle.v_model_yr || t("general.labels.na")}{" "}
{job.vehicle.v_make_desc || t("general.labels.na")}{" "}
{job.vehicle.v_model_desc || t("general.labels.na")} |{" "}
{job.vehicle.plate_no || t("general.labels.na")} |{" "}
{job.vehicle.v_vin || t("general.labels.na")}
</Link>
) : null}
</Tag>
</div>
);
const menuExtra = [
<Button
key="convert"
type="dashed"
disabled={job.converted}
onClick={() => {
mutationConvertJob({
variables: { jobId: job.id }
}).then(r => {
refetch();
notification["success"]({
message: t("jobs.successes.converted")
});
});
}}
>
{t("jobs.actions.convert")}
</Button>,
<Button type="primary" key="submit" htmlType="submit">
{t("general.labels.save")}
</Button>
];
return (
<PageHeader
style={{
border: "1px solid rgb(235, 237, 240)"
}}
title={tombstoneTitle}
subTitle={tombstoneSubtitle}
tags={
<span key="job-status">
{job.job_status ? (
<Tag color="blue">{job.job_status.name}</Tag>
) : null}
</span>
}
extra={menuExtra}
>
<Descriptions size="small" column={5}>
<Descriptions.Item label={t("jobs.fields.repairtotal")}>
<CurrencyFormatter>{job.claim_total}</CurrencyFormatter>
</Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.customerowing")}>
##NO BINDING YET##
</Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.specialcoveragepolicy")}>
<Checkbox checked={job.special_coverage_policy} />
</Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.scheduled_completion")}>
{job.scheduled_completion ? (
<Moment format="MM/DD/YYYY">{job.scheduled_completion}</Moment>
) : null}
</Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.servicecar")}>
{job.service_car}
</Descriptions.Item>
</Descriptions>
</PageHeader>
);
}

View File

@@ -0,0 +1,150 @@
import { Divider, Form, Input, DatePicker } from "antd";
import React, { useContext } from "react";
import { useTranslation } from "react-i18next";
import JobDetailFormContext from "../../pages/jobs-detail/jobs-detail.page.context";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import FormItemPhone from "../form-items-formatted/phone-form-item.component";
import moment from "moment";
export default function JobsDetailInsurance({ job }) {
const form = useContext(JobDetailFormContext);
const { getFieldDecorator, getFieldValue } = form;
const { t } = useTranslation();
console.log("job.loss_date", job.loss_date);
return (
<div>
<Form.Item label={t("jobs.fields.ins_co_id")}>
{getFieldDecorator("ins_co_id", {
initialValue: job.ins_co_id
})(<Input name="ins_co_id" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.policy_no")}>
{getFieldDecorator("policy_no", {
initialValue: job.policy_no
})(<Input name="policy_no" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.clm_no")}>
{getFieldDecorator("clm_no", {
initialValue: job.clm_no
})(<Input name="clm_no" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.regie_number")}>
{getFieldDecorator("regie_number", {
initialValue: job.regie_number
})(<Input name="regie_number" />)}
</Form.Item>
TODO: missing KOL field???
<Form.Item label={t("jobs.fields.loss_date")}>
{getFieldDecorator("loss_date", {
initialValue: job.loss_date ? moment(job.loss_date) : null
})(<DatePicker name="loss_date" />)}
</Form.Item>
DAMAGE {JSON.stringify(job.area_of_damage)}
CAA # seems not correct based on field mapping Class seems not correct
based on field mapping
<Form.Item label={t("jobs.fields.ins_co_nm")}>
{getFieldDecorator("ins_co_nm", {
initialValue: job.ins_co_nm
})(<Input name="ins_co_nm" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.ins_addr1")}>
{getFieldDecorator("ins_addr1", {
initialValue: job.ins_addr1
})(<Input name="ins_addr1" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.ins_city")}>
{getFieldDecorator("ins_city", {
initialValue: job.ins_city
})(<Input name="ins_city" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.ins_ct_ln")}>
{getFieldDecorator("ins_ct_ln", {
initialValue: job.ins_ct_ln
})(<Input name="ins_ct_ln" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.ins_ct_fn")}>
{getFieldDecorator("ins_ct_fn", {
initialValue: job.ins_ct_fn
})(<Input name="ins_ct_fn" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.ins_ph1")}>
{getFieldDecorator("ins_ph1", {
initialValue: job.ins_ph1
})(<FormItemPhone customInput={Input} name="ins_ph1" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.ins_ea")}>
{getFieldDecorator("ins_ea", {
initialValue: job.ins_ea,
rules: [
{
type: "email",
message: "This is not a valid email address."
}
]
})(<FormItemEmail name="ins_ea" email={getFieldValue("ins_ea")} />)}
</Form.Item>
<Divider />
Appraiser Info
<Form.Item label={t("jobs.fields.est_co_nm")}>
{getFieldDecorator("est_co_nm", {
initialValue: job.est_co_nm
})(<Input name="est_co_nm" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.est_ct_fn")}>
{getFieldDecorator("est_ct_fn", {
initialValue: job.est_ct_fn
})(<Input name="est_ct_fn" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.est_ct_ln")}>
{getFieldDecorator("est_ct_ln", {
initialValue: job.est_ct_ln
})(<Input name="est_ct_ln" />)}
</Form.Item>
TODO: Field is pay date but title is inspection date. Likely incorrect?
<Form.Item label={t("jobs.fields.pay_date")}>
{getFieldDecorator("pay_date", {
initialValue: job.pay_date
})(<Input name="pay_date" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.est_ph1")}>
{getFieldDecorator("est_ph1", {
initialValue: job.est_ph1
})(<Input name="est_ph1" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.est_ea")}>
{getFieldDecorator("est_ea", {
initialValue: job.est_ea,
rules: [
{
type: "email",
message: "This is not a valid email address."
}
]
})(<FormItemEmail name="est_ea" email={getFieldValue("est_ea")} />)}
</Form.Item>
<Form.Item label={t("jobs.fields.selling_dealer")}>
{getFieldDecorator("selling_dealer", {
initialValue: job.selling_dealer
})(<Input name="selling_dealer" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.servicing_dealer")}>
{getFieldDecorator("servicing_dealer", {
initialValue: job.servicing_dealer
})(<Input name="servicing_dealer" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.selling_dealer_contact")}>
{getFieldDecorator("selling_dealer_contact", {
initialValue: job.selling_dealer_contact
})(<Input name="selling_dealer_contact" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.servicing_dealer_contact")}>
{getFieldDecorator("servicing_dealer_contact", {
initialValue: job.servicing_dealer_contact
})(<Input name="servicing_dealer_contact" />)}
</Form.Item>
TODO: Adding servicing/selling dealer contact info?
</div>
);
}

View File

@@ -0,0 +1,249 @@
import { Icon, Modal, notification, Upload } from "antd";
import axios from "axios";
import React, { useState } from "react";
import { useMutation } from "react-apollo";
import { useTranslation } from "react-i18next";
import Resizer from "react-image-file-resizer";
import {
INSERT_NEW_DOCUMENT,
DELETE_DOCUMENT
} from "../../graphql/documents.queries";
import "./jobs-documents.styles.scss";
import { generateCdnThumb } from "../../utils/DocHelpers";
function getBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
}
function JobsDocumentsComponent({ shopId, jobId, loading, data, currentUser }) {
const { t } = useTranslation();
const [insertNewDocument] = useMutation(INSERT_NEW_DOCUMENT);
const [deleteDocument] = useMutation(DELETE_DOCUMENT);
const [state, setState] = useState({
previewVisible: false,
previewImage: ""
});
const [fileList, setFileList] = useState(
data.reduce((acc, value) => {
acc.push({
uid: value.id,
url: value.thumb_url,
name: value.name,
status: "done",
full_url: value.url,
key: value.key
});
return acc;
}, [])
);
const uploadToS3 = (
fileName,
fileType,
file,
onError,
onSuccess,
onProgress
) => {
axios
.post("/sign_s3", {
fileName,
fileType
})
.then(response => {
var returnData = response.data.data.returnData;
var signedRequest = returnData.signedRequest;
var url = returnData.url;
setState({ ...state, url: url });
// Put the fileType in the headers for the upload
var options = {
headers: {
"Content-Type": fileType
},
onUploadProgress: e => {
onProgress({ percent: (e.loaded / e.total) * 100 });
}
};
axios
.put(signedRequest, file, options)
.then(response => {
console.log("response from axios", response);
insertNewDocument({
variables: {
docInput: [
{
jobid: jobId,
uploaded_by: currentUser.email,
url,
thumb_url: generateCdnThumb(fileName),
key: fileName
}
]
}
}).then(r => {
onSuccess({
uid: r.data.insert_documents.returning[0].id,
url: r.data.insert_documents.returning[0].thumb_url,
name: r.data.insert_documents.returning[0].name,
status: "done",
full_url: r.data.insert_documents.returning[0].url,
key: r.data.insert_documents.returning[0].key
});
notification["success"]({
message: t("documents.successes.insert")
});
});
setState({ ...state, success: true });
})
.catch(error => {
console.log("Error uploading to S3", error);
onError(error);
notification["error"]({
message: t("documents.errors.insert") + JSON.stringify(error)
});
});
})
.catch(error => {
console.log("Outside Error here.", error);
notification["error"]({
message: t("documents.errors.getpresignurl") + JSON.stringify(error)
});
});
};
const handleUpload = ev => {
const { onError, onSuccess, onProgress } = ev;
//If PDF, upload directly.
//If JPEG, resize and upload.
let key = `${shopId}/${jobId}/${ev.file.name}`;
if (ev.file.type === "application/pdf") {
console.log("It's a PDF.");
uploadToS3(key, ev.file.type, ev.file, onError, onSuccess, onProgress);
} else {
Resizer.imageFileResizer(
ev.file,
3000,
3000,
"JPEG",
75,
0,
uri => {
let file = new File([uri], ev.file.name, {});
file.uid = ev.file.uid;
uploadToS3(key, file.type, file, onError, onSuccess, onProgress);
},
"blob"
);
}
};
const handleCancel = () => setState({ ...state, previewVisible: false });
const handlePreview = async file => {
if (!file.full_url && !file.url) {
file.preview = await getBase64(file.originFileObj);
}
setState({
...state,
previewImage: file.full_url || file.url,
previewVisible: true
});
};
const handleChange = props => {
const { event, fileList, file } = props;
//Required to ensure that the state accurately reflects new data and that images can be deleted in feeded.
if (!event) {
//SPread the new file in where the old one was.
const newFileList = fileList.map(i =>
i.uid === file.uid ? Object.assign({}, i, file.response) : i
);
setFileList(newFileList);
} else {
setFileList(fileList);
}
};
const { previewVisible, previewImage } = state;
const handleRemove = file => {
console.log("file", file);
//Remove the file on S3
axios
.post("/delete_s3", { fileName: file.key })
.then(response => {
//Delete the record in our database.
if (response.status === 200) {
deleteDocument({ variables: { id: file.uid } }).then(r => {
notification["success"]({
message: t("documents.successes.delete")
});
});
} else {
notification["error"]({
message:
1 +
t("documents.errors.deletes3") +
JSON.stringify(response.message)
});
}
})
.catch(error => {
notification["error"]({
message: "2" + t("documents.errors.deletes3") + JSON.stringify(error)
});
});
};
return (
<div className='clearfix'>
<button
onClick={() => {
const imageRequest = JSON.stringify({
bucket: process.env.REACT_APP_S3_BUCKET,
key:
"52b7357c-0edd-4c95-85c3-dfdbcdfad9ac/c8ca5761-681a-4bb3-ab76-34c447357be3/Invoice_353284489.pdf",
edits: { format: "jpeg" }
});
const CloudFrontUrl = "https://d18fc493a0fm4o.cloudfront.net";
const url = `${CloudFrontUrl}/${btoa(imageRequest)}`;
console.log("url", url);
}}>
Test PDF
</button>
<Upload.Dragger
customRequest={handleUpload}
accept='.pdf,.jpg,.jpeg'
listType='picture-card'
fileList={fileList}
multiple={true}
onPreview={handlePreview}
onRemove={handleRemove}
onChange={handleChange}>
<p className='ant-upload-drag-icon'>
<Icon type='inbox' />
</p>
<p className='ant-upload-text'>
Click or drag file to this area to upload
</p>
<p className='ant-upload-hint'>
Support for a single or bulk upload. Strictly prohibit from uploading
company data or other band files
</p>
</Upload.Dragger>
<Modal visible={previewVisible} footer={null} onCancel={handleCancel}>
<img alt='example' style={{ width: "100%" }} src={previewImage} />
</Modal>
</div>
);
}
export default JobsDocumentsComponent;

View File

@@ -4,7 +4,8 @@ import { QUERY_SHOP_ID } from "../../graphql/bodyshop.queries";
import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import JobDocuments from "./jobs-documents.page";
import JobDocuments from "./jobs-documents.component";
import { GET_CURRENT_USER } from "../../graphql/local.queries";
export default function JobsDocumentsContainer({ jobId }) {
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
@@ -16,15 +17,22 @@ export default function JobsDocumentsContainer({ jobId }) {
fetchPolicy: "network-only"
});
if (loading || shopData.loading) return <LoadingSpinner />;
if (error) return <AlertComponent type='error' message={error.message} />;
if (shopData.error)
return <AlertComponent type='error' message={shopData.error.message} />;
const user = useQuery(GET_CURRENT_USER);
if (loading || shopData.loading || user.loading) return <LoadingSpinner />;
if (error || shopData.error || user.error)
return (
<AlertComponent
type='error'
message={error.message || shopData.error.message || user.error.message}
/>
);
return (
<JobDocuments
data={data.documents}
jobId={jobId}
currentUser={user.data.currentUser}
shopId={
shopData.data?.bodyshops[0]?.id
? shopData.data?.bodyshops[0]?.id

View File

@@ -1,204 +0,0 @@
import { Button, Icon, Modal, notification, Upload } from "antd";
import axios from "axios";
import React, { useState } from "react";
import { useMutation } from "react-apollo";
import { useTranslation } from "react-i18next";
import Resizer from "react-image-file-resizer";
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
import "./jobs-documents.styles.scss";
function getBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
}
function JobsDocumentsComponent({ shopId, jobId, loading, data }) {
const { t } = useTranslation();
const [insertNewDocument] = useMutation(INSERT_NEW_DOCUMENT);
const [state, setState] = useState({
previewVisible: false,
previewImage: ""
});
const [fileList, setFileList] = useState(
data.reduce((acc, value) => {
acc.push({
uid: value.id,
url: value.url,
name: value.name,
status: "done",
thumUrl:
"https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
});
return acc;
}, [])
);
const handleUpload = ev => {
const { onError, onSuccess, onProgress } = ev;
Resizer.imageFileResizer(
ev.file,
3000,
3000,
"JPEG",
75,
0,
uri => {
let file = new File([uri], ev.file.name, {});
file.uid = ev.file.uid;
// Split the filename to get the name and type
let fileName = file.name;
let fileType = file.type;
let key = `${shopId}/${jobId}/${fileName}`;
//URL is using the proxy set in pacakges.json.
axios
.post("/sign_s3", {
fileName: key,
fileType
})
.then(response => {
var returnData = response.data.data.returnData;
var signedRequest = returnData.signedRequest;
var url = returnData.url;
setState({ ...state, url: url });
// Put the fileType in the headers for the upload
var options = {
headers: {
"Content-Type": fileType
},
onUploadProgress: e => {
onProgress({ percent: (e.loaded / e.total) * 100 });
}
};
axios
.put(signedRequest, file, options)
.then(response => {
onSuccess(response.body);
insertNewDocument({
variables: {
docInput: [
{
jobid: jobId,
uploaded_by: "patrick@bodyshop.app",
url,
thumb_url: url
}
]
}
}).then(r => {
console.log(r);
notification["success"]({
message: t("documents.successes.insert")
});
});
setState({ ...state, success: true });
})
.catch(error => {
console.log("Error uploading to S3", error);
onError(error);
notification["error"]({
message: t("documents.errors.insert") + JSON.stringify(error)
});
});
})
.catch(error => {
console.log("Outside Error here.", error);
notification["error"]({
message:
t("documents.errors.getpresignurl") + JSON.stringify(error)
});
});
},
"blob"
);
};
const handleCancel = () => setState({ ...state, previewVisible: false });
const handlePreview = async file => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj);
}
setState({
...state,
previewImage: file.url || file.preview,
previewVisible: true
});
};
const handleChange = props => {
const { fileList } = props;
console.log("New fileList", fileList);
setFileList(fileList);
};
const { previewVisible, previewImage } = state;
// const uploadButton = (
// <div>
// <Icon type='plus' />
// <div className='ant-upload-text'>{t("documents.labels.upload")}</div>
// </div>
// );
console.log(
"process.env.REACT_APP_S3_BUCKET",
process.env.REACT_APP_S3_BUCKET
);
const imageRequest = JSON.stringify({
bucket: process.env.REACT_APP_S3_BUCKET,
key:
"52b7357c-0edd-4c95-85c3-dfdbcdfad9ac/f11e92a4-8a7d-4ec0-86ac-2f46b631e438/thumb-1920-459857.jpg",
edits: {
resize: {
height: 100,
width: 100
}
}
});
const CloudFrontUrl = "https://d18fc493a0fm4o.cloudfront.net";
const url = `${CloudFrontUrl}/${btoa(imageRequest)}`;
return (
<div className='clearfix'>
<Button
onClick={() => {
console.log("btn click");
console.log("data", data);
}}>
Test Request
</Button>
<img src={url} alt='test' />
<Upload.Dragger
customRequest={handleUpload}
accept='.pdf,.jpg,.jpeg'
listType='picture-card'
fileList={fileList}
multiple={true}
onPreview={handlePreview}
onChange={handleChange}>
<p className='ant-upload-drag-icon'>
<Icon type='inbox' />
</p>
<p className='ant-upload-text'>
Click or drag file to this area to upload
</p>
<p className='ant-upload-hint'>
Support for a single or bulk upload. Strictly prohibit from uploading
company data or other band files
</p>
</Upload.Dragger>
<Modal visible={previewVisible} footer={null} onCancel={handleCancel}>
<img alt='example' style={{ width: "100%" }} src={previewImage} />
</Modal>
</div>
);
}
export default JobsDocumentsComponent;

View File

@@ -25,6 +25,7 @@ export default withRouter(function JobsList({
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
width: "8%",
// onFilter: (value, record) => record.ro_number.includes(value),
// filteredValue: state.filteredInfo.text || null,
sorter: (a, b) => alphaSort(a, b),
@@ -34,7 +35,7 @@ export default withRouter(function JobsList({
render: (text, record) => (
<span>
<Link to={"/manage/jobs/" + record.id}>
{record.ro_number ? record.ro_number : t("general.labels.na")}
{record.ro_number ? record.ro_number : "EST-" + record.est_number}
</Link>
</span>
)
@@ -45,6 +46,7 @@ export default withRouter(function JobsList({
key: "owner",
ellipsis: true,
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
width: "25%",
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
@@ -64,6 +66,7 @@ export default withRouter(function JobsList({
title: t("jobs.fields.phone1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
width: "12%",
ellipsis: true,
render: (text, record) => {
return record.ownr_ph1 ? (
@@ -86,12 +89,13 @@ export default withRouter(function JobsList({
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
width: "10%",
ellipsis: true,
sorter: (a, b) => alphaSort(a, b),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => {
return record.job_status?.name ?? t("general.labels.na");
return record.job_status?.name || t("general.labels.na");
}
},
@@ -99,6 +103,7 @@ export default withRouter(function JobsList({
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
width: "15%",
ellipsis: true,
render: (text, record) => {
return record.vehicle ? (
@@ -115,6 +120,7 @@ export default withRouter(function JobsList({
title: t("vehicles.fields.plate_no"),
dataIndex: "plate_no",
key: "plate_no",
width: "8%",
ellipsis: true,
sorter: (a, b) => alphaSort(a, b),
sortOrder:
@@ -131,6 +137,7 @@ export default withRouter(function JobsList({
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
width: "12%",
ellipsis: true,
sorter: (a, b) => alphaSort(a, b),
sortOrder:
@@ -147,11 +154,12 @@ export default withRouter(function JobsList({
title: t("jobs.fields.clm_total"),
dataIndex: "clm_total",
key: "clm_total",
sorter: (a, b) => {
return a > b;
},
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
width: "8%",
// sorter: (a, b) => {
// return a > b;
// },
// sortOrder:
// state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
render: (text, record) => {
return record.clm_total ? (
<span>{record.clm_total}</span>
@@ -164,6 +172,7 @@ export default withRouter(function JobsList({
title: t("jobs.fields.owner_owing"),
dataIndex: "owner_owing",
key: "owner_owing",
width: "8%",
render: (text, record) => {
return record.owner_owing ? (
<span>{record.owner_owing}</span>
@@ -202,7 +211,9 @@ export default withRouter(function JobsList({
return (
<Input.Search
placeholder='Search...'
onSearch={value => console.log(value)}
onSearch={value => {
console.log(value);
}}
enterButton
/>
);

View File

@@ -0,0 +1,5 @@
import React from "react";
export default function JobsRatesComponent() {
return <div>Jobs Rates Comp</div>;
}

View File

@@ -0,0 +1,6 @@
import React from "react";
import JobsRatesComponent from "./jobs-rates.component";
export default function JobsRatesContainer() {
return <JobsRatesComponent />;
}

View File

@@ -1,27 +0,0 @@
import React from "react";
import { useTranslation } from "react-i18next";
import i18next from "i18next";
import { Dropdown, Menu, Icon } from "antd";
export default function LanguageSelector() {
const { t } = useTranslation();
const handleMenuClick = e => {
i18next.changeLanguage(e.key, (err, t) => {
if (err)
return console.log("Error encountered when changing languages.", err);
});
};
const menu = (
<Menu onClick={handleMenuClick}>
<Menu.Item key="en_us">{t("general.languages.english")}</Menu.Item>
<Menu.Item key="fr">{t("general.languages.french")}</Menu.Item>
<Menu.Item key="es">{t("general.languages.spanish")}</Menu.Item>
</Menu>
);
return (
<Dropdown overlay={menu}>
<Icon type="global" />
</Dropdown>
);
}

View File

@@ -2,6 +2,16 @@ import React from "react";
import { Spin } from "antd";
import "./loading-spinner.styles.scss";
export default function LoadingSpinner() {
return <Spin className="loading-spinner" size="large" delay="500" />;
export default function LoadingSpinner({ loading = true, message, ...props }) {
return (
<Spin
spinning={loading}
className="loading-spinner"
size="large"
//delay="500"
tip={message ? message : null}
>
{props.children}
</Spin>
);
}

View File

@@ -37,12 +37,12 @@ export default function NoteUpsertModalContainer({
]
}
}).then(r => {
refetch();
changeVisibility(!visible);
notification["success"]({
message: t("notes.successes.create")
});
});
refetch();
changeVisibility(!visible);
};
const updateExistingNote = () => {

View File

@@ -0,0 +1,125 @@
import { Checkbox, Divider, Table } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import PhoneFormatter from "../../utils/PhoneFormatter";
export default function OwnerFindModalComponent({
selectedOwner,
setSelectedOwner,
ownersListLoading,
ownersList
}) {
//setSelectedOwner is used to set the record id of the owner to use for adding the job.
const { t } = useTranslation();
const columns = [
{
title: t("owners.fields.ownr_ln"),
dataIndex: "ownr_ln",
key: "ownr_ln"
//width: "8%",
// onFilter: (value, record) => record.ro_number.includes(value),
// // filteredValue: state.filteredInfo.text || null,
// sorter: (a, b) => alphaSort(a, b),
// sortOrder:
// state.sortedInfo.columnKey === "cieca_id" && state.sortedInfo.order
},
{
title: t("owners.fields.ownr_fn"),
dataIndex: "ownr_fn",
key: "ownr_fn"
// ellipsis: true,
// sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
// //width: "25%",
// sortOrder:
// state.sortedInfo.columnKey === "ownr_name" && state.sortedInfo.order
},
{
title: t("owners.fields.ownr_addr1"),
dataIndex: "ownr_addr1",
key: "ownr_addr1"
// sorter: (a, b) => alphaSort(a.vehicle_info, b.vehicle_info),
// sortOrder:
// state.sortedInfo.columnKey === "vehicle_info" && state.sortedInfo.order
//ellipsis: true
},
{
title: t("owners.fields.ownr_city"),
dataIndex: "ownr_city",
key: "ownr_city"
// sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
// sortOrder:
// state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order
//width: "12%",
//ellipsis: true
},
{
title: t("owners.fields.ownr_ea"),
dataIndex: "ownr_ea",
key: "ownr_ea"
// sorter: (a, b) => a.clm_amt - b.clm_amt,
// sortOrder:
// state.sortedInfo.columnKey === "clm_amt" && state.sortedInfo.order
//width: "12%",
//ellipsis: true
},
{
title: t("owners.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
render: (text, record) => (
<PhoneFormatter>{record.ownr_ph1}</PhoneFormatter>
)
// sorter: (a, b) => alphaSort(a.uploaded_by, b.uploaded_by),
// sortOrder:
// state.sortedInfo.columnKey === "uploaded_by" && state.sortedInfo.order
//width: "12%",
//ellipsis: true
}
];
const handleOnRowClick = record => {
if (record) {
if (record.id) {
setSelectedOwner(record.id);
return;
}
}
setSelectedOwner(null);
};
return (
<div>
<Table
title={() => t("owners.labels.existing_owners")}
size="small"
pagination={{ position: "bottom" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
loading={ownersListLoading}
dataSource={ownersList}
rowSelection={{
onSelect: props => {
setSelectedOwner(props.id);
},
type: "radio",
selectedRowKeys: [selectedOwner]
}}
onRow={(record, rowIndex) => {
return {
onClick: event => {
handleOnRowClick(record);
}
};
}}
/>
<Divider />
<Checkbox
checked={selectedOwner ? false : true}
onClick={() => setSelectedOwner(null)}
>
Create a new Owner record for this job.
</Checkbox>
</div>
);
}

View File

@@ -0,0 +1,53 @@
import { Modal } from "antd";
import React from "react";
import { useQuery } from "react-apollo";
import { useTranslation } from "react-i18next";
import { QUERY_SEARCH_OWNER_BY_IDX } from "../../graphql/owners.queries";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import OwnerFindModalComponent from "./owner-find-modal.component";
export default function OwnerFindModalContainer({
loading,
error,
owner,
selectedOwner,
setSelectedOwner,
...modalProps
}) {
//use owner object to run query and find what possible owners there are.
const { t } = useTranslation();
const ownersList = useQuery(QUERY_SEARCH_OWNER_BY_IDX, {
variables: {
search: owner
? `${owner.ownr_fn} ${owner.ownr_ln} ${owner.ownr_addr1} ${owner.ownr_city} ${owner.ownr_zip} ${owner.ownr_ea} ${owner.ownr_ph1} ${owner.ownr_ph2}`
: null
},
skip: !owner,
fetchPolicy: "network-only"
});
return (
<Modal
title={t("owners.labels.existing_owners")}
width={"80%"}
{...modalProps}
>
{loading ? <LoadingSpinner /> : null}
{error ? <AlertComponent message={error.message} type="error" /> : null}
{owner ? (
<OwnerFindModalComponent
selectedOwner={selectedOwner}
setSelectedOwner={setSelectedOwner}
ownersListLoading={ownersList.loading}
ownersList={
ownersList.data && ownersList.data.search_owners
? ownersList.data.search_owners
: null
}
/>
) : null}
</Modal>
);
}

View File

@@ -1,12 +1,9 @@
import React, { Component } from "react";
//import { Redirect } from "react-router-dom";
import React from "react";
import { useTranslation } from "react-i18next";
import firebase from "../../firebase/firebase.utils";
export default class SignOut extends Component {
state = {
redirect: false
};
signOut = async () => {
export default function SignoutComponent() {
const signOut = async () => {
try {
await firebase.auth().signOut();
// this.setState({
@@ -17,17 +14,7 @@ export default class SignOut extends Component {
}
};
renderRedirect = () => {
if (this.state.redirect) {
//return <Redirect to="/signin" />;
}
};
render() {
return (
<div>
{this.renderRedirect()}
<div onClick={this.signOut}>Sign Out</div>
</div>
);
}
const { t } = useTranslation();
return <div onClick={signOut}>{t("user.actions.signout")}</div>;
}

View File

@@ -63,11 +63,11 @@ export default function WhiteBoardCard({ metadata }) {
<div>
<Card
title={
(metadata.ro_number ?? metadata.est_number) +
(metadata.ro_number || metadata.est_number) +
" | " +
(metadata.owner?.first_name ?? "") +
(metadata.owner?.first_name || "") +
" " +
(metadata.owner?.last_name ?? "")
(metadata.owner?.last_name || "")
}
style={{ width: 300, marginTop: 10 }}
bodyStyle={{ padding: 10 }}
@@ -87,15 +87,15 @@ export default function WhiteBoardCard({ metadata }) {
<Col span={18}>
<Row>
<WrappedSpan>
{metadata.vehicle?.v_model_yr ?? t("general.labels.na")}{" "}
{metadata.vehicle?.v_make_desc ?? t("general.labels.na")}{" "}
{metadata.vehicle?.v_model_desc ?? t("general.labels.na")}
{metadata.vehicle?.v_model_yr || t("general.labels.na")}{" "}
{metadata.vehicle?.v_make_desc || t("general.labels.na")}{" "}
{metadata.vehicle?.v_model_desc || t("general.labels.na")}
</WrappedSpan>
</Row>
{metadata.vehicle?.v_vin ? (
<Row>
<WrappedSpan>
VIN: {metadata.vehicle?.v_vin ?? t("general.labels.na")}
VIN: {metadata.vehicle?.v_vin || t("general.labels.na")}
</WrappedSpan>
</Row>
) : null}

View File

@@ -1,23 +1,13 @@
import firebase from "firebase/app";
import "firebase/firestore";
import "firebase/auth";
import "firebase/database"
const config = {
apiKey: "AIzaSyDV9MsSHZmpLtjoaTK_ObvjFaJ-nMSd2KA",
authDomain: "bodyshop-dev-b1cb6.firebaseapp.com",
databaseURL: "https://bodyshop-dev-b1cb6.firebaseio.com",
projectId: "bodyshop-dev-b1cb6",
storageBucket: "bodyshop-dev-b1cb6.appspot.com",
messagingSenderId: "922785209028",
appId: "1:922785209028:web:96e9df15401eee5d784791",
measurementId: "G-2D5378VCHE"
};
import "firebase/database";
const config = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
firebase.initializeApp(config);
export const createUserProfileDocument = async (userAuth, additionalData) => {
//Needs to be redone to write to GQL database.
//Needs to be redone to write to GQL database.
console.log("userAuth from firebase Utils", userAuth);
if (!userAuth) return;

View File

@@ -2,13 +2,13 @@ import { onError } from "apollo-link-error";
import { Observable } from "apollo-link";
import { auth } from "../firebase/firebase.utils";
//https://stackoverflow.com/questions/57163454/refreshing-a-token-with-apollo-client-firebase-auth
const errorLink = onError(
({ graphQLErrors, networkError, operation, forward }) => {
let access_token = window.localStorage.getItem("token");
console.log("graphQLErrors", graphQLErrors);
console.log("networkError", networkError);
console.log("operation", operation);
console.log("forward", forward);
// console.log("graphQLErrors", graphQLErrors);
// console.log("networkError", networkError);
// console.log("operation", operation);
// console.log("forward", forward);
let expired = false;
@@ -26,45 +26,68 @@ const errorLink = onError(
//User access token has expired
//props.history.push("/network-error");
console.log("We need a new token!");
if (access_token && access_token !== "undefined") {
// Let's refresh token through async request
return new Observable(observer => {
auth.currentUser
.getIdToken(true)
.then(function(idToken) {
if (!idToken) {
window.localStorage.removeItem("token");
return console.log("Refresh token has expired");
}
console.log("Got a new token", idToken);
window.localStorage.setItem("token", idToken);
// Let's refresh token through async request
// reset the headers
operation.setContext(({ headers = {} }) => ({
headers: {
// Re-add old headers
...headers,
// Switch out old access token for new one
authorization: idToken ? `Bearer ${idToken}` : ""
}
}));
auth.currentUser.getIdToken(true).then(token => {
if (token) {
console.log("Got the new token.", token);
window.localStorage.setItem("token", token);
operation.setContext(({ headers = {} }) => ({
headers: {
...headers,
authorization: token ? `Bearer ${token}` : ""
}
}));
const subscriber = {
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer)
};
console.log("About to resend the request.");
// Retry last failed request
forward(operation).subscribe(subscriber);
})
.catch(error => {
// No refresh or client token available, we force user to login
console.log("Hit an error.");
observer.error(error);
});
});
}
return new Observable(observer => {
const subscriber = {
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer)
};
console.log("About to resend the request.");
// Retry last failed request
forward(operation).subscribe(subscriber);
});
}
});
// return new Observable(observer => {
// auth.currentUser
// .getIdToken(true)
// .then(function(idToken) {
// if (!idToken) {
// window.localStorage.removeItem("token");
// return console.log("Refresh token has expired");
// }
// console.log("Got a new token", idToken);
// window.localStorage.setItem("token", idToken);
// // reset the headers
// operation.setContext(({ headers = {} }) => ({
// headers: {
// // Re-add old headers
// ...headers,
// // Switch out old access token for new one
// authorization: idToken ? `Bearer ${idToken}` : ""
// }
// }));
// const subscriber = {
// next: observer.next.bind(observer),
// error: observer.error.bind(observer),
// complete: observer.complete.bind(observer)
// };
// console.log("About to resend the request.");
// // Retry last failed request
// forward(operation).subscribe(subscriber);
// })
// .catch(error => {
// // No refresh or client token available, we force user to login
// console.log("Hit an error.");
// observer.error(error);
// });
// });
}
}
);

View File

@@ -0,0 +1,81 @@
import { gql } from "apollo-boost";
export const QUERY_AVAILABLE_NEW_JOBS = gql`
query QUERY_AVAILABLE_NEW_JOBS {
available_jobs(
where: { issupplement: { _eq: false } }
order_by: { updated_at: desc }
) {
cieca_id
clm_amt
clm_no
created_at
id
issupplement
ownr_name
source_system
supplement_number
updated_at
uploaded_by
vehicle_info
}
}
`;
export const QUERY_AVAILABLE_SUPPLEMENT_JOBS = gql`
query QUERY_AVAILABLE_SUPPLEMENT_JOBS {
available_jobs(
where: { issupplement: { _eq: true } }
order_by: { updated_at: desc }
) {
cieca_id
clm_amt
clm_no
created_at
id
issupplement
ownr_name
source_system
supplement_number
updated_at
uploaded_by
vehicle_info
job {
ro_number
}
}
}
`;
export const DELETE_AVAILABLE_JOB = gql`
mutation DELETE_AVAILABLE_JOB($id: uuid) {
delete_available_jobs(where: { id: { _eq: $id } }) {
affected_rows
}
}
`;
export const DELETE_ALL_AVAILABLE_NEW_JOBS = gql`
mutation DELETE_ALL_AVAILABLE_NEW_JOBS {
delete_available_jobs(where: { issupplement: { _eq: false } }) {
affected_rows
}
}
`;
export const DELETE_ALL_AVAILABLE_SUPPLEMENT_JOBS = gql`
mutation DELETE_ALL_AVAILABLE_NEW_JOBS {
delete_available_jobs(where: { issupplement: { _eq: true } }) {
affected_rows
}
}
`;
export const QUERY_AVAILABLE_NEW_JOBS_EST_DATA_BY_PK = gql`
query QUERY_AVAILABLE_NEW_JOBS_EST_DATA_BY_PK($id: uuid!) {
available_jobs_by_pk(id: $id) {
id
est_data
}
}
`;

View File

@@ -7,6 +7,7 @@ export const GET_DOCUMENTS_BY_JOB = gql`
url
thumb_url
name
key
}
}
`;
@@ -14,6 +15,20 @@ export const GET_DOCUMENTS_BY_JOB = gql`
export const INSERT_NEW_DOCUMENT = gql`
mutation INSERT_NEW_DOCUMENT($docInput: [documents_insert_input!]!) {
insert_documents(objects: $docInput) {
returning {
id
url
thumb_url
name
key
}
}
}
`;
export const DELETE_DOCUMENT = gql`
mutation DELETE_DOCUMENT($id: uuid) {
delete_documents(where: { id: { _eq: $id } }) {
returning {
id
}

View File

@@ -3,5 +3,6 @@ export default {
currentUser: null,
selectedNavItem: "Home",
recentItems: [],
bodyShopData: null
bodyShopData: null,
language: "en_us"
};

View File

@@ -1,34 +1,7 @@
import { gql } from "apollo-boost";
export const GET_ALL_OPEN_JOBS = gql`
query GET_ALL_OPEN_JOBS {
jobs {
id
est_number
ro_number
job_status {
id
name
}
scheduled_completion
scheduled_delivery
vehicle {
id
v_model_yr
v_make_desc
v_model_desc
plate_no
}
owner {
first_name
last_name
}
}
}
`;
export const SUBSCRIPTION_ALL_OPEN_JOBS = gql`
subscription SUBSCRIPTION_ALL_OPEN_JOBS {
export const QUERY_ALL_OPEN_JOBS = gql`
query QUERY_ALL_OPEN_JOBS {
jobs {
ownr_fn
ownr_ln
@@ -89,30 +62,6 @@ export const SUBSCRIPTION_ALL_OPEN_JOBS = gql`
}
`;
export const QUERY_JOBS_IN_PRODUCTION = gql`
query QUERY_JOBS_IN_PRODUCTION {
jobs {
id
updated_at
est_number
ro_number
scheduled_completion
scheduled_delivery
vehicle {
id
v_model_yr
v_make_desc
v_model_desc
plate_no
}
owner {
first_name
last_name
}
}
}
`;
export const SUBSCRIPTION_JOBS_IN_PRODUCTION = gql`
subscription SUBSCRIPTION_JOBS_IN_PRODUCTION {
job_status(
@@ -149,36 +98,19 @@ export const SUBSCRIPTION_JOBS_IN_PRODUCTION = gql`
export const GET_JOB_BY_PK = gql`
query GET_JOB_BY_PK($id: uuid!) {
jobs_by_pk(id: $id) {
actual_completion
actual_delivery
actual_in
created_at
est_number
id
local_tax_rate
owner {
id
first_name
last_name
phone
}
est_co_nm
est_ph1
est_ea
est_ct_fn
est_ct_ln
regie_number
ro_number
scheduled_completion
scheduled_in
service_car
csr
loss_desc
kmin
kmout
referral_source
unit_number
po_number
special_coverage_policy
scheduled_delivery
job_status {
id
name
}
updated_at
claim_total
deductible
converted
est_number
ro_number
vehicle {
id
plate_no
@@ -188,6 +120,62 @@ export const GET_JOB_BY_PK = gql`
v_make_desc
v_color
}
ins_co_id
policy_no
loss_date
area_of_damage
ins_co_nm
ins_addr1
ins_city
ins_ct_ln
ins_ct_fn
ins_ea
ins_ph1
est_co_nm
est_ct_fn
est_ct_ln
pay_date
est_ph1
est_ea
selling_dealer
servicing_dealer
selling_dealer_contact
servicing_dealer_contact
regie_number
scheduled_completion
id
ded_amt
ded_status
depreciation_taxes
federal_tax_payable
other_amount_payable
towing_payable
storage_payable
adjustment_bottom_line
ownr_fn
ownr_ln
owner{
id
ownr_fn
ownr_ln
}
joblines{
id
unq_seq
line_ind
line_desc
part_type
oem_partno
db_price
act_price
part_qty
mod_lbr_ty
db_hrs
mod_lb_hrs
lbr_op
lbr_amt
}
}
}
`;
@@ -246,6 +234,10 @@ export const QUERY_JOB_CARD_DETAILS = gql`
updated_at
claim_total
ded_amt
documents(limit: 3, order_by: { created_at: desc }) {
id
thumb_url
}
vehicle {
id
plate_no
@@ -282,3 +274,13 @@ export const CONVERT_JOB_TO_RO = gql`
}
}
`;
export const INSERT_NEW_JOB = gql`
mutation INSERT_JOB($job: [jobs_insert_input!]!) {
insert_jobs(objects: $job) {
returning {
id
}
}
}
`;

View File

@@ -1,13 +1,5 @@
import { gql } from "apollo-boost";
export const SET_CURRENT_USER = gql`
mutation SetCurrentUser($user: User!) {
setCurrentUser(user: $user) @client {
email
}
}
`;
export const GET_CURRENT_USER = gql`
query GET_CURRENT_USER {
currentUser @client {
@@ -26,16 +18,8 @@ export const GET_CURRENT_SELECTED_NAV_ITEM = gql`
}
`;
export const GET_WHITE_BOARD_LEFT_SIDER_VISIBLE = gql`
{
whiteBoardLeftSiderVisible @client
}
`;
export const GET_BODYSHOP = gql`
query LOCAL_GET_BODY_SHOP {
bodyShopData @client {
shopname
}
export const GET_LANGUAGE = gql`
query GET_USER_LANGUAGE {
language @client
}
`;

View File

@@ -0,0 +1,20 @@
import { gql } from "apollo-boost";
export const QUERY_SEARCH_OWNER_BY_IDX = gql`
query QUERY_SEARCH_OWNER_BY_IDX($search: String!) {
search_owners(args: { search: $search }) {
ownr_fn
ownr_ln
ownr_ph1
ownr_ph2
ownr_addr1
ownr_addr2
ownr_city
ownr_ctry
ownr_ea
ownr_st
ownr_zip
id
}
}
`;

View File

@@ -0,0 +1,23 @@
import React from "react";
import JobsAvailableContainer from "../../components/jobs-available-new/jobs-available-new.container";
import JobsAvailableSupplementContainer from "../../components/jobs-available-supplement/jobs-available-supplement.container";
export default function JobsAvailablePageComponent({
deleteJob,
estDataLazyLoad
}) {
return (
<div>
Available New Jobs
<JobsAvailableContainer
deleteJob={deleteJob}
estDataLazyLoad={estDataLazyLoad}
/>
Available Supplements (//TODO: LOGIC HAS NOT YET BEEN COPIED FROM OTHER COMPONENT)
<JobsAvailableSupplementContainer
deleteJob={deleteJob}
estDataLazyLoad={estDataLazyLoad}
/>
</div>
);
}

View File

@@ -0,0 +1,26 @@
import React from "react";
import { useMutation, useLazyQuery } from "react-apollo";
import {
DELETE_AVAILABLE_JOB,
QUERY_AVAILABLE_NEW_JOBS_EST_DATA_BY_PK
} from "../../graphql/available-jobs.queries";
import JobsAvailablePageComponent from "./jobs-available.page.component";
export default function JobsAvailablePageContainer() {
const [deleteJob] = useMutation(DELETE_AVAILABLE_JOB);
const estDataLazyLoad = useLazyQuery(
QUERY_AVAILABLE_NEW_JOBS_EST_DATA_BY_PK,
{
fetchPolicy: "network-only"
}
);
return (
<div>
<JobsAvailablePageComponent
deleteJob={deleteJob}
estDataLazyLoad={estDataLazyLoad}
/>
</div>
);
}

View File

@@ -0,0 +1,173 @@
import { Alert, Button, Form, Icon, Tabs } from "antd";
import React, { useContext } from "react";
import { useTranslation } from "react-i18next";
import {
FaHardHat,
FaInfo,
FaRegStickyNote,
FaShieldAlt
} from "react-icons/fa";
import JobsLines from '../../components/job-detail-lines/job-lines.component'
import JobsDetailClaims from "../../components/jobs-detail-claims/jobs-detail-claims.component";
import JobsDetailFinancials from "../../components/jobs-detail-financial/jobs-detail-financial.component";
import JobsDetailHeader from "../../components/jobs-detail-header/jobs-detail-header.component";
import JobsDetailInsurance from "../../components/jobs-detail-insurance/jobs-detail-insurance.component";
import JobsDocumentsContainer from "../../components/jobs-documents/jobs-documents.container";
import JobNotesContainer from "../../components/jobs-notes/jobs-notes.container";
import JobDetailFormContext from "./jobs-detail.page.context";
export default function JobsDetailPage({
job,
mutationUpdateJob,
mutationConvertJob,
handleSubmit,
refetch
}) {
const { t } = useTranslation();
const { isFieldsTouched, resetFields } = useContext(JobDetailFormContext);
const formItemLayout = {
labelCol: {
xs: { span: 12 },
sm: { span: 5 }
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 }
}
};
return (
<Form onSubmit={handleSubmit} {...formItemLayout}>
<JobsDetailHeader
job={job}
mutationConvertJob={mutationConvertJob}
refetch={refetch}
/>
{isFieldsTouched() ? (
<Alert
message={
<div>
{t("general.messages.unsavedchanges")}
<Button onClick={() => resetFields()}>
{t("general.actions.reset")}
</Button>
</div>
}
closable
/>
) : null}
<Tabs defaultActiveKey="claimdetail">
<Tabs.TabPane
tab={
<span>
<Icon component={FaInfo} />
{t("menus.jobsdetail.claimdetail")}
</span>
}
key="claimdetail"
>
<JobsDetailClaims job={job} />
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<Icon component={FaShieldAlt} />
{t("menus.jobsdetail.insurance")}
</span>
}
key="insurance"
>
<JobsDetailInsurance job={job} />
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<Icon type="bars" />
{t("menus.jobsdetail.repairdata")}
</span>
}
key="repairdata"
>
<JobsLines job={job} />
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<Icon type="dollar" />
{t("menus.jobsdetail.financials")}
</span>
}
key="financials"
>
<JobsDetailFinancials job={job} />
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<Icon type="tool" />
{t("menus.jobsdetail.partssublet")}
</span>
}
key="partssublet"
>
Partssublet
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<Icon component={FaHardHat} />
{t("menus.jobsdetail.labor")}
</span>
}
key="labor"
>
Labor
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<Icon type="calendar" />
{t("menus.jobsdetail.dates")}
</span>
}
key="dates"
>
Dates
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<Icon type="file-image" />
{t("jobs.labels.documents")}
</span>
}
key="#documents"
>
<JobsDocumentsContainer jobId={job.id} />
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<Icon component={FaRegStickyNote} />
{t("jobs.labels.notes")}
</span>
}
key="#notes"
>
<JobNotesContainer jobId={job.id} />
</Tabs.TabPane>
</Tabs>
</Form>
);
}

View File

@@ -1,38 +1,76 @@
import { useQuery } from "@apollo/react-hooks";
import { Form, notification } from "antd";
import React, { useEffect } from "react";
import { useMutation, useQuery } from "react-apollo";
import { useTranslation } from "react-i18next";
import AlertComponent from "../../components/alert/alert.component";
import SpinComponent from "../../components/loading-spinner/loading-spinner.component";
import { GET_JOB_BY_PK } from "../../graphql/jobs.queries";
import JobsDetailPage from "./jobs-detail.page";
import { CONVERT_JOB_TO_RO, GET_JOB_BY_PK, UPDATE_JOB } from "../../graphql/jobs.queries";
import JobsDetailPage from "./jobs-detail.page.component";
import JobDetailFormContext from "./jobs-detail.page.context";
function JobsDetailPageContainer({ match, location }) {
function JobsDetailPageContainer({ match, form }) {
const { jobId } = match.params;
const { hash } = location;
const { t } = useTranslation();
const { loading, error, data } = useQuery(GET_JOB_BY_PK, {
const { loading, error, data, refetch } = useQuery(GET_JOB_BY_PK, {
variables: { id: jobId },
fetchPolicy: "network-only"
});
const [mutationUpdateJob] = useMutation(UPDATE_JOB);
const [mutationConvertJob] = useMutation(CONVERT_JOB_TO_RO);
useEffect(() => {
document.title = loading
? "..."
: error
? t("titles.app")
: t("titles.jobsdetail", {
ro_number: data.jobs_by_pk.ro_number
});
}, [loading, data, t]);
}, [loading, data, t, error]);
const handleSubmit = e => {
e.preventDefault();
form.validateFieldsAndScroll((err, values) => {
if (err) {
notification["error"]({
message: t("jobs.errors.validationtitle"),
description: t("jobs.errors.validation")
});
}
if (!err) {
mutationUpdateJob({
variables: { jobId: data.jobs_by_pk.id, job: values }
}).then(r => {
notification["success"]({
message: t("jobs.successes.savetitle")
});
//TODO: Better way to reset the field decorators?
refetch().then(r => form.resetFields());
});
}
});
};
if (loading) return <SpinComponent />;
if (error) return <AlertComponent message={error.message} type='error' />;
return (
<JobsDetailPage
hash={hash ? hash.substring(1) : "#lines"}
data={data}
jobId={jobId}
match={match}
/>
return data.jobs_by_pk ? (
<JobDetailFormContext.Provider value={form}>
<JobsDetailPage
job={data.jobs_by_pk}
mutationUpdateJob={mutationUpdateJob}
mutationConvertJob={mutationConvertJob}
handleSubmit={handleSubmit}
getFieldDecorator={form.getFieldDecorator}
refetch={refetch}
/>
</JobDetailFormContext.Provider>
) : (
<AlertComponent message={t("jobs.errors.noaccess")} type='error' />
);
}
export default JobsDetailPageContainer;
export default Form.create({ name: "JobsDetailPageContainer" })(
JobsDetailPageContainer
);

View File

@@ -0,0 +1,3 @@
import React from "react";
const JobDetailFormContext = React.createContext(null);
export default JobDetailFormContext;

View File

@@ -1,80 +0,0 @@
import { Icon, Row, Tabs } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { FaRegStickyNote } from "react-icons/fa";
import { withRouter } from "react-router-dom";
import JobLinesContainer from "../../components/job-lines/job-lines.container.component";
import JobTombstone from "../../components/job-tombstone/job-tombstone.component";
import JobsDocumentsContainer from "../../components/jobs-documents/jobs-documents.container";
import JobNotesContainer from "../../components/jobs-notes/jobs-notes.container";
function JobsDetailPage({ jobId, hash, data, match, history }) {
const { t } = useTranslation();
console.log("hash", hash);
return (
<div>
<Row>
<JobTombstone job={data.jobs_by_pk} />
</Row>
<Row>
<Tabs
defaultActiveKey={`#${hash}`}
onChange={p => {
history.push(p);
}}>
<Tabs.TabPane
tab={
<span>
<Icon type='bars' />
{t("jobs.labels.lines")}
</span>
}
key='#lines'>
<JobLinesContainer match={match} />
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<Icon type='dollar' />
{t("jobs.labels.rates")}
</span>
}
key='#rates'>
Estimate Rates
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<Icon type='tool1' />
{t("jobs.labels.parts")}
</span>
}
key='#parts'>
Estimate Parts
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<Icon type='file-image' />
{t("jobs.labels.documents")}
</span>
}
key='#documents'>
<JobsDocumentsContainer jobId={jobId} />
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<Icon component={FaRegStickyNote} />
{t("jobs.labels.notes")}
</span>
}
key='#notes'>
<JobNotesContainer jobId={jobId} />
</Tabs.TabPane>
</Tabs>
</Row>
</div>
);
}
export default withRouter(JobsDetailPage);

View File

@@ -1,15 +1,15 @@
import React, { useEffect, useState } from "react";
import { useSubscription } from "@apollo/react-hooks";
import { useQuery } from "@apollo/react-hooks";
import AlertComponent from "../../components/alert/alert.component";
import { Col } from "antd";
import { SUBSCRIPTION_ALL_OPEN_JOBS } from "../../graphql/jobs.queries";
import { QUERY_ALL_OPEN_JOBS } from "../../graphql/jobs.queries";
import { useTranslation } from "react-i18next";
import JobsList from "../../components/jobs-list/jobs-list.component";
import JobDetailCards from "../../components/job-detail-cards/job-detail-cards.component";
//TODO: Implement pagination for this.
export default function JobsPage({ match, location }) {
const { loading, error, data } = useSubscription(SUBSCRIPTION_ALL_OPEN_JOBS, {
const { loading, error, data } = useQuery(QUERY_ALL_OPEN_JOBS, {
fetchPolicy: "network-only"
});
const { t } = useTranslation();
@@ -20,7 +20,6 @@ export default function JobsPage({ match, location }) {
const { hash } = location;
const [selectedJob, setSelectedJob] = useState(hash ? hash.substr(1) : null);
console.log("Jobs Page Render.");
if (error) return <AlertComponent message={error.message} type='error' />;
return (

View File

@@ -8,17 +8,21 @@ import HeaderContainer from "../../components/header/header.container";
import FooterComponent from "../../components/footer/footer.component";
import ErrorBoundary from "../../components/error-boundary/error-boundary.component";
import './manage.page.styles.scss'
import "./manage.page.styles.scss";
import ChatWindowContainer from "../../components/chat-window/chat-window.container";
const WhiteBoardPage = lazy(() => import("../white-board/white-board.page"));
const JobsPage = lazy(() => import("../jobs/jobs.page"));
const JobsDetailPage = lazy(() => import("../jobs-detail/jobs-detail.page.container"));
const JobsDetailPage = lazy(() =>
import("../jobs-detail/jobs-detail.page.container")
);
const ProfilePage = lazy(() => import("../profile/profile.container.page"));
const JobsDocumentsPage = lazy(() =>
import("../../components/jobs-documents/jobs-documents.container")
);
const JobsAvailablePage = lazy(() =>
import("../jobs-available/jobs-available.page.container")
);
const { Header, Content, Footer } = Layout;
//This page will handle all routing for the entire application.
@@ -35,13 +39,14 @@ export default function Manage({ match }) {
<HeaderContainer />
</Header>
<Content className="content-container">
<Content className='content-container'>
<ErrorBoundary>
<Suspense
fallback={<div>TODO: Suspended Loading in Manage Page...</div>}>
<Route exact path={`${match.path}`} component={WhiteBoardPage} />
<Route exact path={`${match.path}/jobs`} component={JobsPage} />
<Route
exact
path={`${match.path}/jobs/:jobId`}
@@ -57,11 +62,18 @@ export default function Manage({ match }) {
path={`${match.path}/profile`}
component={ProfilePage}
/>
<Route
exact
path={`${match.path}/available`}
component={JobsAvailablePage}
/>
</Suspense>
</ErrorBoundary>
</Content>
<Footer>
<ChatWindowContainer />
<FooterComponent />
</Footer>
<BackTop />

View File

@@ -1,4 +1,3 @@
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
configure({ adapter: new Adapter() });

View File

@@ -2,18 +2,25 @@
"translation": {
"documents": {
"errors": {
"deletes3": "Error deleting document from storage. ",
"getpresignurl": "Error obtaining presigned URL for document. ",
"insert": "Unable to upload file."
"insert": "Unable to upload file.",
"nodocuments": "There are no documents."
},
"labels": {
"upload": "Upload"
},
"successes": {
"delete": "Document deleted successfully.",
"insert": "Uploaded document successfully. "
}
},
"general": {
"actions": {
"reset": "Reset to original."
},
"labels": {
"actions": "Actions",
"in": "In",
"loading": "Loading...",
"na": "N/A",
@@ -25,17 +32,33 @@
"english": "English",
"french": "French",
"spanish": "Spanish"
},
"messages": {
"unsavedchanges": "You have unsaved changes."
}
},
"joblines": {
"fields": {
"act_price": "Actual Price",
"db_price": "Database Price",
"line_desc": "Line Description",
"part_type": "Part Type",
"unq_seq": "Seq #"
}
},
"jobs": {
"actions": {
"addDocuments": "Add Job Documents",
"addNote": "Add Note",
"convert": "Convert",
"postInvoices": "Post Invoices",
"printCenter": "Print Center"
},
"errors": {
"creating": "Error encountered while creating job. {{error}}",
"deleted": "Error deleting job.",
"noaccess": "This job does not exist or you do not have access to it.",
"nodates": "No dates specified for this job.",
"nojobselected": "No job is selected.",
"noowner": "No owner associated.",
"novehicle": "No vehicle associated.",
@@ -44,25 +67,83 @@
"validationtitle": "Validation Error"
},
"fields": {
"actual_completion": "Actual Completion",
"actual_delivery": "Actual Delivery",
"actual_in": "Actual In",
"adjustment_bottom_line": "Adjustments",
"cieca_id": "CIECA ID",
"claim_total": "Claim Total",
"clm_no": "Claim #",
"clm_total": "Claim Total",
"deductible": "Deductible",
"csr": "Customer Service Rep.",
"customerowing": "Customer Owing",
"date_closed": "Closed",
"date_estimated": "Date Estimated",
"date_exported": "Exported",
"date_invoiced": "Invoiced",
"date_open": "Open",
"date_scheduled": "Scheduled",
"ded_amt": "Deductible",
"ded_status": "Deductible Status",
"depreciation_taxes": "Depreciation/Taxes",
"est_addr1": "Appraiser Address",
"est_co_nm": "Appraiser",
"est_ct_fn": "Appraiser First Name",
"est_ct_ln": "Appraiser Last Name",
"est_ea": "Appraiser Email",
"est_number": "Estimate Number",
"est_ph1": "Appraiser Phone #",
"federal_tax_payable": "Federal Tax Payable",
"ins_addr1": "Insurance Co. Address",
"ins_city": "Insurance City",
"ins_co_id": "Insurance Co. ID",
"ins_co_nm": "Insurance Company Name",
"ins_ct_fn": "File Handler First Name",
"ins_ct_ln": "File Handler Last Name",
"ins_ea": "File Handler Email",
"ins_ph1": "File Handler Phone #",
"kmin": "Mileage In",
"kmout": "Mileage Out",
"loss_date": "Loss Date",
"loss_desc": "Loss of Use",
"other_amount_payable": "Other Amount Payable",
"owner": "Owner",
"owner_owing": "Cust. Owes",
"ownr_ea": "Email",
"pay_date": "Inspection Date",
"phone1": "Phone 1",
"phoneshort": "PH",
"policy_no": "Policy #",
"ponumber": "PO Number",
"referralsource": "Referral Source",
"regie_number": "Registration #",
"repairtotal": "Repair Total",
"ro_number": "RO #",
"scheduled_completion": "Scheduled Completion",
"scheduled_delivery": "Scheduled Delivery",
"scheduled_in": "Scheduled In",
"selling_dealer": "Selling Dealer",
"selling_dealer_contact": "Selling Dealer Contact",
"servicecar": "Service Car",
"servicing_dealer": "Servicing Dealer",
"servicing_dealer_contact": "Servicing Dealer Contact",
"specialcoveragepolicy": "Special Coverage Policy",
"status": "Job Status",
"storage_payable": "Storage/PVRT",
"towing_payable": "Towing Payable",
"unitnumber": "Unit #",
"updated_at": "Updated At",
"uploaded_by": "Uploaded By",
"vehicle": "Vehicle"
},
"labels": {
"available_new_jobs": "",
"cards": {
"appraiser": "Appraiser",
"customer": "Customer Information",
"damage": "Area of Damage",
"dates": "Dates",
"documents": "Documents",
"documents": "Recent Documents",
"estimator": "Estimator",
"filehandler": "File Handler",
"insurance": "Insurance Details",
@@ -71,7 +152,7 @@
"totals": "Totals",
"vehicle": "Vehicle"
},
"convert": "Convert",
"creating_new_job": "Creating new job...",
"documents": "Documents",
"lines": "Estimate Lines",
"notes": "Notes",
@@ -80,7 +161,10 @@
"vehicle_info": "Vehicle"
},
"successes": {
"all_deleted": "{{count}} jobs deleted successfully.",
"converted": "Job converted successfully.",
"created": "Job created successfully. Click to view.",
"deleted": "Job deleted successfully.",
"save": "Record Saved",
"savetitle": "Record saved successfully."
}
@@ -90,6 +174,21 @@
"languageselector": "Language",
"profile": "Profile"
},
"header": {
"activejobs": "Active Jobs",
"availablejobs": "Available Jobs",
"home": "Home",
"jobs": "Jobs"
},
"jobsdetail": {
"claimdetail": "Claim Details",
"dates": "Dates",
"financials": "Financials",
"insurance": "Insurance",
"labor": "Labor",
"partssublet": "Parts/Sublet",
"repairdata": "Repair Data"
},
"profilesidebar": {
"profile": "My Profile",
"shops": "My Shops"
@@ -113,11 +212,24 @@
"newnoteplaceholder": "Add a note..."
},
"successes": {
"created": "Note created successfully.",
"create": "Note created successfully.",
"deleted": "Note deleted successfully.",
"updated": "Note updated successfully."
}
},
"owners": {
"fields": {
"ownr_addr1": "Address",
"ownr_city": "City",
"ownr_ea": "Email",
"ownr_fn": "First Name",
"ownr_ln": "Last Name",
"ownr_ph1": "Phone 1"
},
"labels": {
"existing_owners": "Existing Owners"
}
},
"profile": {
"errors": {
"state": "Error reading page state. Please refresh."
@@ -130,6 +242,11 @@
"jobsdocuments": "Job Documents {{ro_number}} | $t(titles.app)",
"profile": "My Profile | $t(titles.app)"
},
"user": {
"actions": {
"signout": "Sign Out"
}
},
"vehicles": {
"fields": {
"plate_no": "License Plate"

View File

@@ -2,18 +2,25 @@
"translation": {
"documents": {
"errors": {
"deletes3": "Error al eliminar el documento del almacenamiento.",
"getpresignurl": "Error al obtener la URL prescrita para el documento.",
"insert": "Incapaz de cargar el archivo."
"insert": "Incapaz de cargar el archivo.",
"nodocuments": "No hay documentos"
},
"labels": {
"upload": "Subir"
},
"successes": {
"delete": "Documento eliminado con éxito.",
"insert": "Documento cargado con éxito."
}
},
"general": {
"actions": {
"reset": "Restablecer a original."
},
"labels": {
"actions": "Comportamiento",
"in": "en",
"loading": "Cargando...",
"na": "N / A",
@@ -25,17 +32,33 @@
"english": "Inglés",
"french": "francés",
"spanish": "español"
},
"messages": {
"unsavedchanges": "Usted tiene cambios no guardados."
}
},
"joblines": {
"fields": {
"act_price": "Precio actual",
"db_price": "Precio de base de datos",
"line_desc": "Descripción de línea",
"part_type": "Tipo de parte",
"unq_seq": "Seq #"
}
},
"jobs": {
"actions": {
"addDocuments": "Agregar documentos de trabajo",
"addNote": "Añadir la nota",
"convert": "Convertir",
"postInvoices": "Contabilizar facturas",
"printCenter": "Centro de impresión"
},
"errors": {
"creating": "",
"deleted": "Error al eliminar el trabajo.",
"noaccess": "Este trabajo no existe o no tiene acceso a él.",
"nodates": "No hay fechas especificadas para este trabajo.",
"nojobselected": "No hay trabajo seleccionado.",
"noowner": "Ningún propietario asociado.",
"novehicle": "No hay vehículo asociado.",
@@ -44,25 +67,83 @@
"validationtitle": "Error de validacion"
},
"fields": {
"actual_completion": "Realización real",
"actual_delivery": "Entrega real",
"actual_in": "Real en",
"adjustment_bottom_line": "Ajustes",
"cieca_id": "CIECA ID",
"claim_total": "Reclamar total",
"clm_no": "Reclamación #",
"clm_total": "Reclamar total",
"deductible": "Deducible",
"csr": "Representante de servicio al cliente.",
"customerowing": "Cliente debido",
"date_closed": "Cerrado",
"date_estimated": "Fecha estimada",
"date_exported": "Exportado",
"date_invoiced": "Facturado",
"date_open": "Abierto",
"date_scheduled": "Programado",
"ded_amt": "Deducible",
"ded_status": "Estado deducible",
"depreciation_taxes": "Depreciación / Impuestos",
"est_addr1": "Dirección del tasador",
"est_co_nm": "Tasador",
"est_ct_fn": "Nombre del tasador",
"est_ct_ln": "Apellido del tasador",
"est_ea": "Correo electrónico del tasador",
"est_number": "Numero Estimado",
"est_ph1": "Número de teléfono del tasador",
"federal_tax_payable": "Impuesto federal por pagar",
"ins_addr1": "Dirección de Insurance Co.",
"ins_city": "Ciudad de seguros",
"ins_co_id": "ID de la compañía de seguros",
"ins_co_nm": "Nombre de la compañía de seguros",
"ins_ct_fn": "Nombre del controlador de archivos",
"ins_ct_ln": "Apellido del manejador de archivos",
"ins_ea": "Correo electrónico del controlador de archivos",
"ins_ph1": "File Handler Phone #",
"kmin": "Kilometraje en",
"kmout": "Kilometraje",
"loss_date": "Fecha de pérdida",
"loss_desc": "Perdida de uso",
"other_amount_payable": "Otra cantidad a pagar",
"owner": "Propietario",
"owner_owing": "Cust. Debe",
"ownr_ea": "Email",
"pay_date": "Fecha de inspección",
"phone1": "Teléfono 1",
"phoneshort": "PH",
"policy_no": "Política #",
"ponumber": "numero postal",
"referralsource": "Fuente de referencia",
"regie_number": "N. ° de registro",
"repairtotal": "Reparación total",
"ro_number": "RO #",
"scheduled_completion": "Finalización programada",
"scheduled_delivery": "Entrega programada",
"scheduled_in": "Programado en",
"selling_dealer": "Distribuidor vendedor",
"selling_dealer_contact": "Contacto con el vendedor",
"servicecar": "Auto de servicio",
"servicing_dealer": "Distribuidor de servicio",
"servicing_dealer_contact": "Servicio Contacto con el concesionario",
"specialcoveragepolicy": "Política de cobertura especial",
"status": "Estado del trabajo",
"storage_payable": "Almacenamiento / PVRT",
"towing_payable": "Remolque a pagar",
"unitnumber": "Unidad #",
"updated_at": "Actualizado en",
"uploaded_by": "Subido por",
"vehicle": "Vehículo"
},
"labels": {
"available_new_jobs": "",
"cards": {
"appraiser": "Tasador",
"customer": "Información al cliente",
"damage": "Área de Daño",
"dates": "fechas",
"documents": "documentos",
"documents": "Documentos recientes",
"estimator": "Estimador",
"filehandler": "File Handler",
"insurance": "detalles del seguro",
@@ -71,7 +152,7 @@
"totals": "Totales",
"vehicle": "Vehículo"
},
"convert": "Convertir",
"creating_new_job": "Creando nuevo trabajo ...",
"documents": "documentos",
"lines": "Líneas estimadas",
"notes": "Notas",
@@ -80,7 +161,10 @@
"vehicle_info": "Vehículo"
},
"successes": {
"all_deleted": "{{count}} trabajos eliminados con éxito.",
"converted": "Trabajo convertido con éxito.",
"created": "Trabajo creado con éxito. Click para ver.",
"deleted": "Trabajo eliminado con éxito.",
"save": "Registro guardado",
"savetitle": "Registro guardado con éxito."
}
@@ -90,6 +174,21 @@
"languageselector": "idioma",
"profile": "Perfil"
},
"header": {
"activejobs": "Empleos activos",
"availablejobs": "Trabajos disponibles",
"home": "Casa",
"jobs": "Trabajos"
},
"jobsdetail": {
"claimdetail": "Detalles de la reclamación",
"dates": "fechas",
"financials": "finanzas",
"insurance": "Seguro",
"labor": "Labor",
"partssublet": "Piezas / Subarrendamiento",
"repairdata": "Datos de reparación"
},
"profilesidebar": {
"profile": "Mi perfil",
"shops": "Mis tiendas"
@@ -113,11 +212,24 @@
"newnoteplaceholder": "Agrega una nota..."
},
"successes": {
"created": "Nota creada con éxito.",
"create": "Nota creada con éxito.",
"deleted": "Nota eliminada con éxito.",
"updated": "Nota actualizada con éxito."
}
},
"owners": {
"fields": {
"ownr_addr1": "Dirección",
"ownr_city": "ciudad",
"ownr_ea": "Email",
"ownr_fn": "Nombre de pila",
"ownr_ln": "Apellido",
"ownr_ph1": ""
},
"labels": {
"existing_owners": "Propietarios existentes"
}
},
"profile": {
"errors": {
"state": "Error al leer el estado de la página. Porfavor refresca."
@@ -130,6 +242,11 @@
"jobsdocuments": "Documentos de trabajo {{ro_number}} | $ t (títulos.app)",
"profile": "Mi perfil | $t(titles.app)"
},
"user": {
"actions": {
"signout": "desconectar"
}
},
"vehicles": {
"fields": {
"plate_no": "Placa"

View File

@@ -2,18 +2,25 @@
"translation": {
"documents": {
"errors": {
"deletes3": "Erreur lors de la suppression du document du stockage.",
"getpresignurl": "Erreur lors de l'obtention de l'URL présignée pour le document.",
"insert": "Incapable de télécharger le fichier."
"insert": "Incapable de télécharger le fichier.",
"nodocuments": "Il n'y a pas de documents."
},
"labels": {
"upload": "Télécharger"
},
"successes": {
"delete": "Le document a bien été supprimé.",
"insert": "Document téléchargé avec succès."
}
},
"general": {
"actions": {
"reset": "Rétablir l'original."
},
"labels": {
"actions": "actes",
"in": "dans",
"loading": "Chargement...",
"na": "N / A",
@@ -25,17 +32,33 @@
"english": "Anglais",
"french": "Francais",
"spanish": "Espanol"
},
"messages": {
"unsavedchanges": "Vous avez des changements non enregistrés."
}
},
"joblines": {
"fields": {
"act_price": "Prix actuel",
"db_price": "Prix de la base de données",
"line_desc": "Description de la ligne",
"part_type": "Type de pièce",
"unq_seq": "Seq #"
}
},
"jobs": {
"actions": {
"addDocuments": "Ajouter des documents de travail",
"addNote": "Ajouter une note",
"convert": "Convertir",
"postInvoices": "Poster des factures",
"printCenter": "Centre d'impression"
},
"errors": {
"creating": "",
"deleted": "Erreur lors de la suppression du travail.",
"noaccess": "Ce travail n'existe pas ou vous n'y avez pas accès.",
"nodates": "Aucune date spécifiée pour ce travail.",
"nojobselected": "Aucun travail n'est sélectionné.",
"noowner": "Aucun propriétaire associé.",
"novehicle": "Aucun véhicule associé.",
@@ -44,25 +67,83 @@
"validationtitle": "Erreur de validation"
},
"fields": {
"actual_completion": "Achèvement réel",
"actual_delivery": "Livraison réelle",
"actual_in": "En réel",
"adjustment_bottom_line": "Ajustements",
"cieca_id": "CIECA ID",
"claim_total": "Total réclamation",
"clm_no": "Prétendre #",
"clm_total": "Total réclamation",
"deductible": "Déductible",
"csr": "représentant du service à la clientèle",
"customerowing": "Client propriétaire",
"date_closed": "Fermé",
"date_estimated": "Date estimée",
"date_exported": "Exportés",
"date_invoiced": "Facturé",
"date_open": "Ouvrir",
"date_scheduled": "Prévu",
"ded_amt": "Déductible",
"ded_status": "Statut de franchise",
"depreciation_taxes": "Amortissement / taxes",
"est_addr1": "Adresse de l'évaluateur",
"est_co_nm": "Expert",
"est_ct_fn": "Prénom de l'évaluateur",
"est_ct_ln": "Nom de l'évaluateur",
"est_ea": "Courriel de l'évaluateur",
"est_number": "Numéro d'estimation",
"est_ph1": "Numéro de téléphone de l'évaluateur",
"federal_tax_payable": "Impôt fédéral à payer",
"ins_addr1": "Adresse Insurance Co.",
"ins_city": "Insurance City",
"ins_co_id": "ID de la compagnie d'assurance",
"ins_co_nm": "Nom de la compagnie d'assurance",
"ins_ct_fn": "Prénom du gestionnaire de fichiers",
"ins_ct_ln": "Nom du gestionnaire de fichiers",
"ins_ea": "Courriel du gestionnaire de fichiers",
"ins_ph1": "Numéro de téléphone du gestionnaire de fichiers",
"kmin": "Kilométrage en",
"kmout": "Kilométrage hors",
"loss_date": "Date de perte",
"loss_desc": "Perte d'usage",
"other_amount_payable": "Autre montant à payer",
"owner": "Propriétaire",
"owner_owing": "Cust. Owes",
"ownr_ea": "Email",
"pay_date": "Date d'inspection",
"phone1": "Téléphone 1",
"phoneshort": "PH",
"policy_no": "Politique #",
"ponumber": "Numéro de bon de commande",
"referralsource": "Source de référence",
"regie_number": "Enregistrement #",
"repairtotal": "Réparation totale",
"ro_number": "RO #",
"scheduled_completion": "Achèvement planifié",
"scheduled_delivery": "Livraison programmée",
"scheduled_in": "Planifié dans",
"selling_dealer": "Revendeur vendeur",
"selling_dealer_contact": "Contacter le revendeur",
"servicecar": "Voiture de service",
"servicing_dealer": "Concessionnaire",
"servicing_dealer_contact": "Contacter le concessionnaire",
"specialcoveragepolicy": "Politique de couverture spéciale",
"status": "Statut de l'emploi",
"storage_payable": "Stockage / PVRT",
"towing_payable": "Remorquage à payer",
"unitnumber": "Unité #",
"updated_at": "Mis à jour à",
"uploaded_by": "Telechargé par",
"vehicle": "Véhicule"
},
"labels": {
"available_new_jobs": "",
"cards": {
"appraiser": "Expert",
"customer": "Informations client",
"damage": "Zone de dommages",
"dates": "Rendez-vous",
"documents": "Les documents",
"documents": "Documents récents",
"estimator": "Estimateur",
"filehandler": "Gestionnaire de fichiers",
"insurance": "Détails de l'assurance",
@@ -71,7 +152,7 @@
"totals": "Totaux",
"vehicle": "Véhicule"
},
"convert": "Convertir",
"creating_new_job": "Création d'un nouvel emploi ...",
"documents": "Les documents",
"lines": "Estimer les lignes",
"notes": "Remarques",
@@ -80,7 +161,10 @@
"vehicle_info": "Véhicule"
},
"successes": {
"all_deleted": "{{count}} travaux supprimés avec succès.",
"converted": "Travail converti avec succès.",
"created": "Le travail a été créé avec succès. Clique pour voir.",
"deleted": "Le travail a bien été supprimé.",
"save": "Enregistrement enregistré",
"savetitle": "Enregistrement enregistré avec succès."
}
@@ -90,6 +174,21 @@
"languageselector": "La langue",
"profile": "Profil"
},
"header": {
"activejobs": "Emplois actifs",
"availablejobs": "Emplois disponibles",
"home": "Accueil",
"jobs": "Emplois"
},
"jobsdetail": {
"claimdetail": "Détails de la réclamation",
"dates": "Rendez-vous",
"financials": "Financiers",
"insurance": "Assurance",
"labor": "La main d'oeuvre",
"partssublet": "Pièces / Sous-location",
"repairdata": "Données de réparation"
},
"profilesidebar": {
"profile": "Mon profil",
"shops": "Mes boutiques"
@@ -113,11 +212,24 @@
"newnoteplaceholder": "Ajouter une note..."
},
"successes": {
"created": "Remarque créée avec succès.",
"create": "Remarque créée avec succès.",
"deleted": "Remarque supprimée avec succès.",
"updated": "Remarque mise à jour avec succès."
}
},
"owners": {
"fields": {
"ownr_addr1": "Adresse",
"ownr_city": "Ville",
"ownr_ea": "Email",
"ownr_fn": "Prénom",
"ownr_ln": "Nom de famille",
"ownr_ph1": ""
},
"labels": {
"existing_owners": "Propriétaires existants"
}
},
"profile": {
"errors": {
"state": "Erreur lors de la lecture de l'état de la page. Rafraichissez, s'il vous plait."
@@ -130,6 +242,11 @@
"jobsdocuments": "Documents de travail {{ro_number}} | $ t (titres.app)",
"profile": "Mon profil | $t(titles.app)"
},
"user": {
"actions": {
"signout": "Déconnexion"
}
},
"vehicles": {
"fields": {
"plate_no": "Plaque d'immatriculation"

View File

@@ -0,0 +1,13 @@
import React from "react";
import NumberFormat from "react-number-format";
export default function CurrencyFormatter(props) {
return (
<NumberFormat
thousandSeparator={true}
prefix={"$"}
value={props.children}
displayType={"text"}
/>
);
}

View File

@@ -0,0 +1,10 @@
import React from "react";
import Moment from "react-moment";
export function DateFormatter(props) {
return <Moment format="MM/DD/YYYY">{props.children || ""}</Moment>;
}
export function DateTimeFormatter(props) {
return <Moment format="MM/DD/YYYY @ HH:mm">{props.children || ""}</Moment>;
}

View File

@@ -0,0 +1,13 @@
export const generateCdnThumb = key => {
const imageRequest = JSON.stringify({
bucket: process.env.REACT_APP_S3_BUCKET,
key: key,
edits: {
resize: {
height: 100,
width: 100
}
}
});
return `${process.env.REACT_APP_S3_CDN}/${btoa(imageRequest)}`;
};

View File

@@ -79,6 +79,15 @@
"@apollo/react-hooks" "^3.1.3"
tslib "^1.10.0"
"@apollo/react-testing@^3.1.3":
version "3.1.3"
resolved "https://registry.yarnpkg.com/@apollo/react-testing/-/react-testing-3.1.3.tgz#d8dd318f58fb6a404976bfa3f8a79e976a5c6562"
integrity sha512-58R7gROl4TZMHm5kS76Nof9FfZhD703AU3SmJTA2f7naiMqC9Qd8pZ4oNCBafcab0SYN//UtWvLcluK5O7V/9g==
dependencies:
"@apollo/react-common" "^3.1.3"
fast-json-stable-stringify "^2.0.0"
tslib "^1.10.0"
"@babel/code-frame@7.5.5", "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d"
@@ -11051,6 +11060,8 @@ rxjs@^6.4.0, rxjs@^6.5.3:
version "6.5.4"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c"
integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==
dependencies:
tslib "^1.9.0"
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"

View File

@@ -1 +1,2 @@
endpoint: https://bodyshop-dev-db.herokuapp.com
#endpoint: https://bodyshop-staging-db.herokuapp.com/

View File

@@ -0,0 +1,3 @@
- args:
sql: ALTER TABLE "public"."documents" DROP COLUMN "key";
type: run_sql

View File

@@ -0,0 +1,3 @@
- args:
sql: ALTER TABLE "public"."documents" ADD COLUMN "key" text NOT NULL DEFAULT '0';
type: run_sql

View File

@@ -0,0 +1,36 @@
- args:
role: user
table:
name: documents
schema: public
type: drop_insert_permission
- args:
permission:
check:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
columns:
- name
- thumb_url
- uploaded_by
- url
- created_at
- updated_at
- id
- jobid
localPresets:
- key: ""
value: ""
set: {}
role: user
table:
name: documents
schema: public
type: create_insert_permission

View File

@@ -0,0 +1,37 @@
- args:
role: user
table:
name: documents
schema: public
type: drop_insert_permission
- args:
permission:
check:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
columns:
- created_at
- id
- jobid
- key
- name
- thumb_url
- updated_at
- uploaded_by
- url
localPresets:
- key: ""
value: ""
set: {}
role: user
table:
name: documents
schema: public
type: create_insert_permission

View File

@@ -0,0 +1,34 @@
- args:
role: user
table:
name: documents
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- name
- thumb_url
- uploaded_by
- url
- created_at
- updated_at
- id
- jobid
computed_fields: []
filter:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
role: user
table:
name: documents
schema: public
type: create_select_permission

View File

@@ -0,0 +1,35 @@
- args:
role: user
table:
name: documents
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- created_at
- id
- jobid
- key
- name
- thumb_url
- updated_at
- uploaded_by
- url
computed_fields: []
filter:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
role: user
table:
name: documents
schema: public
type: create_select_permission

View File

@@ -0,0 +1,36 @@
- args:
role: user
table:
name: documents
schema: public
type: drop_update_permission
- args:
permission:
columns:
- name
- thumb_url
- uploaded_by
- url
- created_at
- updated_at
- id
- jobid
filter:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
localPresets:
- key: ""
value: ""
set: {}
role: user
table:
name: documents
schema: public
type: create_update_permission

View File

@@ -0,0 +1,37 @@
- args:
role: user
table:
name: documents
schema: public
type: drop_update_permission
- args:
permission:
columns:
- created_at
- id
- jobid
- key
- name
- thumb_url
- updated_at
- uploaded_by
- url
filter:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
localPresets:
- key: ""
value: ""
set: {}
role: user
table:
name: documents
schema: public
type: create_update_permission

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,4 @@
- args:
cascade: true
sql: "CREATE EXTENSION pg_trgm;\r\n"
type: run_sql

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,7 @@
- args:
cascade: true
sql: "CREATE INDEX jobs_gin_idx ON jobs\r\nUSING GIN ((est_number || ' ' || ro_number
\ || ' ' || clm_no || ' ' || ownr_ln || ' ' || ownr_fn || ' ' || ownr_ph1
\r\n|| ' ' || ownr_ea \r\n|| ' ' || insd_ln \r\n|| ' ' || insd_fn \r\n|| ' '
|| insd_ea \r\n|| ' ' || insd_ph1 ) gin_trgm_ops);"
type: run_sql

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,16 @@
- args:
cascade: true
sql: "CREATE FUNCTION search_jobs(search text)\r\nRETURNS SETOF jobs AS $$\r\n
\ SELECT *\r\n FROM jobs\r\n WHERE\r\n search <% (est_number ||
' ' || ro_number || ' ' || clm_no || ' ' || ownr_ln || ' ' || ownr_fn ||
' ' || ownr_ph1 \r\n|| ' ' || ownr_ea \r\n|| ' ' || insd_ln \r\n|| ' ' || insd_fn
\r\n|| ' ' || insd_ea \r\n|| ' ' || insd_ph1 )\r\n ORDER BY\r\n similarity(search,
(est_number || ' ' || ro_number || ' ' || clm_no || ' ' || ownr_ln || '
' || ownr_fn || ' ' || ownr_ph1 \r\n|| ' ' || ownr_ea \r\n|| ' ' || insd_ln
\r\n|| ' ' || insd_fn \r\n|| ' ' || insd_ea \r\n|| ' ' || insd_ph1 )) DESC\r\n
\ LIMIT 50;\r\n$$ LANGUAGE sql STABLE;"
type: run_sql
- args:
name: search_jobs
schema: public
type: track_function

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,4 @@
- args:
cascade: true
sql: 'drop index jobs_gin_idx '
type: run_sql

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,4 @@
- args:
cascade: true
sql: drop function search_jobs
type: run_sql

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,7 @@
- args:
cascade: true
sql: CREATE INDEX jobs_search_idx ON jobs USING gin (est_number gin_trgm_ops,
ro_number gin_trgm_ops, clm_no gin_trgm_ops, ownr_ln gin_trgm_ops, ownr_fn gin_trgm_ops,
ownr_ph1 gin_trgm_ops, ownr_ea gin_trgm_ops, insd_ln gin_trgm_ops, insd_fn gin_trgm_ops,
insd_ea gin_trgm_ops, insd_ph1 gin_trgm_ops);
type: run_sql

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,4 @@
- args:
cascade: true
sql: 'drop INDEX jobs_search_idx '
type: run_sql

View File

@@ -0,0 +1,3 @@
- args:
sql: ALTER TABLE "public"."jobs" DROP COLUMN "search_idx_col";
type: run_sql

View File

@@ -0,0 +1,3 @@
- args:
sql: ALTER TABLE "public"."jobs" ADD COLUMN "search_idx_col" tsvector NULL;
type: run_sql

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,15 @@
- args:
cascade: true
sql: "CREATE FUNCTION search_jobs(search text)\r\nRETURNS SETOF jobs AS $$\r\n
\ SELECT *\r\n FROM jobs\r\n WHERE\r\n ownr_fn ilike ('%' || search
|| '%')\r\n OR ownr_ln ilike ('%' || search || '%')\r\n OR ro_number
ilike ('%' || search || '%')\r\n OR est_number ilike ('%' || search ||
'%')\r\n OR clm_no ilike ('%' || search || '%')\r\n OR ownr_ph1 ilike
('%' || search || '%')\r\n OR ownr_ea ilike ('%' || search || '%')\r\n
\ OR insd_ln ilike ('%' || search || '%')\r\n OR insd_fn ilike ('%'
|| search || '%')\r\n$$ LANGUAGE sql STABLE;\r\n"
type: run_sql
- args:
name: search_jobs
schema: public
type: track_function

Some files were not shown because too many files have changed in this diff Show More