0
.elasticbeanstalk/config.yml
Normal file
0
.elasticbeanstalk/config.yml
Normal file
92
_reference/AuditTriggerFunctions.sql
Normal file
92
_reference/AuditTriggerFunctions.sql
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
|
||||||
|
|
||||||
|
CREATE SCHEMA audit;
|
||||||
|
|
||||||
|
CREATE TABLE audit_trail (
|
||||||
|
|
||||||
|
id serial PRIMARY KEY,
|
||||||
|
|
||||||
|
tstamp timestamp DEFAULT now(),
|
||||||
|
|
||||||
|
schemaname text,
|
||||||
|
|
||||||
|
tabname text,
|
||||||
|
|
||||||
|
operation text,
|
||||||
|
recordid uuid,
|
||||||
|
|
||||||
|
-- who text DEFAULT current_user,
|
||||||
|
|
||||||
|
new_val json,
|
||||||
|
|
||||||
|
old_val json,
|
||||||
|
useremail text,
|
||||||
|
bodyshopid uuid
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
-- More as an example than anything else, I wanted a function that would take two JSONB objects in PostgreSQL, and return how the left-hand side differs from the right-hand side. This means any key that is in the left but not in the right would be returned, along with any key whose value on the left is different from the right.
|
||||||
|
|
||||||
|
-- Here’s a quick example of how to do this in a single SELECT. In real life, you probably want more error checking, but it shows how nice the built-in primitives are:
|
||||||
|
CREATE OR REPLACE FUNCTION json_diff(l JSONB, r JSONB) RETURNS JSONB AS
|
||||||
|
$json_diff$
|
||||||
|
SELECT jsonb_object_agg(a.key, a.value) FROM
|
||||||
|
( SELECT key, value FROM jsonb_each(l) ) a LEFT OUTER JOIN
|
||||||
|
( SELECT key, value FROM jsonb_each(r) ) b ON a.key = b.key
|
||||||
|
WHERE a.value != b.value OR b.key IS NULL;
|
||||||
|
$json_diff$
|
||||||
|
LANGUAGE sql;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION audit_trigger() RETURNS trigger AS $$
|
||||||
|
|
||||||
|
DECLARE
|
||||||
|
shopid text ;
|
||||||
|
email text;
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
|
||||||
|
select b.id, u.email INTO shopid, email from users u join associations a on u.email = a.useremail join bodyshops b on b.id = a.shopid where u.authid = current_setting('hasura.user', 't')::jsonb->>'x-hasura-user-id' and a.active = true;
|
||||||
|
|
||||||
|
IF TG_OP = 'INSERT'
|
||||||
|
|
||||||
|
THEN
|
||||||
|
|
||||||
|
INSERT INTO public.audit_trail (tabname, schemaname, operation, new_val, recordid, bodyshopid, useremail)
|
||||||
|
|
||||||
|
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(NEW), NEW.id, shopid, email);
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
|
||||||
|
ELSIF TG_OP = 'UPDATE'
|
||||||
|
|
||||||
|
THEN
|
||||||
|
|
||||||
|
INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, new_val, recordid, bodyshopid, useremail)
|
||||||
|
|
||||||
|
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP,
|
||||||
|
|
||||||
|
json_diff(to_jsonb(OLD), to_jsonb(NEW)) , json_diff(to_jsonb(NEW), to_jsonb(OLD)), OLD.id, shopid, email);
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
|
||||||
|
ELSIF TG_OP = 'DELETE'
|
||||||
|
|
||||||
|
THEN
|
||||||
|
|
||||||
|
INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, recordid, bodyshopid, useremail)
|
||||||
|
|
||||||
|
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(OLD), OLD.ID, shopid, email);
|
||||||
|
|
||||||
|
RETURN OLD;
|
||||||
|
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
END;
|
||||||
|
|
||||||
|
$$ LANGUAGE 'plpgsql' SECURITY DEFINER;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TRIGGER audit_trigger_users AFTER INSERT OR UPDATE OR DELETE ON users
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE audit_trigger();
|
||||||
File diff suppressed because it is too large
Load Diff
3
client/debug.log
Normal file
3
client/debug.log
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[0309/123120.472:ERROR:process_reader_win.cc(108)] process 40916 not found
|
||||||
|
[0309/123120.472:ERROR:exception_snapshot_win.cc(98)] thread ID 50448 not found in process
|
||||||
|
[0309/123120.472:ERROR:scoped_process_suspend.cc(40)] NtResumeProcess: An attempt was made to access an exiting process. (0xc000010a)
|
||||||
@@ -4,46 +4,51 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "https://localhost:5000",
|
"proxy": "https://localhost:5000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ckeditor/ckeditor5-build-classic": "^16.0.0",
|
"@ckeditor/ckeditor5-build-classic": "^18.0.0",
|
||||||
"@ckeditor/ckeditor5-react": "^2.1.0",
|
"@ckeditor/ckeditor5-react": "^2.1.0",
|
||||||
"antd": "^3.26.8",
|
"@nivo/pie": "^0.61.1",
|
||||||
|
"@tanem/react-nprogress": "^3.0.20",
|
||||||
|
"aamva": "^1.2.0",
|
||||||
|
"antd": "^4.1.0",
|
||||||
"apollo-boost": "^0.4.4",
|
"apollo-boost": "^0.4.4",
|
||||||
"apollo-link-context": "^1.0.19",
|
"apollo-link-context": "^1.0.19",
|
||||||
"apollo-link-error": "^1.1.12",
|
"apollo-link-error": "^1.1.12",
|
||||||
"apollo-link-logger": "^1.2.3",
|
"apollo-link-logger": "^1.2.3",
|
||||||
|
"apollo-link-retry": "^2.2.15",
|
||||||
"apollo-link-ws": "^1.0.19",
|
"apollo-link-ws": "^1.0.19",
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.19.2",
|
||||||
"chart.js": "^2.9.3",
|
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"firebase": "^7.8.1",
|
"firebase": "^7.13.1",
|
||||||
"graphql": "^14.6.0",
|
"graphql": "^14.6.0",
|
||||||
"i18next": "^19.1.0",
|
"i18next": "^19.3.4",
|
||||||
"node-sass": "^4.13.1",
|
"node-sass": "^4.13.1",
|
||||||
"react": "^16.12.0",
|
"react": "^16.13.1",
|
||||||
"react-apollo": "^3.1.3",
|
"react-apollo": "^3.1.3",
|
||||||
"react-barcode": "^1.4.0",
|
"react-barcode": "^1.4.0",
|
||||||
"react-big-calendar": "^0.23.0",
|
"react-big-calendar": "^0.24.1",
|
||||||
"react-chartjs-2": "^2.9.0",
|
"react-dom": "^16.13.1",
|
||||||
"react-dom": "^16.12.0",
|
"react-grid-gallery": "^0.5.5",
|
||||||
|
"react-grid-layout": "^0.18.3",
|
||||||
"react-html-email": "^3.0.0",
|
"react-html-email": "^3.0.0",
|
||||||
"react-i18next": "^11.3.1",
|
"react-i18next": "^11.3.4",
|
||||||
"react-icons": "^3.9.0",
|
"react-icons": "^3.9.0",
|
||||||
"react-image-file-resizer": "^0.2.1",
|
"react-image-file-resizer": "^0.2.1",
|
||||||
"react-moment": "^0.9.7",
|
"react-moment": "^0.9.7",
|
||||||
"react-number-format": "^4.3.1",
|
"react-number-format": "^4.4.1",
|
||||||
"react-redux": "^7.1.3",
|
"react-pdf": "^4.1.0",
|
||||||
|
"react-redux": "^7.2.0",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-scripts": "3.3.1",
|
"react-scripts": "3.4.1",
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"redux-saga": "^1.1.3",
|
"redux-saga": "^1.1.3",
|
||||||
"reselect": "^4.0.0",
|
"reselect": "^4.0.0",
|
||||||
"styled-components": "^5.0.1",
|
"styled-components": "^5.0.1",
|
||||||
"subscriptions-transport-ws": "^0.9.16",
|
"subscriptions-transport-ws": "^0.9.16"
|
||||||
"twilio": "^3.39.5"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
@@ -67,6 +72,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@apollo/react-testing": "^3.1.3",
|
"@apollo/react-testing": "^3.1.3",
|
||||||
"enzyme": "^3.11.0",
|
"enzyme": "^3.11.0",
|
||||||
"enzyme-adapter-react-16": "^1.15.2"
|
"enzyme-adapter-react-16": "^1.15.2",
|
||||||
|
"source-map-explorer": "^2.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ApolloProvider } from "@apollo/react-common";
|
||||||
import { ApolloLink } from "apollo-boost";
|
import { ApolloLink } from "apollo-boost";
|
||||||
import { InMemoryCache } from "apollo-cache-inmemory";
|
import { InMemoryCache } from "apollo-cache-inmemory";
|
||||||
import ApolloClient from "apollo-client";
|
import ApolloClient from "apollo-client";
|
||||||
@@ -5,21 +6,17 @@ import { split } from "apollo-link";
|
|||||||
import { setContext } from "apollo-link-context";
|
import { setContext } from "apollo-link-context";
|
||||||
import { HttpLink } from "apollo-link-http";
|
import { HttpLink } from "apollo-link-http";
|
||||||
import apolloLogger from "apollo-link-logger";
|
import apolloLogger from "apollo-link-logger";
|
||||||
|
import { RetryLink } from "apollo-link-retry";
|
||||||
import { WebSocketLink } from "apollo-link-ws";
|
import { WebSocketLink } from "apollo-link-ws";
|
||||||
import { getMainDefinition } from "apollo-utilities";
|
import { getMainDefinition } from "apollo-utilities";
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import { ApolloProvider } from "react-apollo";
|
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
||||||
import SpinnerComponent from "../components/loading-spinner/loading-spinner.component";
|
import { auth } from "../firebase/firebase.utils";
|
||||||
//import { shouldRefreshToken, refreshToken } from "../graphql/middleware";
|
|
||||||
import errorLink from "../graphql/apollo-error-handling";
|
import errorLink from "../graphql/apollo-error-handling";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
export default class AppContainer extends Component {
|
||||||
class AppContainer extends Component {
|
constructor() {
|
||||||
state = {
|
super();
|
||||||
client: null,
|
|
||||||
loaded: false
|
|
||||||
};
|
|
||||||
async componentDidMount() {
|
|
||||||
const httpLink = new HttpLink({
|
const httpLink = new HttpLink({
|
||||||
uri: process.env.REACT_APP_GRAPHQL_ENDPOINT
|
uri: process.env.REACT_APP_GRAPHQL_ENDPOINT
|
||||||
});
|
});
|
||||||
@@ -29,8 +26,9 @@ class AppContainer extends Component {
|
|||||||
options: {
|
options: {
|
||||||
lazy: true,
|
lazy: true,
|
||||||
reconnect: true,
|
reconnect: true,
|
||||||
connectionParams: () => {
|
connectionParams: async () => {
|
||||||
const token = localStorage.getItem("token");
|
//const token = localStorage.getItem("token");
|
||||||
|
const token = await auth.currentUser.getIdToken(true);
|
||||||
if (token) {
|
if (token) {
|
||||||
return {
|
return {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -41,6 +39,13 @@ class AppContainer extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const subscriptionMiddleware = {
|
||||||
|
applyMiddleware: async (options, next) => {
|
||||||
|
options.authToken = await auth.currentUser.getIdToken(true);
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
wsLink.subscriptionClient.use([subscriptionMiddleware]);
|
||||||
|
|
||||||
const link = split(
|
const link = split(
|
||||||
// split based on operation type
|
// split based on operation type
|
||||||
@@ -64,16 +69,29 @@ class AppContainer extends Component {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const authLink = setContext((_, { headers }) => {
|
const authLink = setContext((_, { headers }) => {
|
||||||
const token = localStorage.getItem("token");
|
return auth.currentUser.getIdToken().then(token => {
|
||||||
if (token) {
|
if (token) {
|
||||||
return {
|
return {
|
||||||
headers: {
|
headers: {
|
||||||
...headers,
|
...headers,
|
||||||
authorization: token ? `Bearer ${token}` : ""
|
authorization: token ? `Bearer ${token}` : ""
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return { headers };
|
return { headers };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const retryLink = new RetryLink({
|
||||||
|
delay: {
|
||||||
|
initial: 300,
|
||||||
|
max: 5,
|
||||||
|
jitter: true
|
||||||
|
},
|
||||||
|
attempts: {
|
||||||
|
max: 5,
|
||||||
|
retryIf: (error, _operation) => !!error
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -81,37 +99,27 @@ class AppContainer extends Component {
|
|||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
middlewares.push(apolloLogger);
|
middlewares.push(apolloLogger);
|
||||||
}
|
}
|
||||||
middlewares.push(errorLink.concat(authLink.concat(link)));
|
|
||||||
|
middlewares.push(retryLink.concat(errorLink.concat(authLink.concat(link))));
|
||||||
|
|
||||||
const cache = new InMemoryCache();
|
const cache = new InMemoryCache();
|
||||||
|
|
||||||
const client = new ApolloClient({
|
const client = new ApolloClient({
|
||||||
link: ApolloLink.from(middlewares),
|
link: ApolloLink.from(middlewares),
|
||||||
cache,
|
cache,
|
||||||
connectToDevTools: true
|
connectToDevTools: true
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setState({
|
this.state = { client };
|
||||||
client,
|
|
||||||
loaded: true
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { client, loaded } = this.state;
|
const { client } = this.state;
|
||||||
|
|
||||||
if (!loaded) {
|
|
||||||
return <SpinnerComponent />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ApolloProvider client={client}>
|
<ApolloProvider client={client}>
|
||||||
|
<GlobalLoadingBar />
|
||||||
<App />
|
<App />
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppContainer;
|
|
||||||
|
|||||||
@@ -1,27 +1 @@
|
|||||||
@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;
|
|
||||||
} */
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
checkUserSession: () => dispatch(checkUserSession())
|
checkUserSession: () => dispatch(checkUserSession())
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
@@ -34,9 +35,10 @@ export default connect(
|
|||||||
checkUserSession();
|
checkUserSession();
|
||||||
return () => {};
|
return () => {};
|
||||||
}, [checkUserSession]);
|
}, [checkUserSession]);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
if (currentUser && currentUser.language)
|
if (currentUser && currentUser.language)
|
||||||
i18next.changeLanguage(currentUser.language, (err, t) => {
|
i18next.changeLanguage(currentUser.language, err => {
|
||||||
if (err)
|
if (err)
|
||||||
return console.log("Error encountered when changing languages.", err);
|
return console.log("Error encountered when changing languages.", err);
|
||||||
});
|
});
|
||||||
@@ -49,14 +51,13 @@ export default connect(
|
|||||||
<div>
|
<div>
|
||||||
<Switch>
|
<Switch>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Suspense fallback={<LoadingSpinner />}>
|
<Suspense fallback={<LoadingSpinner message='App.Js Suspense' />}>
|
||||||
<Route exact path='/' component={LandingPage} />
|
<Route exact path='/' component={LandingPage} />
|
||||||
<Route exact path='/unauthorized' component={Unauthorized} />
|
<Route exact path='/unauthorized' component={Unauthorized} />
|
||||||
|
|
||||||
<Route exact path='/signin' component={SignInPage} />
|
<Route exact path='/signin' component={SignInPage} />
|
||||||
|
|
||||||
<PrivateRoute
|
<PrivateRoute
|
||||||
//isAuthorized={HookCurrentUser.data.currentUser ? true : false}
|
|
||||||
isAuthorized={currentUser.authorized}
|
isAuthorized={currentUser.authorized}
|
||||||
path='/manage'
|
path='/manage'
|
||||||
component={ManagePage}
|
component={ManagePage}
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
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", () => {
|
|
||||||
ReactDOM.render(
|
|
||||||
<MockedProvider>
|
|
||||||
<App />
|
|
||||||
</MockedProvider>,
|
|
||||||
div
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("unmounts without crashing", () => {
|
|
||||||
ReactDOM.unmountComponentAtNode(div);
|
|
||||||
});
|
|
||||||
@@ -1,37 +1,51 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import {
|
||||||
|
startLoading,
|
||||||
|
endLoading
|
||||||
|
} from "../../redux/application/application.actions";
|
||||||
import { setEmailOptions } from "../../redux/email/email.actions";
|
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||||
import T from "../../emails/parts-order/parts-order.email";
|
import T, {
|
||||||
import { REPORT_QUERY_PARTS_ORDER_BY_PK } from "../../emails/parts-order/parts-order.query";
|
Subject
|
||||||
|
} from "../../emails/templates/appointment-confirmation/appointment-confirmation.template";
|
||||||
|
import { EMAIL_APPOINTMENT_CONFIRMATION } from "../../emails/templates/appointment-confirmation/appointment-confirmation.query";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
setEmailOptions: e => dispatch(setEmailOptions(e))
|
setEmailOptions: e => dispatch(setEmailOptions(e)),
|
||||||
|
load: () => dispatch(startLoading()),
|
||||||
|
endload: () => dispatch(endLoading())
|
||||||
});
|
});
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(function Test({ setEmailOptions }) {
|
)(function Test({ setEmailOptions, load, endload }) {
|
||||||
return (
|
return (
|
||||||
<button
|
<div>
|
||||||
onClick={() =>
|
<button
|
||||||
setEmailOptions({
|
onClick={() =>
|
||||||
messageOptions: {
|
setEmailOptions({
|
||||||
from: { name: "Kavia Autobdoy", address: "noreply@bodyshop.app" },
|
messageOptions: {
|
||||||
to: "patrickwf@gmail.com",
|
from: { name: "Kavia Autobody", address: "noreply@bodyshop.app" },
|
||||||
replyTo: "snaptsoft@gmail.com"
|
to: "patrickwf@gmail.com",
|
||||||
},
|
replyTo: "snaptsoft@gmail.com",
|
||||||
template: T,
|
subject: Subject
|
||||||
queryConfig: [
|
},
|
||||||
REPORT_QUERY_PARTS_ORDER_BY_PK,
|
template: T,
|
||||||
{ variables: { id: "46f3aa34-c3bd-46c8-83fc-c93b7ce84f46" } }
|
queryConfig: [
|
||||||
]
|
EMAIL_APPOINTMENT_CONFIRMATION,
|
||||||
})
|
{ variables: { id: "91bb31dd-ea87-4cfc-bbe2-2ec754dcb861" } }
|
||||||
}>
|
]
|
||||||
Set email config.
|
})
|
||||||
</button>
|
}
|
||||||
|
>
|
||||||
|
Set email config.
|
||||||
|
</button>
|
||||||
|
<button onClick={() => load()}>Load</button>
|
||||||
|
<button onClick={() => endload()}>Stop</button>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Alert component should render Alert component 1`] = `ShallowWrapper {}`;
|
||||||
@@ -6,8 +6,18 @@ import { shallow, mount } from "enzyme";
|
|||||||
|
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
|
|
||||||
it("renders without crashing", () => {
|
describe("Alert component", () => {
|
||||||
const wrapper = mount(<Alert type="error" message="Test Error" />);
|
let wrapper;
|
||||||
console.log("wrapper", wrapper);
|
beforeEach(() => {
|
||||||
// expect(wrapper.children()).to.have.lengthOf(1);
|
const mockProps = {
|
||||||
|
type: "error",
|
||||||
|
message: "Test error message."
|
||||||
|
};
|
||||||
|
|
||||||
|
wrapper = shallow(<Alert {...mockProps} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render Alert component", () => {
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`AllocationsAssignmentComponent component should create an allocation on save 1`] = `ReactWrapper {}`;
|
||||||
|
|
||||||
|
exports[`AllocationsAssignmentComponent component should render AllocationsAssignmentComponent component 1`] = `ReactWrapper {}`;
|
||||||
@@ -9,10 +9,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export function AllocationsAssignmentComponent({
|
||||||
mapStateToProps,
|
|
||||||
null
|
|
||||||
)(function AllocationsAssignmentComponent({
|
|
||||||
bodyshop,
|
bodyshop,
|
||||||
handleAssignment,
|
handleAssignment,
|
||||||
assignment,
|
assignment,
|
||||||
@@ -23,7 +20,6 @@ export default connect(
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
console.log("e", e);
|
|
||||||
setAssignment({ ...assignment, employeeid: e });
|
setAssignment({ ...assignment, employeeid: e });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,16 +27,15 @@ export default connect(
|
|||||||
|
|
||||||
const popContent = (
|
const popContent = (
|
||||||
<div>
|
<div>
|
||||||
<Select
|
<Select id="employeeSelector"
|
||||||
showSearch
|
showSearch
|
||||||
style={{ width: 200 }}
|
style={{ width: 200 }}
|
||||||
placeholder="Select a person"
|
placeholder='Select a person'
|
||||||
optionFilterProp="children"
|
optionFilterProp='children'
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
filterOption={(input, option) =>
|
filterOption={(input, option) =>
|
||||||
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||||
}
|
}>
|
||||||
>
|
|
||||||
{bodyshop.employees.map(emp => (
|
{bodyshop.employees.map(emp => (
|
||||||
<Select.Option value={emp.id} key={emp.id}>
|
<Select.Option value={emp.id} key={emp.id}>
|
||||||
{`${emp.first_name} ${emp.last_name}`}
|
{`${emp.first_name} ${emp.last_name}`}
|
||||||
@@ -56,10 +51,9 @@ export default connect(
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type='primary'
|
||||||
disabled={!assignment.employeeid}
|
disabled={!assignment.employeeid}
|
||||||
onClick={handleAssignment}
|
onClick={handleAssignment}>
|
||||||
>
|
|
||||||
Assign
|
Assign
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => setVisibility(false)}>Close</Button>
|
<Button onClick={() => setVisibility(false)}>Close</Button>
|
||||||
@@ -73,4 +67,6 @@ export default connect(
|
|||||||
</Button>
|
</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, null)(AllocationsAssignmentComponent);
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { mount, shallow } from "enzyme";
|
||||||
|
import React from "react";
|
||||||
|
import { AllocationsAssignmentComponent } from "./allocations-assignment.component";
|
||||||
|
import { MockBodyshop } from "../../utils/TestingHelpers";
|
||||||
|
import { Select } from "antd";
|
||||||
|
const div = document.createElement("div");
|
||||||
|
|
||||||
|
describe("AllocationsAssignmentComponent component", () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const mockProps = {
|
||||||
|
bodyshop: MockBodyshop,
|
||||||
|
handleAssignment: jest.fn(),
|
||||||
|
assignment: {},
|
||||||
|
setAssignment: jest.fn(),
|
||||||
|
visibilityState: [false, jest.fn()],
|
||||||
|
maxHours: 4
|
||||||
|
};
|
||||||
|
|
||||||
|
wrapper = mount(<AllocationsAssignmentComponent {...mockProps} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render AllocationsAssignmentComponent component", () => {
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render a list of employees", () => {
|
||||||
|
const empList = wrapper.find("#employeeSelector");
|
||||||
|
console.log(empList.debug());
|
||||||
|
expect(empList.children()).to.have.lengthOf(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create an allocation on save", () => {
|
||||||
|
wrapper.find("Button").simulate("click");
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import AllocationsAssignmentComponent from "./allocations-assignment.component";
|
import AllocationsAssignmentComponent from "./allocations-assignment.component";
|
||||||
import { useMutation } from "react-apollo";
|
import { useMutation } from "@apollo/react-hooks";
|
||||||
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
|
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { notification } from "antd";
|
import { notification } from "antd";
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { shallow } from "enzyme";
|
|
||||||
import AllocationsAssignmentContainer from "./allocations-assignment.container";
|
|
||||||
|
|
||||||
describe("LineAllocationsContainer", () => {
|
|
||||||
let mockRefetch;
|
|
||||||
let jobLineId;
|
|
||||||
let wrapper;
|
|
||||||
beforeEach(() => {
|
|
||||||
mockRefetch = jest.fn;
|
|
||||||
jobLineId = "b76e44a8-943f-4c67-b8f4-38d14db8b4b8";
|
|
||||||
const mockProps = {
|
|
||||||
refetch: mockRefetch,
|
|
||||||
jobLineId,
|
|
||||||
hours: 5
|
|
||||||
};
|
|
||||||
|
|
||||||
shallow(<AllocationsAssignmentContainer {...mockProps} />);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import AllocationsBulkAssignment from "./allocations-bulk-assignment.component";
|
import AllocationsBulkAssignment from "./allocations-bulk-assignment.component";
|
||||||
import { useMutation } from "react-apollo";
|
import { useMutation } from "@apollo/react-hooks";
|
||||||
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
|
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { notification } from "antd";
|
import { notification } from "antd";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Icon } from "antd";
|
import Icon from "@ant-design/icons";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { MdRemoveCircleOutline } from "react-icons/md";
|
import { MdRemoveCircleOutline } from "react-icons/md";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useMutation } from "react-apollo";
|
import { useMutation } from "@apollo/react-hooks";
|
||||||
import { DELETE_ALLOCATION } from "../../graphql/allocations.queries";
|
import { DELETE_ALLOCATION } from "../../graphql/allocations.queries";
|
||||||
import AllocationsLabelComponent from "./allocations-employee-label.component";
|
import AllocationsLabelComponent from "./allocations-employee-label.component";
|
||||||
import { notification } from "antd";
|
import { notification } from "antd";
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Table } from "antd";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component";
|
||||||
|
|
||||||
|
export default function AuditTrailListComponent({ loading, data }) {
|
||||||
|
const [state, setState] = useState({
|
||||||
|
sortedInfo: {},
|
||||||
|
filteredInfo: {}
|
||||||
|
});
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("audit.fields.created"),
|
||||||
|
dataIndex: " created",
|
||||||
|
key: " created",
|
||||||
|
width: "10%",
|
||||||
|
render: (text, record) => (
|
||||||
|
<DateTimeFormatter>{record.created}</DateTimeFormatter>
|
||||||
|
),
|
||||||
|
sorter: (a, b) => a.created - b.created,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "created" && state.sortedInfo.order
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("audit.fields.operation"),
|
||||||
|
dataIndex: "operation",
|
||||||
|
key: "operation",
|
||||||
|
width: "10%",
|
||||||
|
sorter: (a, b) => alphaSort(a.operation, b.operation),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "operation" && state.sortedInfo.order
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("audit.fields.values"),
|
||||||
|
dataIndex: " old_val",
|
||||||
|
key: " old_val",
|
||||||
|
width: "10%",
|
||||||
|
render: (text, record) => (
|
||||||
|
<AuditTrailValuesComponent
|
||||||
|
oldV={record.old_val}
|
||||||
|
newV={record.new_val}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("audit.fields.useremail"),
|
||||||
|
dataIndex: "useremail",
|
||||||
|
key: "useremail",
|
||||||
|
width: "10%",
|
||||||
|
sorter: (a, b) => alphaSort(a.useremail, b.useremail),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const formItemLayout = {
|
||||||
|
labelCol: {
|
||||||
|
xs: { span: 12 },
|
||||||
|
sm: { span: 5 }
|
||||||
|
},
|
||||||
|
wrapperCol: {
|
||||||
|
xs: { span: 24 },
|
||||||
|
sm: { span: 12 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
{...formItemLayout}
|
||||||
|
loading={loading}
|
||||||
|
size="small"
|
||||||
|
pagination={{ position: "top", defaultPageSize: 25 }}
|
||||||
|
columns={columns.map(item => ({ ...item }))}
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={data}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import React from "react";
|
||||||
|
import AuditTrailListComponent from "./audit-trail-list.component";
|
||||||
|
import { useQuery } from "@apollo/react-hooks";
|
||||||
|
import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
|
||||||
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
|
||||||
|
export default function AuditTrailListContainer({ recordId }) {
|
||||||
|
const { loading, error, data } = useQuery(QUERY_AUDIT_TRAIL, {
|
||||||
|
variables: { id: recordId },
|
||||||
|
fetchPolicy: "network-only"
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{error ? (
|
||||||
|
<AlertComponent type="error" message={error.message} />
|
||||||
|
) : (
|
||||||
|
<AuditTrailListComponent
|
||||||
|
loading={loading}
|
||||||
|
data={data ? data.audit_trail : null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { List } from "antd";
|
||||||
|
import Icon from "@ant-design/icons";
|
||||||
|
import { FaArrowRight } from "react-icons/fa";
|
||||||
|
export default function AuditTrailValuesComponent({ oldV, newV }) {
|
||||||
|
console.log("(!oldV & !newV)", !oldV && !newV);
|
||||||
|
console.log("(!oldV & newV)", !oldV && newV);
|
||||||
|
if (!oldV && !newV) return <div></div>;
|
||||||
|
|
||||||
|
if (!oldV && newV)
|
||||||
|
return (
|
||||||
|
<List style={{ width: "800px" }} bordered size="small">
|
||||||
|
{Object.keys(newV).map((key, idx) => (
|
||||||
|
<List.Item key={idx} value={key}>
|
||||||
|
{key}: {JSON.stringify(newV[key])}
|
||||||
|
</List.Item>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List style={{ width: "800px" }} bordered size="small">
|
||||||
|
{Object.keys(oldV).map((key, idx) => (
|
||||||
|
<List.Item key={idx}>
|
||||||
|
{key}: {oldV[key]} <Icon component={FaArrowRight} />
|
||||||
|
{JSON.stringify(newV[key])}
|
||||||
|
</List.Item>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { ShrinkOutlined } from "@ant-design/icons";
|
||||||
|
import { Badge } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import {
|
||||||
|
openConversation,
|
||||||
|
toggleChatVisible
|
||||||
|
} from "../../redux/messaging/messaging.actions";
|
||||||
|
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
toggleChatVisible: () => dispatch(toggleChatVisible()),
|
||||||
|
openConversation: number => dispatch(openConversation(number))
|
||||||
|
});
|
||||||
|
|
||||||
|
export function ChatConversationListComponent({
|
||||||
|
toggleChatVisible,
|
||||||
|
conversationList,
|
||||||
|
openConversation
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className='chat-overlay-open'>
|
||||||
|
<ShrinkOutlined onClick={() => toggleChatVisible()} />
|
||||||
|
{conversationList.map(item => (
|
||||||
|
<Badge count={item.messages_aggregate.aggregate.count || 0}>
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
style={{ cursor: "pointer", display: "block" }}
|
||||||
|
onClick={() =>
|
||||||
|
openConversation({ phone_num: item.phone_num, id: item.id })
|
||||||
|
}>
|
||||||
|
<div>
|
||||||
|
<PhoneNumberFormatter>{item.phone_num}</PhoneNumberFormatter>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default connect(null, mapDispatchToProps)(ChatConversationListComponent);
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { CloseCircleFilled } from "@ant-design/icons";
|
||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import {
|
||||||
|
closeConversation,
|
||||||
|
sendMessage,
|
||||||
|
toggleConversationVisible
|
||||||
|
} from "../../redux/messaging/messaging.actions";
|
||||||
|
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
toggleConversationVisible: conversationId =>
|
||||||
|
dispatch(toggleConversationVisible(conversationId)),
|
||||||
|
closeConversation: phone => dispatch(closeConversation(phone)),
|
||||||
|
sendMessage: message => dispatch(sendMessage(message))
|
||||||
|
});
|
||||||
|
|
||||||
|
function ChatConversationClosedComponent({
|
||||||
|
conversation,
|
||||||
|
toggleConversationVisible,
|
||||||
|
closeConversation
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='chat-conversation-closed'
|
||||||
|
onClick={() => toggleConversationVisible(conversation.id)}>
|
||||||
|
<PhoneFormatter>{conversation.phone_num}</PhoneFormatter>
|
||||||
|
<CloseCircleFilled
|
||||||
|
onClick={() => closeConversation(conversation.phone_num)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
null,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(ChatConversationClosedComponent);
|
||||||
@@ -1,120 +1,29 @@
|
|||||||
import { Button, Card, Input, Icon } from "antd";
|
import { Badge, Card } from "antd";
|
||||||
import React, { useEffect, useState } from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import ChatConversationClosedComponent from "./chat-conversation.closed.component";
|
||||||
import { createStructuredSelector } from "reselect";
|
import ChatConversationOpenComponent from "./chat-conversation.open.component";
|
||||||
import twilio from "twilio";
|
|
||||||
import {
|
|
||||||
closeConversation,
|
|
||||||
toggleConversationVisible
|
|
||||||
} from "../../redux/messaging/messaging.actions";
|
|
||||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
|
||||||
import "./chat-conversation.styles.scss"; //https://bootsnipp.com/snippets/exR5v
|
|
||||||
import { MdSend } from "react-icons/md";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
const client = twilio(
|
export default function ChatConversationComponent({
|
||||||
"ACf1b1aaf0e04740828b49b6e58467d787",
|
|
||||||
"0bea5e29a6d77593183ab1caa01d23de"
|
|
||||||
);
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
|
||||||
//currentUser: selectCurrentUser
|
|
||||||
});
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
toggleConversationVisible: conversationId =>
|
|
||||||
dispatch(toggleConversationVisible(conversationId)),
|
|
||||||
closeConversation: phone => dispatch(closeConversation(phone))
|
|
||||||
});
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(function ChatConversationComponent({
|
|
||||||
conversation,
|
conversation,
|
||||||
toggleConversationVisible,
|
messages,
|
||||||
closeConversation
|
subState,
|
||||||
|
unreadCount
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const [messages, setMessages] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
client.messages.list({ limit: 20 }, (error, items) => {
|
|
||||||
setMessages(
|
|
||||||
items.reduce((acc, value) => {
|
|
||||||
acc.push({
|
|
||||||
sid: value.sid,
|
|
||||||
direction: value.direction,
|
|
||||||
body: value.body
|
|
||||||
});
|
|
||||||
return acc;
|
|
||||||
}, [])
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return () => {};
|
|
||||||
}, [setMessages]);
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className='chat-conversation'>
|
||||||
<Card
|
<Badge count={unreadCount}>
|
||||||
title={
|
<Card size='small'>
|
||||||
conversation.open ? (
|
{conversation.open ? (
|
||||||
<div style={{ display: "flex" }}>
|
<ChatConversationOpenComponent
|
||||||
<div
|
messages={messages}
|
||||||
onClick={() => toggleConversationVisible(conversation.phone)}
|
conversation={conversation}
|
||||||
>
|
subState={subState}
|
||||||
<PhoneFormatter>{conversation.phone}</PhoneFormatter>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="danger"
|
|
||||||
shape="circle-outline"
|
|
||||||
onClick={() => closeConversation(conversation.phone)}
|
|
||||||
>
|
|
||||||
X
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
style={{
|
|
||||||
width: conversation.open ? "400px" : "175px",
|
|
||||||
margin: "0px 10px"
|
|
||||||
}}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{conversation.open ? (
|
|
||||||
<div>
|
|
||||||
<div className="messages" style={{ height: "400px" }}>
|
|
||||||
<ul>
|
|
||||||
{messages.map(item => (
|
|
||||||
<li
|
|
||||||
key={item.sid}
|
|
||||||
className={`${
|
|
||||||
item.direction === "inbound" ? "sent" : "replies"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<p> {item.body}</p>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<Input.Search
|
|
||||||
placeholder={t("messaging.labels.typeamessage")}
|
|
||||||
enterButton={<Icon component={MdSend} />}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
) : (
|
||||||
) : (
|
<ChatConversationClosedComponent conversation={conversation} />
|
||||||
<div style={{ display: "flex" }}>
|
)}
|
||||||
<div onClick={() => toggleConversationVisible(conversation.phone)}>
|
</Card>
|
||||||
<PhoneFormatter>{conversation.phone}</PhoneFormatter>
|
</Badge>
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
shape="circle-outline"
|
|
||||||
onClick={() => closeConversation(conversation.phone)}
|
|
||||||
>
|
|
||||||
X
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|||||||
@@ -1,17 +1,34 @@
|
|||||||
|
import { useSubscription } from "@apollo/react-hooks";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { CONVERSATION_SUBSCRIPTION_BY_PK } from "../../graphql/conversations.queries";
|
||||||
import ChatConversationComponent from "./chat-conversation.component";
|
import ChatConversationComponent from "./chat-conversation.component";
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
export default function ChatConversationContainer({ conversation }) {
|
||||||
import { createStructuredSelector } from "reselect";
|
const { loading, error, data } = useSubscription(
|
||||||
const mapStateToProps = createStructuredSelector({
|
CONVERSATION_SUBSCRIPTION_BY_PK,
|
||||||
//currentUser: selectCurrentUser
|
{
|
||||||
});
|
variables: { conversationId: conversation.id }
|
||||||
const mapDispatchToProps = dispatch => ({
|
}
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
);
|
||||||
});
|
|
||||||
export default connect(
|
return (
|
||||||
mapStateToProps,
|
<ChatConversationComponent
|
||||||
mapDispatchToProps
|
subState={[loading, error]}
|
||||||
)(function ChatConversationContainer({ conversation }) {
|
conversation={conversation}
|
||||||
return <ChatConversationComponent conversation={conversation} />;
|
unreadCount={
|
||||||
});
|
(data &&
|
||||||
|
data.conversations_by_pk &&
|
||||||
|
data.conversations_by_pk.messages_aggregate &&
|
||||||
|
data.conversations_by_pk.messages_aggregate.aggregate &&
|
||||||
|
data.conversations_by_pk.messages_aggregate.aggregate.count) ||
|
||||||
|
0
|
||||||
|
}
|
||||||
|
messages={
|
||||||
|
(data &&
|
||||||
|
data.conversations_by_pk &&
|
||||||
|
data.conversations_by_pk.messages) ||
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { toggleConversationVisible } from "../../redux/messaging/messaging.actions";
|
||||||
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
import ChatMessageListComponent from "../chat-messages-list/chat-message-list.component";
|
||||||
|
import ChatSendMessage from "../chat-send-message/chat-send-message.component";
|
||||||
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
|
import { ShrinkOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
toggleConversationVisible: conversation =>
|
||||||
|
dispatch(toggleConversationVisible(conversation))
|
||||||
|
});
|
||||||
|
|
||||||
|
export function ChatConversationOpenComponent({
|
||||||
|
conversation,
|
||||||
|
messages,
|
||||||
|
subState,
|
||||||
|
toggleConversationVisible
|
||||||
|
}) {
|
||||||
|
const [loading, error] = subState;
|
||||||
|
|
||||||
|
if (loading) return <LoadingSpinner />;
|
||||||
|
if (error) return <AlertComponent message={error.message} type='error' />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='chat-conversation-open'>
|
||||||
|
<ShrinkOutlined
|
||||||
|
onClick={() => toggleConversationVisible(conversation.id)}
|
||||||
|
/>
|
||||||
|
<ChatMessageListComponent messages={messages} />
|
||||||
|
<ChatSendMessage conversation={conversation} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default connect(null, mapDispatchToProps)(ChatConversationOpenComponent);
|
||||||
32
client/src/components/chat-dock/chat-dock.container.jsx
Normal file
32
client/src/components/chat-dock/chat-dock.container.jsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Affix } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectConversations } from "../../redux/messaging/messaging.selectors";
|
||||||
|
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
|
||||||
|
import ChatMessagesButtonContainer from "../chat-messages-button/chat-messages-button.container";
|
||||||
|
import "./chat-dock.styles.scss";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
activeConversations: selectConversations
|
||||||
|
});
|
||||||
|
|
||||||
|
export function ChatOverlayContainer({ activeConversations }) {
|
||||||
|
return (
|
||||||
|
<Affix offsetBottom={0}>
|
||||||
|
<div className='chat-dock'>
|
||||||
|
<ChatMessagesButtonContainer />
|
||||||
|
{activeConversations
|
||||||
|
? activeConversations.map(conversation => (
|
||||||
|
<ChatConversationContainer
|
||||||
|
conversation={conversation}
|
||||||
|
key={conversation.id}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
</Affix>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, null)(ChatOverlayContainer);
|
||||||
@@ -1,6 +1,67 @@
|
|||||||
|
.chat-dock {
|
||||||
|
z-index: 5;
|
||||||
|
//overflow-x: scroll;
|
||||||
|
// overflow-y: hidden;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-conversation {
|
||||||
|
margin: 2em 1em 0em 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-conversation-open {
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
// .chat-messages {
|
||||||
|
// height: 80%;
|
||||||
|
// overflow-x: hidden;
|
||||||
|
// overflow-y: scroll;
|
||||||
|
// flex-grow: 1;
|
||||||
|
|
||||||
|
// ul {
|
||||||
|
// list-style: none;
|
||||||
|
// margin: 0;
|
||||||
|
// padding: 0;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ul li {
|
||||||
|
// display: inline-block;
|
||||||
|
// clear: both;
|
||||||
|
// padding: 3px 10px;
|
||||||
|
// border-radius: 30px;
|
||||||
|
// margin-bottom: 2px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .inbound {
|
||||||
|
// background: #eee;
|
||||||
|
// float: left;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .outbound {
|
||||||
|
// float: right;
|
||||||
|
// background: #0084ff;
|
||||||
|
// color: #fff;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .inbound + .outbound {
|
||||||
|
// border-bottom-right-radius: 5px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .outbound + .outbound {
|
||||||
|
// border-top-right-radius: 5px;
|
||||||
|
// border-bottom-right-radius: 5px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .outbound:last-of-type {
|
||||||
|
// border-bottom-right-radius: 30px;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
.messages {
|
.messages {
|
||||||
height: auto;
|
height: auto;
|
||||||
min-height: calc(100% - 93px);
|
min-height: calc(100% - 10px);
|
||||||
max-height: calc(100% - 93px);
|
max-height: calc(100% - 93px);
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
@@ -21,7 +82,7 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
clear: both;
|
clear: both;
|
||||||
//float: left;
|
//float: left;
|
||||||
margin: 5px 15px 5px 15px;
|
margin: 5px;
|
||||||
width: calc(100% - 25px);
|
width: calc(100% - 25px);
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { MessageFilled } from "@ant-design/icons";
|
||||||
|
import { Badge, Card } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
|
||||||
|
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
|
||||||
|
import ChatConversationListComponent from "../chat-conversation-list/chat-conversation-list.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
chatVisible: selectChatVisible
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
toggleChatVisible: () => dispatch(toggleChatVisible())
|
||||||
|
});
|
||||||
|
|
||||||
|
export function ChatWindowComponent({
|
||||||
|
chatVisible,
|
||||||
|
toggleChatVisible,
|
||||||
|
conversationList,
|
||||||
|
unreadCount
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div className='chat-conversation'>
|
||||||
|
<Badge count={unreadCount}>
|
||||||
|
<Card size='small'>
|
||||||
|
{chatVisible ? (
|
||||||
|
<ChatConversationListComponent
|
||||||
|
conversationList={conversationList}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div onClick={() => toggleChatVisible()}>
|
||||||
|
<MessageFilled />
|
||||||
|
<strong>{t("messaging.labels.messaging")}</strong>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(ChatWindowComponent);
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { useSubscription } from "@apollo/react-hooks";
|
||||||
|
import React from "react";
|
||||||
|
import { CONVERSATION_LIST_SUBSCRIPTION } from "../../graphql/conversations.queries";
|
||||||
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
|
import ChatMessagesButtonComponent from "./chat-messages-button.component";
|
||||||
|
|
||||||
|
export default function ChatMessagesButtonContainer() {
|
||||||
|
const { loading, error, data } = useSubscription(
|
||||||
|
CONVERSATION_LIST_SUBSCRIPTION
|
||||||
|
);
|
||||||
|
if (loading) return <LoadingSpinner />;
|
||||||
|
if (error) return <AlertComponent message={error.message} type='error' />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChatMessagesButtonComponent
|
||||||
|
conversationList={(data && data.conversations) || []}
|
||||||
|
unreadCount={
|
||||||
|
(data &&
|
||||||
|
data.conversations.reduce((acc, val) => {
|
||||||
|
return (acc = acc + val.messages_aggregate.aggregate.count);
|
||||||
|
}, 0)) ||
|
||||||
|
0
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { CheckCircleOutlined, CheckOutlined } from "@ant-design/icons";
|
||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
export default function ChatMessageListComponent({ messages }) {
|
||||||
|
const messagesEndRef = useRef(null);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
console.log("use");
|
||||||
|
!!messagesEndRef.current &&
|
||||||
|
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(scrollToBottom, [messages]);
|
||||||
|
const StatusRender = status => {
|
||||||
|
switch (status) {
|
||||||
|
case "sent":
|
||||||
|
return <CheckOutlined style={{ margin: "2px", float: "right" }} />;
|
||||||
|
case "delivered":
|
||||||
|
return (
|
||||||
|
<CheckCircleOutlined style={{ margin: "2px", float: "right" }} />
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='messages'>
|
||||||
|
<ul>
|
||||||
|
{messages.map(item => (
|
||||||
|
<li
|
||||||
|
key={item.id}
|
||||||
|
className={`${item.isoutbound ? "replies" : "sent"}`}>
|
||||||
|
<p>
|
||||||
|
{item.text}
|
||||||
|
{StatusRender(item.status)}
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li ref={messagesEndRef} />
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { openConversation } from "../../redux/messaging/messaging.actions";
|
import { openConversation } from "../../redux/messaging/messaging.actions";
|
||||||
import { Icon } from "antd";
|
import { MessageFilled } from "@ant-design/icons";
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
});
|
});
|
||||||
@@ -14,9 +14,8 @@ export default connect(
|
|||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(function ChatOpenButton({ openConversation, phone }) {
|
)(function ChatOpenButton({ openConversation, phone }) {
|
||||||
return (
|
return (
|
||||||
<Icon
|
<MessageFilled
|
||||||
style={{ margin: 4 }}
|
style={{ margin: 4 }}
|
||||||
type="message"
|
|
||||||
onClick={() => openConversation(phone)}
|
onClick={() => openConversation(phone)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import { Badge, Card, Icon } from "antd";
|
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
export default function ChatWindowComponent({
|
|
||||||
chatVisible,
|
|
||||||
toggleChatVisible
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Badge count={5}>
|
|
||||||
<Card
|
|
||||||
onClick={() => toggleChatVisible()}
|
|
||||||
style={{
|
|
||||||
width: chatVisible ? "300px" : "125px",
|
|
||||||
margin: "0px 10px"
|
|
||||||
}}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{chatVisible ? (
|
|
||||||
<div className="messages" style={{ height: "400px" }}>
|
|
||||||
List of chats here.
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<Icon type="message" />
|
|
||||||
<strong style={{ paddingLeft: "10px" }}>
|
|
||||||
{t("messaging.labels.messaging")}
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { Affix, Badge } from "antd";
|
|
||||||
import React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
|
|
||||||
import {
|
|
||||||
selectChatVisible,
|
|
||||||
selectConversations
|
|
||||||
} from "../../redux/messaging/messaging.selectors";
|
|
||||||
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
|
|
||||||
import ChatOverlayComponent from "./chat-overlay.component";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
|
||||||
chatVisible: selectChatVisible,
|
|
||||||
conversations: selectConversations
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
toggleChatVisible: () => dispatch(toggleChatVisible())
|
|
||||||
});
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(function ChatWindowContainer({
|
|
||||||
chatVisible,
|
|
||||||
toggleChatVisible,
|
|
||||||
conversations
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Affix offsetBottom={0}>
|
|
||||||
<div>
|
|
||||||
<Badge count={10}>
|
|
||||||
<ChatOverlayComponent
|
|
||||||
chatVisible={chatVisible}
|
|
||||||
toggleChatVisible={toggleChatVisible}
|
|
||||||
/>
|
|
||||||
</Badge>
|
|
||||||
{conversations
|
|
||||||
? conversations.map((conversation, idx) => (
|
|
||||||
<Badge key={idx} count={5}>
|
|
||||||
<ChatConversationContainer conversation={conversation} />
|
|
||||||
</Badge>
|
|
||||||
))
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
</Affix>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { Input, Spin } from "antd";
|
||||||
|
import { LoadingOutlined } from "@ant-design/icons";
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { sendMessage } from "../../redux/messaging/messaging.actions";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
sendMessage: message => dispatch(sendMessage(message))
|
||||||
|
});
|
||||||
|
|
||||||
|
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage }) {
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (conversation.isSending === false) {
|
||||||
|
setMessage("");
|
||||||
|
}
|
||||||
|
}, [conversation, setMessage]);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleEnter = () => {
|
||||||
|
sendMessage({
|
||||||
|
to: conversation.phone_num,
|
||||||
|
body: message,
|
||||||
|
messagingServiceSid: bodyshop.messagingservicesid,
|
||||||
|
conversationid: conversation.id
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex " }}>
|
||||||
|
<Input.TextArea
|
||||||
|
allowClear
|
||||||
|
autoFocus
|
||||||
|
suffix={<span>a</span>}
|
||||||
|
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||||
|
value={message}
|
||||||
|
disabled={conversation.isSending}
|
||||||
|
placeholder={t("messaging.labels.typeamessage")}
|
||||||
|
onChange={e => setMessage(e.target.value)}
|
||||||
|
onPressEnter={event => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!!!event.shiftKey) handleEnter();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Spin
|
||||||
|
style={{ display: `${conversation.isSending ? "" : "none"}` }}
|
||||||
|
indicator={
|
||||||
|
<LoadingOutlined
|
||||||
|
style={{
|
||||||
|
fontSize: 24
|
||||||
|
}}
|
||||||
|
spin
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(ChatSendMessageComponent);
|
||||||
119
client/src/components/contract-cars/contract-cars.component.jsx
Normal file
119
client/src/components/contract-cars/contract-cars.component.jsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { Input, Table } from "antd";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
|
||||||
|
export default function ContractsCarsComponent({
|
||||||
|
loading,
|
||||||
|
data,
|
||||||
|
selectedCar,
|
||||||
|
handleSelect
|
||||||
|
}) {
|
||||||
|
const [state, setState] = useState({
|
||||||
|
sortedInfo: {},
|
||||||
|
filteredInfo: { text: "" },
|
||||||
|
search: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("courtesycars.fields.fleetnumber"),
|
||||||
|
dataIndex: "fleetnumber",
|
||||||
|
key: "fleetnumber",
|
||||||
|
sorter: (a, b) => alphaSort(a.fleetnumber, b.fleetnumber),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "fleetnumber" && state.sortedInfo.order
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("courtesycars.fields.status"),
|
||||||
|
dataIndex: "status",
|
||||||
|
key: "status",
|
||||||
|
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "status" && state.sortedInfo.order
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("courtesycars.fields.year"),
|
||||||
|
dataIndex: "year",
|
||||||
|
key: "year",
|
||||||
|
sorter: (a, b) => alphaSort(a.year, b.year),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "year" && state.sortedInfo.order
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("courtesycars.fields.make"),
|
||||||
|
dataIndex: "make",
|
||||||
|
key: "make",
|
||||||
|
sorter: (a, b) => alphaSort(a.make, b.make),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "make" && state.sortedInfo.order
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("courtesycars.fields.model"),
|
||||||
|
dataIndex: "model",
|
||||||
|
key: "model",
|
||||||
|
sorter: (a, b) => alphaSort(a.model, b.model),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "model" && state.sortedInfo.order
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("courtesycars.fields.plate"),
|
||||||
|
dataIndex: "plate",
|
||||||
|
key: "plate",
|
||||||
|
sorter: (a, b) => alphaSort(a.plate, b.plate),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "plate" && state.sortedInfo.order
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredData =
|
||||||
|
state.search === ""
|
||||||
|
? data
|
||||||
|
: data.filter(
|
||||||
|
cc =>
|
||||||
|
(cc.fleetnumber || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(state.search.toLowerCase()) ||
|
||||||
|
(cc.status || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(state.search.toLowerCase()) ||
|
||||||
|
(cc.year || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(state.search.toLowerCase()) ||
|
||||||
|
(cc.make || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(state.search.toLowerCase()) ||
|
||||||
|
(cc.model || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(state.search.toLowerCase()) ||
|
||||||
|
(cc.plate || "").toLowerCase().includes(state.search.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
loading={loading}
|
||||||
|
title={() => (
|
||||||
|
<Input.Search
|
||||||
|
placeholder={t("general.labels.search")}
|
||||||
|
value={state.search}
|
||||||
|
onChange={e => setState({ ...state, search: e.target.value })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
size="small"
|
||||||
|
pagination={{ position: "top" }}
|
||||||
|
columns={columns.map(item => ({ ...item }))}
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={filteredData}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
rowSelection={{
|
||||||
|
onSelect: handleSelect,
|
||||||
|
type: "radio",
|
||||||
|
selectedRowKeys: [selectedCar]
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { useQuery } from "@apollo/react-hooks";
|
||||||
|
import React from "react";
|
||||||
|
import { QUERY_AVAILABLE_CC } from "../../graphql/courtesy-car.queries";
|
||||||
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
import ContractCarsComponent from "./contract-cars.component";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default function ContractCarsContainer({ selectedCarState, bodyshop }) {
|
||||||
|
const { loading, error, data } = useQuery(QUERY_AVAILABLE_CC);
|
||||||
|
|
||||||
|
const [selectedCar, setSelectedCar] = selectedCarState;
|
||||||
|
|
||||||
|
const handleSelect = record => {
|
||||||
|
setSelectedCar(record.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ContractCarsComponent
|
||||||
|
handleSelect={handleSelect}
|
||||||
|
selectedCar={selectedCar}
|
||||||
|
loading={loading}
|
||||||
|
data={data ? data.courtesycars : []}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Descriptions, Card } from "antd";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
export default function ContractCourtesyCarBlock({ courtesyCar }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Link to={`/manage/courtesycars/${courtesyCar && courtesyCar.id}`}>
|
||||||
|
<Card title={t("courtesycars.labels.courtesycar")}>
|
||||||
|
<Descriptions size="small" column={1}>
|
||||||
|
<Descriptions.Item label={t("courtesycars.fields.fleetnumber")}>
|
||||||
|
{(courtesyCar && courtesyCar.fleetnumber) || ""}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label={t("courtesycars.fields.plate")}>
|
||||||
|
{(courtesyCar && courtesyCar.plate) || ""}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label={t("courtesycars.labels.vehicle")}>
|
||||||
|
{`${(courtesyCar && courtesyCar.year) || ""} ${(courtesyCar &&
|
||||||
|
courtesyCar.make) ||
|
||||||
|
""} ${(courtesyCar && courtesyCar.model) || ""}`}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
264
client/src/components/contract-form/contract-form.component.jsx
Normal file
264
client/src/components/contract-form/contract-form.component.jsx
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Form, Input, DatePicker, InputNumber, Button } from "antd";
|
||||||
|
import aamva from "aamva";
|
||||||
|
import InputPhone from "../form-items-formatted/phone-form-item.component";
|
||||||
|
import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
|
||||||
|
|
||||||
|
export default function ContractFormComponent() {
|
||||||
|
const [state, setState] = useState("");
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ background: "#f00" }}>
|
||||||
|
TEST AREA
|
||||||
|
<Input value={state} onChange={e => setState(e.target.value)} />
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
console.log("state", state);
|
||||||
|
//let data = state;
|
||||||
|
|
||||||
|
var data =
|
||||||
|
"%FLDELRAY BEACH^DOE$JOHN$^4818 S FEDERAL BLVD^ ? ;6360100462172082009=2101198299090=? #! 33435 I 1600 ECCECC00000?";
|
||||||
|
data = data.replace(/\n/, "");
|
||||||
|
// replace spaces with regular space
|
||||||
|
data = data.replace(/\s/g, " ");
|
||||||
|
var track = data.match(/(.*?\?)(.*?\?)(.*?\?)/);
|
||||||
|
console.log("data", data);
|
||||||
|
console.log("track", track);
|
||||||
|
const a = aamva.stripe(data);
|
||||||
|
console.log(JSON.stringify(a));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Decode
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("contracts.fields.status")}
|
||||||
|
name="status"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<ContractStatusSelector />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("contracts.fields.start")}
|
||||||
|
name="start"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<DatePicker />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("contracts.fields.scheduledreturn")}
|
||||||
|
name="scheduledreturn"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<DatePicker />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("contracts.fields.actualreturn")} name="actualreturn">
|
||||||
|
<DatePicker />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("contracts.fields.kmstart")}
|
||||||
|
name="kmstart"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("contracts.fields.kmend")} name="kmend">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("contracts.fields.driver_dlnumber")}
|
||||||
|
name="driver_dlnumber"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("contracts.fields.driver_dlexpiry")}
|
||||||
|
name="driver_dlexpiry"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<DatePicker />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("contracts.fields.driver_dlst")}
|
||||||
|
name="driver_dlst"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("contracts.fields.driver_fn")}
|
||||||
|
name="driver_fn"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("contracts.fields.driver_ln")}
|
||||||
|
name="driver_ln"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("contracts.fields.driver_addr1")}
|
||||||
|
name="driver_addr1"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("contracts.fields.driver_addr2")} name="driver_addr2">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("contracts.fields.driver_city")}
|
||||||
|
name="driver_city"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("contracts.fields.driver_state")}
|
||||||
|
name="driver_state"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("contracts.fields.driver_zip")}
|
||||||
|
name="driver_zip"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("contracts.fields.driver_ph1")}
|
||||||
|
name="driver_ph1"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputPhone />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("contracts.fields.driver_dob")}
|
||||||
|
name="driver_dob"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<DatePicker />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("contracts.fields.cc_num")}
|
||||||
|
name="cc_num"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("contracts.fields.cc_expiry")}
|
||||||
|
name="cc_expiry"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("contracts.fields.cc_cardholder")}
|
||||||
|
name="cc_cardholder"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Descriptions, Card } from "antd";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function ContractJobBlock({ job }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Link to={`/manage/jobs/${job && job.id}`}>
|
||||||
|
<Card title={t("jobs.labels.job")}>
|
||||||
|
<Descriptions size="small" column={1}>
|
||||||
|
<Descriptions.Item label={t("jobs.fields.ro_number")}>
|
||||||
|
{(job && job.ro_number) || ""}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label={t("jobs.fields.vehicle")}>
|
||||||
|
{`${(job && job.v_model_yr) || ""} ${(job && job.v_make_desc) ||
|
||||||
|
""} ${(job && job.v_model_desc) || ""}`}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label={t("jobs.fields.owner")}>
|
||||||
|
{`${(job && job.ownr_fn) || ""} ${(job && job.ownr_ln) ||
|
||||||
|
""} ${(job && job.ownr_co_nm) || ""}`}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
187
client/src/components/contract-jobs/contract-jobs.component.jsx
Normal file
187
client/src/components/contract-jobs/contract-jobs.component.jsx
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import { Table, Input } from "antd";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
|
||||||
|
export default function ContractsJobsComponent({
|
||||||
|
loading,
|
||||||
|
data,
|
||||||
|
selectedJob,
|
||||||
|
handleSelect
|
||||||
|
}) {
|
||||||
|
const [state, setState] = useState({
|
||||||
|
sortedInfo: {},
|
||||||
|
filteredInfo: { text: "" },
|
||||||
|
search: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.ro_number"),
|
||||||
|
dataIndex: "ro_number",
|
||||||
|
key: "ro_number",
|
||||||
|
width: "8%",
|
||||||
|
sorter: (a, b) =>
|
||||||
|
alphaSort(
|
||||||
|
a.ro_number ? a.ro_number : "EST-" + a.est_number,
|
||||||
|
b.ro_number ? b.ro_number : "EST-" + b.est_number
|
||||||
|
),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||||
|
|
||||||
|
render: (text, record) => (
|
||||||
|
<span>
|
||||||
|
{record.ro_number ? record.ro_number : "EST-" + record.est_number}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.owner"),
|
||||||
|
dataIndex: "owner",
|
||||||
|
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) => {
|
||||||
|
return record.owner ? (
|
||||||
|
<span>
|
||||||
|
{record.ownr_fn} {record.ownr_ln}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>{`${record.ownr_fn} ${record.ownr_ln}`}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.status"),
|
||||||
|
dataIndex: "status",
|
||||||
|
key: "status",
|
||||||
|
width: "10%",
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => {
|
||||||
|
return record.status || t("general.labels.na");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.vehicle"),
|
||||||
|
dataIndex: "vehicle",
|
||||||
|
key: "vehicle",
|
||||||
|
width: "15%",
|
||||||
|
ellipsis: true,
|
||||||
|
render: (text, record) => {
|
||||||
|
return record.vehicleid ? (
|
||||||
|
<span>
|
||||||
|
{`${record.v_model_yr || ""} ${record.v_make_desc ||
|
||||||
|
""} ${record.v_model_desc || ""}`}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
t("jobs.errors.novehicle")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("vehicles.fields.plate_no"),
|
||||||
|
dataIndex: "plate_no",
|
||||||
|
key: "plate_no",
|
||||||
|
width: "8%",
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => {
|
||||||
|
return record.plate_no ? (
|
||||||
|
<span>{record.plate_no}</span>
|
||||||
|
) : (
|
||||||
|
t("general.labels.unknown")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.clm_no"),
|
||||||
|
dataIndex: "clm_no",
|
||||||
|
key: "clm_no",
|
||||||
|
width: "12%",
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => {
|
||||||
|
return record.clm_no ? (
|
||||||
|
<span>{record.clm_no}</span>
|
||||||
|
) : (
|
||||||
|
t("general.labels.unknown")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredData =
|
||||||
|
state.search === ""
|
||||||
|
? data
|
||||||
|
: data.filter(
|
||||||
|
j =>
|
||||||
|
(j.est_number || "")
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(state.search.toLowerCase()) ||
|
||||||
|
(j.ro_number || "")
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(state.search.toLowerCase()) ||
|
||||||
|
(j.ownr_fn || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(state.search.toLowerCase()) ||
|
||||||
|
(j.ownr_ln || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(state.search.toLowerCase()) ||
|
||||||
|
(j.clm_no || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(state.search.toLowerCase()) ||
|
||||||
|
(j.v_make_desc || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(state.search.toLowerCase()) ||
|
||||||
|
(j.v_model_desc || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(state.search.toLowerCase()) ||
|
||||||
|
(j.plate_no || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(state.search.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
loading={loading}
|
||||||
|
title={() => (
|
||||||
|
<Input.Search
|
||||||
|
placeholder={t("general.labels.search")}
|
||||||
|
value={state.search}
|
||||||
|
onChange={e => setState({ ...state, search: e.target.value })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
size="small"
|
||||||
|
pagination={{ position: "top" }}
|
||||||
|
columns={columns.map(item => ({ ...item }))}
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={filteredData}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
rowSelection={{
|
||||||
|
onSelect: handleSelect,
|
||||||
|
type: "radio",
|
||||||
|
selectedRowKeys: [selectedJob]
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { useQuery } from "@apollo/react-hooks";
|
||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
import ContractJobsComponent from "./contract-jobs.component";
|
||||||
|
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
//currentUser: selectCurrentUser
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
export function ContractJobsContainer({ selectedJobState, bodyshop }) {
|
||||||
|
const { loading, error, data } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
|
||||||
|
variables: {
|
||||||
|
statuses: bodyshop.md_ro_statuses.open_statuses || ["Open"]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const [selectedJob, setSelectedJob] = selectedJobState;
|
||||||
|
|
||||||
|
const handleSelect = record => {
|
||||||
|
setSelectedJob(record.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ContractJobsComponent
|
||||||
|
handleSelect={handleSelect}
|
||||||
|
selectedJob={selectedJob}
|
||||||
|
loading={loading}
|
||||||
|
data={data ? data.jobs : []}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default connect(mapStateToProps, null)(ContractJobsContainer);
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Select } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
const ContractStatusComponent = ({
|
||||||
|
value = "contracts.status.new",
|
||||||
|
onChange
|
||||||
|
}) => {
|
||||||
|
const [option, setOption] = useState(value);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (onChange) {
|
||||||
|
onChange(option);
|
||||||
|
}
|
||||||
|
}, [option, onChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
value={option}
|
||||||
|
style={{
|
||||||
|
width: 100
|
||||||
|
}}
|
||||||
|
onChange={setOption}
|
||||||
|
>
|
||||||
|
<Option value="contracts.status.new">{t("contracts.status.new")}</Option>
|
||||||
|
<Option value="contracts.status.out">{t("contracts.status.out")}</Option>
|
||||||
|
<Option value="contracts.status.returned">
|
||||||
|
{t("contracts.status.returned")}
|
||||||
|
</Option>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ContractStatusComponent;
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { Table } from "antd";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
|
|
||||||
|
export default function ContractsList({ loading, contracts }) {
|
||||||
|
const [state, setState] = useState({
|
||||||
|
sortedInfo: {},
|
||||||
|
filteredInfo: { text: "" }
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("contracts.fields.agreementnumber"),
|
||||||
|
dataIndex: "agreementnumber",
|
||||||
|
key: "agreementnumber",
|
||||||
|
sorter: (a, b) => a.agreementnumber - b.agreementnumber,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "agreementnumber" &&
|
||||||
|
state.sortedInfo.order,
|
||||||
|
render: (text, record) => (
|
||||||
|
<Link to={`/manage/courtesycars/contracts/${record.id}`}>
|
||||||
|
{record.agreementnumber || ""}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.ro_number"),
|
||||||
|
dataIndex: "job.ro_number",
|
||||||
|
key: "job.ro_number",
|
||||||
|
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "job.ro_number" &&
|
||||||
|
state.sortedInfo.order,
|
||||||
|
render: (text, record) => (
|
||||||
|
<Link to={`/manage/jobs/${record.job.id}`}>
|
||||||
|
{record.job.ro_number || ""}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("contracts.fields.driver"),
|
||||||
|
dataIndex: "driver_ln",
|
||||||
|
key: "driver_ln",
|
||||||
|
sorter: (a, b) => alphaSort(a.driver_ln, b.driver_ln),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "driver_ln" && state.sortedInfo.order,
|
||||||
|
render: (text, record) =>
|
||||||
|
`${record.driver_fn || ""} ${record.driver_ln || ""}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("contracts.fields.status"),
|
||||||
|
dataIndex: "status",
|
||||||
|
key: "status",
|
||||||
|
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => t(record.status)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("contracts.fields.start"),
|
||||||
|
dataIndex: "start",
|
||||||
|
key: "start",
|
||||||
|
sorter: (a, b) => alphaSort(a.start, b.start),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => <DateFormatter>{record.start}</DateFormatter>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("contracts.fields.scheduledreturn"),
|
||||||
|
dataIndex: "scheduledreturn",
|
||||||
|
key: "scheduledreturn",
|
||||||
|
sorter: (a, b) => alphaSort(a.scheduledreturn, b.scheduledreturn),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "scheduledreturn" &&
|
||||||
|
state.sortedInfo.order,
|
||||||
|
render: (text, record) => (
|
||||||
|
<DateFormatter>{record.scheduledreturn}</DateFormatter>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
loading={loading}
|
||||||
|
size="small"
|
||||||
|
pagination={{ position: "top" }}
|
||||||
|
columns={columns.map(item => ({ ...item }))}
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={contracts}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { Table } from "antd";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
|
|
||||||
|
export default function CourtesyCarContractListComponent({ contracts }) {
|
||||||
|
const [state, setState] = useState({
|
||||||
|
sortedInfo: {},
|
||||||
|
filteredInfo: { text: "" }
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("contracts.fields.agreementnumber"),
|
||||||
|
dataIndex: "agreementnumber",
|
||||||
|
key: "agreementnumber",
|
||||||
|
sorter: (a, b) => a.agreementnumber - b.agreementnumber,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "agreementnumber" &&
|
||||||
|
state.sortedInfo.order,
|
||||||
|
render: (text, record) => (
|
||||||
|
<Link to={`/manage/courtesycars/contracts/${record.id}`}>
|
||||||
|
{record.agreementnumber || ""}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.ro_number"),
|
||||||
|
dataIndex: "job.ro_number",
|
||||||
|
key: "job.ro_number",
|
||||||
|
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "job.ro_number" &&
|
||||||
|
state.sortedInfo.order,
|
||||||
|
render: (text, record) => (
|
||||||
|
<Link to={`/manage/jobs/${record.job.id}`}>
|
||||||
|
{record.job.ro_number || ""}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("contracts.fields.driver"),
|
||||||
|
dataIndex: "driver_ln",
|
||||||
|
key: "driver_ln",
|
||||||
|
sorter: (a, b) => alphaSort(a.driver_ln, b.driver_ln),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "driver_ln" && state.sortedInfo.order,
|
||||||
|
render: (text, record) =>
|
||||||
|
`${record.driver_fn || ""} ${record.driver_ln || ""}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("contracts.fields.status"),
|
||||||
|
dataIndex: "status",
|
||||||
|
key: "status",
|
||||||
|
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => t(record.status)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("contracts.fields.start"),
|
||||||
|
dataIndex: "start",
|
||||||
|
key: "start",
|
||||||
|
sorter: (a, b) => alphaSort(a.start, b.start),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => <DateFormatter>{record.start}</DateFormatter>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("contracts.fields.scheduledreturn"),
|
||||||
|
dataIndex: "scheduledreturn",
|
||||||
|
key: "scheduledreturn",
|
||||||
|
sorter: (a, b) => a.scheduledreturn - b.scheduledreturn,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "scheduledreturn" &&
|
||||||
|
state.sortedInfo.order,
|
||||||
|
render: (text, record) => (
|
||||||
|
<DateFormatter>{record.scheduledreturn}</DateFormatter>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
size="small"
|
||||||
|
pagination={{ position: "top" }}
|
||||||
|
columns={columns.map(item => ({ ...item }))}
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={contracts}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Form, Input, InputNumber, DatePicker, Button } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
|
import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component";
|
||||||
|
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
|
||||||
|
|
||||||
|
export default function CourtesyCarCreateFormComponent() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
{t("general.actions.save")}
|
||||||
|
</Button>
|
||||||
|
<Form.Item
|
||||||
|
label={t("courtesycars.fields.make")}
|
||||||
|
name="make"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("courtesycars.fields.model")}
|
||||||
|
name="model"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("courtesycars.fields.year")}
|
||||||
|
name="year"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("courtesycars.fields.plate")}
|
||||||
|
name="plate"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("courtesycars.fields.color")}
|
||||||
|
name="color"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("courtesycars.fields.vin")}
|
||||||
|
name="vin"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("courtesycars.fields.fleetnumber")}
|
||||||
|
name="fleetnumber"
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("courtesycars.fields.purchasedate")}
|
||||||
|
name="purchasedate"
|
||||||
|
>
|
||||||
|
<DatePicker />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("courtesycars.fields.servicestartdate")}
|
||||||
|
name="servicestartdate"
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("courtesycars.fields.serviceenddate")}
|
||||||
|
name="serviceenddate"
|
||||||
|
>
|
||||||
|
<DatePicker />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("courtesycars.fields.leaseenddate")}
|
||||||
|
name="leaseenddate"
|
||||||
|
>
|
||||||
|
<DatePicker />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("courtesycars.fields.status")}
|
||||||
|
name="status"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CourtesyCarStatus />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("courtesycars.fields.nextservicekm")}
|
||||||
|
name="nextservicekm"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("courtesycars.fields.nextservicedate")}
|
||||||
|
name="nextservicedate"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<DatePicker />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("courtesycars.fields.damage")} name="damage">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("courtesycars.fields.notes")} name="notes">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("courtesycars.fields.fuel")}
|
||||||
|
name="fuel"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CourtesyCarFuelSlider />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("courtesycars.fields.registrationexpires")}
|
||||||
|
name="registrationexpires"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<DatePicker />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("courtesycars.fields.insuranceexpires")}
|
||||||
|
name="insuranceexpires"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<DatePicker />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("courtesycars.fields.dailycost")} name="dailycost">
|
||||||
|
<CurrencyInput />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Slider } from "antd";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
const CourtesyCarFuelComponent = ({ value = 100, onChange }) => {
|
||||||
|
const [option, setOption] = useState(value);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (onChange) {
|
||||||
|
onChange(option);
|
||||||
|
}
|
||||||
|
}, [option, onChange]);
|
||||||
|
|
||||||
|
const marks = {
|
||||||
|
0: {
|
||||||
|
style: {
|
||||||
|
color: "#f50"
|
||||||
|
},
|
||||||
|
label: t("courtesycars.labels.fuel.empty")
|
||||||
|
},
|
||||||
|
13: t("courtesycars.labels.fuel.18"),
|
||||||
|
25: t("courtesycars.labels.fuel.14"),
|
||||||
|
38: t("courtesycars.labels.fuel.38"),
|
||||||
|
50: t("courtesycars.labels.fuel.12"),
|
||||||
|
63: t("courtesycars.labels.fuel.58"),
|
||||||
|
75: t("courtesycars.labels.fuel.34"),
|
||||||
|
88: t("courtesycars.labels.fuel.78"),
|
||||||
|
100: {
|
||||||
|
style: {
|
||||||
|
color: "#008000"
|
||||||
|
},
|
||||||
|
label: <strong>{t("courtesycars.labels.fuel.full")}</strong>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slider
|
||||||
|
marks={marks}
|
||||||
|
defaultValue={value}
|
||||||
|
onChange={setOption}
|
||||||
|
step={null}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default CourtesyCarFuelComponent;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Form, DatePicker, InputNumber } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
|
||||||
|
|
||||||
|
export default function CourtesyCarReturnModalComponent() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={t("contracts.fields.actualreturn")}
|
||||||
|
name="actualreturn"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<DatePicker />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("contracts.fields.kmend")}
|
||||||
|
name="kmend"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("courtesycars.fields.fuel")}
|
||||||
|
name={["courtesycar", "data", "fuel"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CourtesyCarFuelSlider />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { Form, Modal, notification } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
|
import { selectCourtesyCarReturn } from "../../redux/modals/modals.selectors";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import CourtesyCarReturnModalComponent from "./courtesy-car-return-modal.component";
|
||||||
|
import moment from "moment";
|
||||||
|
import { RETURN_CONTRACT } from "../../graphql/cccontracts.queries";
|
||||||
|
import { useMutation } from "@apollo/react-hooks";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
courtesyCarReturnModal: selectCourtesyCarReturn,
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
toggleModalVisible: () => dispatch(toggleModalVisible("courtesyCarReturn"))
|
||||||
|
});
|
||||||
|
|
||||||
|
export function InvoiceEnterModalContainer({
|
||||||
|
courtesyCarReturnModal,
|
||||||
|
toggleModalVisible,
|
||||||
|
bodyshop
|
||||||
|
}) {
|
||||||
|
const { visible, context, actions } = courtesyCarReturnModal;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [updateContract] = useMutation(RETURN_CONTRACT);
|
||||||
|
const handleFinish = values => {
|
||||||
|
console.log("Finish", values);
|
||||||
|
|
||||||
|
updateContract({
|
||||||
|
variables: {
|
||||||
|
contractId: context.contractId,
|
||||||
|
cccontract: {
|
||||||
|
kmend: values.kmend,
|
||||||
|
actualreturn: values.actualreturn,
|
||||||
|
status: "contracts.status.returned"
|
||||||
|
},
|
||||||
|
courtesycarid: context.courtesyCarId,
|
||||||
|
courtesycar: {
|
||||||
|
status: "courtesycars.status.in",
|
||||||
|
fuel: values.fuel,
|
||||||
|
mileage: values.kmend
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(r => {
|
||||||
|
if (actions.refetch) actions.refetch();
|
||||||
|
toggleModalVisible();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("contracts.errors.returning", { error: error })
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t("courtesycars.labels.return")}
|
||||||
|
visible={visible}
|
||||||
|
onCancel={() => toggleModalVisible()}
|
||||||
|
width={"90%"}
|
||||||
|
okText={t("general.actions.save")}
|
||||||
|
onOk={() => form.submit()}
|
||||||
|
okButtonProps={{ htmlType: "submit" }}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
onFinish={handleFinish}
|
||||||
|
initialValues={{ fuel: 100, actualreturn: moment(new Date()) }}
|
||||||
|
>
|
||||||
|
<CourtesyCarReturnModalComponent />
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(InvoiceEnterModalContainer);
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Select } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
const CourtesyCarStatusComponent = ({
|
||||||
|
value = "courtesycars.status.in",
|
||||||
|
onChange
|
||||||
|
}) => {
|
||||||
|
const [option, setOption] = useState(value);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (onChange) {
|
||||||
|
onChange(option);
|
||||||
|
}
|
||||||
|
}, [option, onChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
value={option}
|
||||||
|
style={{
|
||||||
|
width: 100
|
||||||
|
}}
|
||||||
|
onChange={setOption}
|
||||||
|
>
|
||||||
|
<Option value="courtesycars.status.in">
|
||||||
|
{t("courtesycars.status.in")}
|
||||||
|
</Option>
|
||||||
|
<Option value="courtesycars.status.inservice">
|
||||||
|
{t("courtesycars.status.inservice")}
|
||||||
|
</Option>
|
||||||
|
<Option value="courtesycars.status.out">
|
||||||
|
{t("courtesycars.status.out")}
|
||||||
|
</Option>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default CourtesyCarStatusComponent;
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { Table } from "antd";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
|
||||||
|
export default function CourtesyCarsList({ loading, courtesycars }) {
|
||||||
|
const [state, setState] = useState({
|
||||||
|
sortedInfo: {},
|
||||||
|
filteredInfo: { text: "" }
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("courtesycars.fields.fleetnumber"),
|
||||||
|
dataIndex: "fleetnumber",
|
||||||
|
key: "fleetnumber",
|
||||||
|
sorter: (a, b) => alphaSort(a.fleetnumber, b.fleetnumber),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "fleetnumber" && state.sortedInfo.order
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("courtesycars.fields.vin"),
|
||||||
|
dataIndex: "vin",
|
||||||
|
key: "vin",
|
||||||
|
sorter: (a, b) => alphaSort(a.vin, b.vin),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "vin" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => (
|
||||||
|
<Link to={`/manage/courtesycars/${record.id}`}>{record.vin}</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("courtesycars.fields.status"),
|
||||||
|
dataIndex: "status",
|
||||||
|
key: "status",
|
||||||
|
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => t(record.status)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("courtesycars.fields.year"),
|
||||||
|
dataIndex: "year",
|
||||||
|
key: "year",
|
||||||
|
sorter: (a, b) => alphaSort(a.year, b.year),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "year" && state.sortedInfo.order
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("courtesycars.fields.make"),
|
||||||
|
dataIndex: "make",
|
||||||
|
key: "make",
|
||||||
|
sorter: (a, b) => alphaSort(a.make, b.make),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "make" && state.sortedInfo.order
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("courtesycars.fields.model"),
|
||||||
|
dataIndex: "model",
|
||||||
|
key: "model",
|
||||||
|
sorter: (a, b) => alphaSort(a.model, b.model),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "model" && state.sortedInfo.order
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
loading={loading}
|
||||||
|
size="small"
|
||||||
|
pagination={{ position: "top" }}
|
||||||
|
columns={columns.map(item => ({ ...item }))}
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={courtesycars}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { Card } from "antd";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Responsive, WidthProvider } from "react-grid-layout";
|
||||||
|
import styled from "styled-components";
|
||||||
|
//Combination of the following:
|
||||||
|
// /node_modules/react-grid-layout/css/styles.css
|
||||||
|
// /node_modules/react-resizable/css/styles.css
|
||||||
|
import "./dashboard-grid.styles.css";
|
||||||
|
|
||||||
|
const Sdiv = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
height: 80%;
|
||||||
|
width: 80%;
|
||||||
|
top: 10%;
|
||||||
|
left: 10%;
|
||||||
|
// background-color: #ffcc00;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ResponsiveReactGridLayout = WidthProvider(Responsive);
|
||||||
|
|
||||||
|
export default function DashboardGridComponent() {
|
||||||
|
const [state, setState] = useState({
|
||||||
|
layout: [
|
||||||
|
{ i: "1", x: 0, y: 0, w: 2, h: 2 },
|
||||||
|
{ i: "2", x: 2, y: 0, w: 2, h: 2 },
|
||||||
|
{ i: "3", x: 4, y: 0, w: 2, h: 2 }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
className: "layout",
|
||||||
|
breakpoints: { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }
|
||||||
|
// cols: { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 },
|
||||||
|
// rowHeight: 100
|
||||||
|
};
|
||||||
|
|
||||||
|
// We're using the cols coming back from this to calculate where to add new items.
|
||||||
|
const onBreakpointChange = (breakpoint, cols) => {
|
||||||
|
console.log("breakpoint, cols", breakpoint, cols);
|
||||||
|
// setState({ ...state, breakpoint: breakpoint, cols: cols });
|
||||||
|
};
|
||||||
|
if (true) return null;
|
||||||
|
return (
|
||||||
|
<Sdiv>
|
||||||
|
The Grid.
|
||||||
|
<ResponsiveReactGridLayout
|
||||||
|
{...defaultProps}
|
||||||
|
onBreakpointChange={onBreakpointChange}
|
||||||
|
width='100%'
|
||||||
|
onLayoutChange={layout => {
|
||||||
|
console.log("layout", layout);
|
||||||
|
setState({ ...state, layout });
|
||||||
|
}}>
|
||||||
|
{state.layout.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<Card style={{ width: "100px" }} key={item.i} data-grid={item}>
|
||||||
|
A Card {index}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ResponsiveReactGridLayout>
|
||||||
|
</Sdiv>
|
||||||
|
);
|
||||||
|
}
|
||||||
126
client/src/components/dashboard-grid/dashboard-grid.styles.css
Normal file
126
client/src/components/dashboard-grid/dashboard-grid.styles.css
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
.react-resizable {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.react-resizable-handle {
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-origin: content-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iNnB4Ij48ZyBvcGFjaXR5PSIwLjMwMiI+PHBhdGggZD0iTSA2IDYgTCAwIDYgTCAwIDQuMiBMIDQgNC4yIEwgNC4yIDQuMiBMIDQuMiAwIEwgNiAwIEwgNiA2IEwgNiA2IFoiIGZpbGw9IiMwMDAwMDAiLz48L2c+PC9zdmc+");
|
||||||
|
background-position: bottom right;
|
||||||
|
padding: 0 3px 3px 0;
|
||||||
|
}
|
||||||
|
.react-resizable-handle-sw {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
cursor: sw-resize;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
.react-resizable-handle-se {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
cursor: se-resize;
|
||||||
|
}
|
||||||
|
.react-resizable-handle-nw {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
cursor: nw-resize;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
.react-resizable-handle-ne {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
cursor: ne-resize;
|
||||||
|
transform: rotate(270deg);
|
||||||
|
}
|
||||||
|
.react-resizable-handle-w,
|
||||||
|
.react-resizable-handle-e {
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -10px;
|
||||||
|
cursor: ew-resize;
|
||||||
|
}
|
||||||
|
.react-resizable-handle-w {
|
||||||
|
left: 0;
|
||||||
|
transform: rotate(135deg);
|
||||||
|
}
|
||||||
|
.react-resizable-handle-e {
|
||||||
|
right: 0;
|
||||||
|
transform: rotate(315deg);
|
||||||
|
}
|
||||||
|
.react-resizable-handle-n,
|
||||||
|
.react-resizable-handle-s {
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -10px;
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
.react-resizable-handle-n {
|
||||||
|
top: 0;
|
||||||
|
transform: rotate(225deg);
|
||||||
|
}
|
||||||
|
.react-resizable-handle-s {
|
||||||
|
bottom: 0;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
.react-grid-layout {
|
||||||
|
position: relative;
|
||||||
|
transition: height 200ms ease;
|
||||||
|
}
|
||||||
|
.react-grid-item {
|
||||||
|
transition: all 200ms ease;
|
||||||
|
transition-property: left, top;
|
||||||
|
}
|
||||||
|
.react-grid-item.cssTransforms {
|
||||||
|
transition-property: transform;
|
||||||
|
}
|
||||||
|
.react-grid-item.resizing {
|
||||||
|
z-index: 1;
|
||||||
|
will-change: width, height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-grid-item.react-draggable-dragging {
|
||||||
|
transition: none;
|
||||||
|
z-index: 3;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-grid-item.dropping {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-grid-item.react-grid-placeholder {
|
||||||
|
background: red;
|
||||||
|
opacity: 0.2;
|
||||||
|
transition-duration: 100ms;
|
||||||
|
z-index: 2;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
-o-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-grid-item > .react-resizable-handle {
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
cursor: se-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-grid-item > .react-resizable-handle::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
right: 3px;
|
||||||
|
bottom: 3px;
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-right: 2px solid rgba(0, 0, 0, 0.4);
|
||||||
|
border-bottom: 2px solid rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-resizable-hide > .react-resizable-handle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { UploadOutlined } from "@ant-design/icons";
|
||||||
|
import { Button, Upload } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function DocumentsUploadComponent({ handleUpload }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Upload
|
||||||
|
multiple={true}
|
||||||
|
customRequest={handleUpload}
|
||||||
|
accept="audio/*,video/*,image/*"
|
||||||
|
>
|
||||||
|
<Button>
|
||||||
|
<UploadOutlined /> Click to Upload
|
||||||
|
</Button>
|
||||||
|
</Upload>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import { notification } from "antd";
|
||||||
|
import axios from "axios";
|
||||||
|
import React from "react";
|
||||||
|
import { useMutation } from "@apollo/react-hooks";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Resizer from "react-image-file-resizer";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
|
||||||
|
import {
|
||||||
|
selectBodyshop,
|
||||||
|
selectCurrentUser
|
||||||
|
} from "../../redux/user/user.selectors";
|
||||||
|
import { generateCdnThumb } from "../../utils/DocHelpers";
|
||||||
|
import DocumentsUploadComponent from "./documents-upload.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
currentUser: selectCurrentUser,
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
null
|
||||||
|
)(function DocumentsUploadContainer({
|
||||||
|
jobId,
|
||||||
|
invoiceId,
|
||||||
|
currentUser,
|
||||||
|
bodyshop,
|
||||||
|
callbackAfterUpload
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [insertNewDocument] = useMutation(INSERT_NEW_DOCUMENT);
|
||||||
|
|
||||||
|
const handleUpload = ev => {
|
||||||
|
const { onError, onSuccess, onProgress } = ev;
|
||||||
|
//If PDF, upload directly.
|
||||||
|
//If JPEG, resize and upload.
|
||||||
|
//TODO If this is just an invoice job? Where to put it?
|
||||||
|
let key = `${bodyshop.id}/${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,
|
||||||
|
"PNG",
|
||||||
|
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 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 => {
|
||||||
|
insertNewDocument({
|
||||||
|
variables: {
|
||||||
|
docInput: [
|
||||||
|
{
|
||||||
|
jobid: jobId,
|
||||||
|
uploaded_by: currentUser.email,
|
||||||
|
url,
|
||||||
|
thumb_url:
|
||||||
|
fileType === "application/pdf"
|
||||||
|
? "application/pdf"
|
||||||
|
: generateCdnThumb(fileName),
|
||||||
|
key: fileName,
|
||||||
|
invoiceid: invoiceId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}).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")
|
||||||
|
});
|
||||||
|
if (callbackAfterUpload) {
|
||||||
|
callbackAfterUpload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
onError(error);
|
||||||
|
notification["error"]({
|
||||||
|
message: t("documents.errors.insert", {
|
||||||
|
message: JSON.stringify(error)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("documents.errors.getpresignurl", {
|
||||||
|
message: JSON.stringify(error)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return <DocumentsUploadComponent handleUpload={handleUpload} />;
|
||||||
|
});
|
||||||
@@ -3,7 +3,7 @@ import { Input } from "antd";
|
|||||||
import CKEditor from "@ckeditor/ckeditor5-react";
|
import CKEditor from "@ckeditor/ckeditor5-react";
|
||||||
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
|
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
|
||||||
|
|
||||||
export default function SendEmailButtonComponent({
|
export default function EmailOverlayComponent({
|
||||||
messageOptions,
|
messageOptions,
|
||||||
handleConfigChange,
|
handleConfigChange,
|
||||||
handleHtmlChange
|
handleHtmlChange
|
||||||
@@ -13,24 +13,29 @@ export default function SendEmailButtonComponent({
|
|||||||
<Input
|
<Input
|
||||||
defaultValue={messageOptions.to}
|
defaultValue={messageOptions.to}
|
||||||
onChange={handleConfigChange}
|
onChange={handleConfigChange}
|
||||||
name='to'
|
name="to"
|
||||||
/>
|
/>
|
||||||
CC
|
CC
|
||||||
<Input
|
<Input
|
||||||
defaultValue={messageOptions.cc}
|
defaultValue={messageOptions.cc}
|
||||||
onChange={handleConfigChange}
|
onChange={handleConfigChange}
|
||||||
name='cc'
|
name="cc"
|
||||||
/>
|
/>
|
||||||
Subject
|
Subject
|
||||||
<Input
|
<Input
|
||||||
defaultValue={messageOptions.subject}
|
defaultValue={messageOptions.subject}
|
||||||
onChange={handleConfigChange}
|
onChange={handleConfigChange}
|
||||||
name='subject'
|
name="subject"
|
||||||
/>
|
/>
|
||||||
<CKEditor
|
<CKEditor
|
||||||
editor={ClassicEditor}
|
editor={ClassicEditor}
|
||||||
data={messageOptions.html}
|
data={messageOptions.html}
|
||||||
onChange={(event, editor) => {
|
onChange={(event, editor) => {
|
||||||
|
// handleHtmlChange(editor.getData());
|
||||||
|
//TODO Ensure that removing onchange never introduces a race condition
|
||||||
|
}}
|
||||||
|
onBlur={(event, editor) => {
|
||||||
|
console.log("Blur.");
|
||||||
handleHtmlChange(editor.getData());
|
handleHtmlChange(editor.getData());
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { Button, Modal, notification } from "antd";
|
import { Modal, notification } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useLazyQuery } from "react-apollo";
|
import { useLazyQuery } from "@apollo/react-hooks";
|
||||||
import ReactDOMServer from "react-dom/server";
|
import ReactDOMServer from "react-dom/server";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { toggleEmailOverlayVisible } from "../../redux/email/email.actions";
|
import { toggleEmailOverlayVisible } from "../../redux/email/email.actions";
|
||||||
import { selectEmailConfig, selectEmailVisible } from "../../redux/email/email.selectors.js";
|
import {
|
||||||
|
selectEmailConfig,
|
||||||
|
selectEmailVisible
|
||||||
|
} from "../../redux/email/email.selectors.js";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
import EmailOverlayComponent from "./email-overlay.component";
|
import EmailOverlayComponent from "./email-overlay.component";
|
||||||
|
|
||||||
@@ -21,11 +24,17 @@ const mapDispatchToProps = dispatch => ({
|
|||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(function SendEmail({ emailConfig, modalVisible, toggleEmailOverlayVisible }) {
|
)(function EmailOverlayContainer({
|
||||||
|
emailConfig,
|
||||||
|
modalVisible,
|
||||||
|
toggleEmailOverlayVisible
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [messageOptions, setMessageOptions] = useState(
|
const [messageOptions, setMessageOptions] = useState(
|
||||||
emailConfig.messageOptions
|
emailConfig.messageOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMessageOptions(emailConfig.messageOptions);
|
setMessageOptions(emailConfig.messageOptions);
|
||||||
}, [setMessageOptions, emailConfig.messageOptions]);
|
}, [setMessageOptions, emailConfig.messageOptions]);
|
||||||
@@ -53,7 +62,6 @@ export default connect(
|
|||||||
html: ReactDOMServer.renderToStaticMarkup(
|
html: ReactDOMServer.renderToStaticMarkup(
|
||||||
<emailConfig.template data={data} />
|
<emailConfig.template data={data} />
|
||||||
)
|
)
|
||||||
//html: renderEmail(<emailConfig.template data={data} />)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +81,7 @@ export default connect(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfigChange = event => {
|
const handleConfigChange = event => {
|
||||||
const { name, value } = event.target;
|
const { name, value } = event.target;
|
||||||
setMessageOptions({ ...messageOptions, [name]: value });
|
setMessageOptions({ ...messageOptions, [name]: value });
|
||||||
@@ -82,23 +91,28 @@ export default connect(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Modal
|
||||||
<Modal
|
destroyOnClose={true}
|
||||||
destroyOnClose={true}
|
visible={modalVisible}
|
||||||
visible={modalVisible}
|
width={"80%"}
|
||||||
width={"80%"}
|
onOk={handleOk}
|
||||||
onOk={handleOk}
|
onCancel={() => toggleEmailOverlayVisible()}
|
||||||
onCancel={() => toggleEmailOverlayVisible()}>
|
>
|
||||||
<LoadingSpinner loading={loading}>
|
<LoadingSpinner loading={loading}>
|
||||||
<EmailOverlayComponent
|
<EmailOverlayComponent
|
||||||
handleConfigChange={handleConfigChange}
|
handleConfigChange={handleConfigChange}
|
||||||
messageOptions={messageOptions}
|
messageOptions={messageOptions}
|
||||||
handleHtmlChange={handleHtmlChange}
|
handleHtmlChange={handleHtmlChange}
|
||||||
/>
|
/>
|
||||||
</LoadingSpinner>
|
<button
|
||||||
</Modal>
|
onClick={() => {
|
||||||
|
console.log(messageOptions.html);
|
||||||
<Button onClick={() => toggleEmailOverlayVisible()}>Show</Button>
|
navigator.clipboard.writeText(messageOptions.html);
|
||||||
</div>
|
}}
|
||||||
|
>
|
||||||
|
Get HTML
|
||||||
|
</button>
|
||||||
|
</LoadingSpinner>
|
||||||
|
</Modal>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
0
client/src/components/email-overlay/email-setup.md
Normal file
0
client/src/components/email-overlay/email-setup.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { InputNumber } from "antd";
|
||||||
|
import React, { forwardRef } from "react";
|
||||||
|
function FormItemCurrency(props, ref) {
|
||||||
|
return (
|
||||||
|
<InputNumber
|
||||||
|
{...props}
|
||||||
|
//formatter={value => `$ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
|
||||||
|
// parser={value => value.replace(/\$\s?|(,*)/g, "")}
|
||||||
|
precision={2}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default forwardRef(FormItemCurrency);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Icon, Input } from "antd";
|
import { Input } from "antd";
|
||||||
|
import { MailFilled } from "@ant-design/icons";
|
||||||
import React, { forwardRef } from "react";
|
import React, { forwardRef } from "react";
|
||||||
function FormItemEmail(props, ref) {
|
function FormItemEmail(props, ref) {
|
||||||
return (
|
return (
|
||||||
@@ -7,10 +8,10 @@ function FormItemEmail(props, ref) {
|
|||||||
addonAfter={
|
addonAfter={
|
||||||
props.email ? (
|
props.email ? (
|
||||||
<a href={`mailto:${props.email}`}>
|
<a href={`mailto:${props.email}`}>
|
||||||
<Icon type="mail" />
|
<MailFilled />
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<Icon type="mail" />
|
<MailFilled />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { useNProgress } from "@tanem/react-nprogress";
|
||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectLoading } from "../../redux/application/application.selectors";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
loading: selectLoading
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, null)(GlobalLoadingHeader);
|
||||||
|
|
||||||
|
function GlobalLoadingHeader({ loading }) {
|
||||||
|
|
||||||
|
const { animationDuration, isFinished, progress } = useNProgress({
|
||||||
|
isAnimating: loading
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
opacity: isFinished ? 0 : 1,
|
||||||
|
pointerEvents: "none",
|
||||||
|
transition: `opacity ${animationDuration}ms linear`
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "#29d",
|
||||||
|
height: 4,
|
||||||
|
left: 0,
|
||||||
|
marginLeft: `${(-1 + progress) * 100}%`,
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
transition: `margin-left ${animationDuration}ms linear`,
|
||||||
|
width: "100%",
|
||||||
|
zIndex: 1031
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
boxShadow: "0 0 10px #29d, 0 0 5px #29d",
|
||||||
|
display: "block",
|
||||||
|
height: "100%",
|
||||||
|
opacity: 1,
|
||||||
|
position: "absolute",
|
||||||
|
right: 0,
|
||||||
|
transform: "rotate(3deg) translate(0px, -4px)",
|
||||||
|
width: 100
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Avatar, Col, Icon, Menu, Row } from "antd";
|
import Icon, { CarFilled, FileAddFilled, FileFilled, GlobalOutlined, HomeFilled, TeamOutlined } from "@ant-design/icons";
|
||||||
|
import { Avatar, Col, Menu, Row } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { FaCalendarAlt, FaCarCrash } from "react-icons/fa";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import UserImage from "../../assets/User.svg";
|
import UserImage from "../../assets/User.svg";
|
||||||
import { signOutStart } from "../../redux/user/user.actions";
|
|
||||||
import ManageSignInButton from "../manage-sign-in-button/manage-sign-in-button.component";
|
import ManageSignInButton from "../manage-sign-in-button/manage-sign-in-button.component";
|
||||||
|
|
||||||
export default ({
|
export default ({
|
||||||
@@ -11,7 +12,8 @@ export default ({
|
|||||||
selectedNavItem,
|
selectedNavItem,
|
||||||
logo,
|
logo,
|
||||||
handleMenuClick,
|
handleMenuClick,
|
||||||
currentUser
|
currentUser,
|
||||||
|
signOutStart
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
//TODO Add
|
//TODO Add
|
||||||
@@ -23,7 +25,7 @@ export default ({
|
|||||||
<img alt="Shop Logo" src={logo} style={{ height: "40px" }} />
|
<img alt="Shop Logo" src={logo} style={{ height: "40px" }} />
|
||||||
</Col>
|
</Col>
|
||||||
) : null}
|
) : null}
|
||||||
<Col span={14}>
|
<Col span={21}>
|
||||||
{landingHeader ? (
|
{landingHeader ? (
|
||||||
<Menu
|
<Menu
|
||||||
theme="dark"
|
theme="dark"
|
||||||
@@ -49,7 +51,7 @@ export default ({
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Menu.Item onClick={signOutStart()}>
|
<Menu.Item onClick={() => signOutStart()}>
|
||||||
{t("user.actions.signout")}
|
{t("user.actions.signout")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
@@ -60,7 +62,7 @@ export default ({
|
|||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
title={
|
title={
|
||||||
<span>
|
<span>
|
||||||
<Icon type="global" />
|
<GlobalOutlined />
|
||||||
<span>{t("menus.currentuser.languageselector")}</span>
|
<span>{t("menus.currentuser.languageselector")}</span>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -87,14 +89,21 @@ export default ({
|
|||||||
>
|
>
|
||||||
<Menu.Item key="home">
|
<Menu.Item key="home">
|
||||||
<Link to="/manage">
|
<Link to="/manage">
|
||||||
<Icon type="home" />
|
<HomeFilled />
|
||||||
{t("menus.header.home")}
|
{t("menus.header.home")}
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.SubMenu title={t("menus.header.jobs")}>
|
<Menu.SubMenu
|
||||||
|
title={
|
||||||
|
<span>
|
||||||
|
<Icon component={FaCarCrash} />
|
||||||
|
<span>{t("menus.header.jobs")}</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Menu.Item key="schedule">
|
<Menu.Item key="schedule">
|
||||||
<Link to="/manage/schedule">
|
<Link to="/manage/schedule">
|
||||||
<Icon type="calendar" />
|
<Icon component={FaCalendarAlt} />
|
||||||
{t("menus.header.schedule")}
|
{t("menus.header.schedule")}
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
@@ -110,17 +119,46 @@ export default ({
|
|||||||
<Menu.SubMenu title={t("menus.header.customers")}>
|
<Menu.SubMenu title={t("menus.header.customers")}>
|
||||||
<Menu.Item key="owners">
|
<Menu.Item key="owners">
|
||||||
<Link to="/manage/owners">
|
<Link to="/manage/owners">
|
||||||
<Icon type="team" />
|
<TeamOutlined />
|
||||||
{t("menus.header.owners")}
|
{t("menus.header.owners")}
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item key="vehicles">
|
<Menu.Item key="vehicles">
|
||||||
<Link to="/manage/vehicles">
|
<Link to="/manage/vehicles">
|
||||||
<Icon type="car" />
|
<CarFilled />
|
||||||
{t("menus.header.vehicles")}
|
{t("menus.header.vehicles")}
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
|
|
||||||
|
<Menu.SubMenu
|
||||||
|
title={
|
||||||
|
<span>
|
||||||
|
<CarFilled />
|
||||||
|
<span>{t("menus.header.courtesycars")}</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Menu.Item key="courtesycarsall">
|
||||||
|
<Link to="/manage/courtesycars">
|
||||||
|
<CarFilled />
|
||||||
|
{t("menus.header.courtesycars-all")}
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="contracts">
|
||||||
|
<Link to="/manage/courtesycars/contracts">
|
||||||
|
<FileFilled />
|
||||||
|
{t("menus.header.courtesycars-contracts")}
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="newcontract">
|
||||||
|
<Link to="/manage/courtesycars/contracts/new">
|
||||||
|
<FileAddFilled />
|
||||||
|
{t("menus.header.courtesycars-newcontract")}
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.SubMenu>
|
||||||
|
|
||||||
<Menu.SubMenu title={t("menus.header.shop")}>
|
<Menu.SubMenu title={t("menus.header.shop")}>
|
||||||
<Menu.Item key="shop">
|
<Menu.Item key="shop">
|
||||||
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
|
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
|
||||||
@@ -147,7 +185,7 @@ export default ({
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Menu.Item onClick={signOutStart()}>
|
<Menu.Item onClick={() => signOutStart()}>
|
||||||
{t("user.actions.signout")}
|
{t("user.actions.signout")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
@@ -158,7 +196,7 @@ export default ({
|
|||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
title={
|
title={
|
||||||
<span>
|
<span>
|
||||||
<Icon type="global" />
|
<GlobalOutlined />
|
||||||
<span>{t("menus.currentuser.languageselector")}</span>
|
<span>{t("menus.currentuser.languageselector")}</span>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Button, Popover, Input, InputNumber, Form } from "antd";
|
||||||
|
import { SelectOutlined } from "@ant-design/icons";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export default function InvoiceAddLineButton({ jobLine, discount, disabled }) {
|
||||||
|
const [visibility, setVisibility] = useState(false);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const popContent = (
|
||||||
|
<div style={{ display: "flex" }}>
|
||||||
|
<Form.Item name="line_desc" label={t("joblines.fields.line_desc")}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="oem_partno" label={t("joblines.fields.oem_partno")}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="retail" label={t("invoicelines.fields.retail")}>
|
||||||
|
<InputNumber precision={2} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="actual" label={t("invoicelines.fields.actual")}>
|
||||||
|
<InputNumber precision={2} />
|
||||||
|
</Form.Item>
|
||||||
|
DISC: {discount}
|
||||||
|
<Button onClick={() => setVisibility(false)}>X</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover content={popContent} visible={visibility}>
|
||||||
|
<Button onClick={() => setVisibility(true)} disabled={!disabled}>
|
||||||
|
<SelectOutlined />
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,71 +1,176 @@
|
|||||||
import { Modal, Form, Input, InputNumber } from "antd";
|
import {
|
||||||
|
Button,
|
||||||
|
DatePicker,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
Select,
|
||||||
|
Switch,
|
||||||
|
Tag
|
||||||
|
} from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import ResetForm from "../form-items-formatted/reset-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
|
import InvoiceEnterModalLinesComponent from "./invoice-enter-modal.lines.component";
|
||||||
|
|
||||||
export default function InvoiceEnterModalComponent({
|
export default function InvoiceEnterModalComponent({
|
||||||
visible,
|
visible,
|
||||||
invoice,
|
invoice,
|
||||||
handleCancel,
|
handleCancel,
|
||||||
handleSubmit,
|
handleFinish,
|
||||||
form
|
handleRoSelect,
|
||||||
|
roAutoCompleteOptions,
|
||||||
|
handleVendorSelect,
|
||||||
|
vendorAutoCompleteOptions,
|
||||||
|
lineData,
|
||||||
|
vendor,
|
||||||
|
job,
|
||||||
|
responsibilityCenters
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { getFieldDecorator, isFieldsTouched, resetFields } = form;
|
const [form] = Form.useForm();
|
||||||
|
const { resetFields } = form;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Form onFinish={handleFinish} autoComplete={"off"} form={form}>
|
||||||
title={
|
<Modal
|
||||||
invoice && invoice.id
|
title={
|
||||||
? t("invoice.labels.edit")
|
invoice && invoice.id
|
||||||
: t("invoice.labels.new")
|
? t("invoices.labels.edit")
|
||||||
}
|
: t("invoices.labels.new")
|
||||||
visible={visible}
|
|
||||||
okText={t("general.labels.save")}
|
|
||||||
onOk={handleSubmit}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
>
|
|
||||||
{isFieldsTouched() ? <ResetForm resetFields={resetFields} /> : null}
|
|
||||||
<Form onSubmit={handleSubmit} autoComplete={"off"}>
|
|
||||||
{JSON.stringify(invoice)}
|
|
||||||
{
|
|
||||||
// <Form.Item label={t("joblines.fields.line_desc")}>
|
|
||||||
// {getFieldDecorator("line_desc", {
|
|
||||||
// initialValue: jobLine.line_desc
|
|
||||||
// })(<Input name="line_desc" />)}
|
|
||||||
// </Form.Item>
|
|
||||||
// <Form.Item label={t("joblines.fields.oem_partno")}>
|
|
||||||
// {getFieldDecorator("oem_partno", {
|
|
||||||
// initialValue: jobLine.oem_partno
|
|
||||||
// })(<Input name="oem_partno" />)}
|
|
||||||
// </Form.Item>
|
|
||||||
// <Form.Item label={t("joblines.fields.part_type")}>
|
|
||||||
// {getFieldDecorator("part_type", {
|
|
||||||
// initialValue: jobLine.part_type
|
|
||||||
// })(<Input name="part_type" />)}
|
|
||||||
// </Form.Item>
|
|
||||||
// <Form.Item label={t("joblines.fields.mod_lbr_ty")}>
|
|
||||||
// {getFieldDecorator("mod_lbr_ty", {
|
|
||||||
// initialValue: jobLine.mod_lbr_ty
|
|
||||||
// })(<Input name="mod_lbr_ty" />)}
|
|
||||||
// </Form.Item>
|
|
||||||
// <Form.Item label={t("joblines.fields.op_code_desc")}>
|
|
||||||
// {getFieldDecorator("op_code_desc", {
|
|
||||||
// initialValue: jobLine.op_code_desc
|
|
||||||
// })(<Input name="op_code_desc" />)}
|
|
||||||
// </Form.Item>
|
|
||||||
// <Form.Item label={t("joblines.fields.mod_lb_hrs")}>
|
|
||||||
// {getFieldDecorator("mod_lb_hrs", {
|
|
||||||
// initialValue: jobLine.mod_lb_hrs
|
|
||||||
// })(<InputNumber name="mod_lb_hrs" />)}
|
|
||||||
// </Form.Item>
|
|
||||||
// <Form.Item label={t("joblines.fields.act_price")}>
|
|
||||||
// {getFieldDecorator("act_price", {
|
|
||||||
// initialValue: jobLine.act_price
|
|
||||||
// })(<InputNumber name="act_price" />)}
|
|
||||||
// </Form.Item>
|
|
||||||
}
|
}
|
||||||
</Form>
|
width={"90%"}
|
||||||
</Modal>
|
visible={visible}
|
||||||
|
okText={t("general.actions.save")}
|
||||||
|
onOk={() => form.submit()}
|
||||||
|
okButtonProps={{ htmlType: "submit" }}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex" }}>
|
||||||
|
<Form.Item
|
||||||
|
name="jobid"
|
||||||
|
label={t("invoices.fields.ro_number")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
defaultValue={
|
||||||
|
job ? (job.ro_number ? job.ro_number : job.est_number) : null
|
||||||
|
}
|
||||||
|
autoFocus
|
||||||
|
defaultOpen
|
||||||
|
style={{ width: "300px" }}
|
||||||
|
onSelect={handleRoSelect}
|
||||||
|
>
|
||||||
|
{roAutoCompleteOptions
|
||||||
|
? roAutoCompleteOptions.map(o => (
|
||||||
|
<Select.Option
|
||||||
|
key={o.id}
|
||||||
|
value={o.ro_number ? o.ro_number : o.est_number}
|
||||||
|
>
|
||||||
|
{`${
|
||||||
|
o.ro_number ? o.ro_number : o.est_number
|
||||||
|
} | ${o.ownr_ln || ""} ${o.ownr_fn ||
|
||||||
|
""} | ${o.v_model_yr || ""} ${o.v_make_desc ||
|
||||||
|
""} ${o.v_model_desc || ""}`}
|
||||||
|
</Select.Option>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("invoices.fields.vendor")}
|
||||||
|
name="vendorid"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
onSelect={handleVendorSelect}
|
||||||
|
style={{ width: "300px" }}
|
||||||
|
>
|
||||||
|
{vendorAutoCompleteOptions
|
||||||
|
? vendorAutoCompleteOptions.map(o => (
|
||||||
|
<Select.Option key={o.id} value={o.name}>
|
||||||
|
<div style={{ display: "flex" }}>
|
||||||
|
{o.name}
|
||||||
|
<Tag color="green">{`${o.discount * 100}%`}</Tag>
|
||||||
|
</div>
|
||||||
|
</Select.Option>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Button onClick={() => resetFields()}>
|
||||||
|
{t("general.actions.reset")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex" }}>
|
||||||
|
<Form.Item
|
||||||
|
label={t("invoices.fields.invoice_number")}
|
||||||
|
name="invoice_number"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("invoices.fields.date")}
|
||||||
|
name="date"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<DatePicker />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("invoices.fields.is_credit_memo")}
|
||||||
|
name="is_credit_memo"
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("invoices.fields.total")}
|
||||||
|
name="total"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<InvoiceEnterModalLinesComponent
|
||||||
|
lineData={lineData}
|
||||||
|
discount={vendor && vendor.discount}
|
||||||
|
form={form}
|
||||||
|
responsibilityCenters={responsibilityCenters}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button onClick={() => console.log(form.getFieldsValue())}>
|
||||||
|
Field Values
|
||||||
|
</Button>
|
||||||
|
</Modal>
|
||||||
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import { Form, notification } from "antd";
|
import React, { useState } from "react";
|
||||||
import React from "react";
|
import { notification } from "antd";
|
||||||
import { useMutation } from "react-apollo";
|
import { useLazyQuery, useQuery, useMutation } from "@apollo/react-hooks";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {
|
import { GET_JOB_LINES_TO_ENTER_INVOICE } from "../../graphql/jobs-lines.queries";
|
||||||
INSERT_NEW_JOB_LINE,
|
import { ACTIVE_JOBS_FOR_AUTOCOMPLETE } from "../../graphql/jobs.queries";
|
||||||
UPDATE_JOB_LINE
|
import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
|
||||||
} from "../../graphql/jobs-lines.queries";
|
|
||||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
import { selectInvoiceEnterModal } from "../../redux/modals/modals.selectors";
|
import { selectInvoiceEnterModal } from "../../redux/modals/modals.selectors";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import InvoiceEnterModalComponent from "./invoice-enter-modal.component";
|
import InvoiceEnterModalComponent from "./invoice-enter-modal.component";
|
||||||
|
import { INSERT_NEW_INVOICE } from "../../graphql/invoices.queries";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
invoiceEnterModal: selectInvoiceEnterModal
|
invoiceEnterModal: selectInvoiceEnterModal,
|
||||||
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("invoiceEnter"))
|
toggleModalVisible: () => dispatch(toggleModalVisible("invoiceEnter"))
|
||||||
@@ -22,71 +24,84 @@ const mapDispatchToProps = dispatch => ({
|
|||||||
function InvoiceEnterModalContainer({
|
function InvoiceEnterModalContainer({
|
||||||
invoiceEnterModal,
|
invoiceEnterModal,
|
||||||
toggleModalVisible,
|
toggleModalVisible,
|
||||||
form
|
bodyshop
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// const [insertJobLine] = useMutation(INSERT_NEW_JOB_LINE);
|
const linesState = useState([]);
|
||||||
// const [updateJobLine] = useMutation(UPDATE_JOB_LINE);
|
const roSearchState = useState({ text: "", selectedId: null });
|
||||||
|
const [roSearch, setRoSearch] = roSearchState;
|
||||||
|
|
||||||
const handleSubmit = e => {
|
const [insertInvoice] = useMutation(INSERT_NEW_INVOICE);
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
form.validateFieldsAndScroll((err, values) => {
|
const { data: RoAutoCompleteData } = useQuery(ACTIVE_JOBS_FOR_AUTOCOMPLETE, {
|
||||||
if (err) {
|
fetchPolicy: "network-only",
|
||||||
notification["error"]({
|
variables: { statuses: bodyshop.md_ro_statuses.open_statuses || ["Open"] },
|
||||||
message: t("invoices.errors.validation"),
|
skip: !invoiceEnterModal.visible
|
||||||
description: err.message
|
});
|
||||||
|
|
||||||
|
const vendorSearchState = useState({
|
||||||
|
text: "",
|
||||||
|
selectedId: null
|
||||||
|
});
|
||||||
|
const [vendorSearch, setVendorSearch] = vendorSearchState;
|
||||||
|
const { data: VendorAutoCompleteData } = useQuery(
|
||||||
|
SEARCH_VENDOR_AUTOCOMPLETE,
|
||||||
|
{
|
||||||
|
fetchPolicy: "network-only",
|
||||||
|
skip: !invoiceEnterModal.visible
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const [loadLines, { called, data: lineData }] = useLazyQuery(
|
||||||
|
GET_JOB_LINES_TO_ENTER_INVOICE,
|
||||||
|
{
|
||||||
|
fetchPolicy: "network-only",
|
||||||
|
variables: { id: roSearch.selectedId }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (roSearch.selectedId) {
|
||||||
|
if (!called) loadLines();
|
||||||
|
}
|
||||||
|
const handleRoSelect = (value, obj) => {
|
||||||
|
setRoSearch({ ...roSearch, selectedId: obj.key });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVendorSelect = (value, obj) => {
|
||||||
|
setVendorSearch({ ...vendorSearch, selectedId: obj.key });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFinish = values => {
|
||||||
|
insertInvoice({
|
||||||
|
variables: {
|
||||||
|
invoice: [
|
||||||
|
Object.assign(
|
||||||
|
{},
|
||||||
|
values,
|
||||||
|
{ jobid: roSearch.selectedId },
|
||||||
|
{ vendorid: vendorSearch.selectedId },
|
||||||
|
{ invoicelines: { data: values.invoicelines } }
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(r => {
|
||||||
|
// if (jobLineEditModal.actions.refetch)
|
||||||
|
// jobLineEditModal.actions.refetch();
|
||||||
|
// toggleModalVisible();
|
||||||
|
notification["success"]({
|
||||||
|
message: t("invoices.successes.created")
|
||||||
});
|
});
|
||||||
}
|
})
|
||||||
if (!err) {
|
.catch(error => {
|
||||||
alert("Closing this modal.");
|
console.log("error", error);
|
||||||
toggleModalVisible();
|
|
||||||
// if (!jobLineEditModal.context.id) {
|
notification["error"]({
|
||||||
// insertJobLine({
|
message: t("invoices.errors.creating", {
|
||||||
// variables: {
|
message: error.message
|
||||||
// lineInput: [{ jobid: jobLineEditModal.context.jobid, ...values }]
|
})
|
||||||
// }
|
});
|
||||||
// })
|
});
|
||||||
// .then(r => {
|
|
||||||
// if (jobLineEditModal.actions.refetch)
|
|
||||||
// jobLineEditModal.actions.refetch();
|
|
||||||
// toggleModalVisible();
|
|
||||||
// notification["success"]({
|
|
||||||
// message: t("joblines.successes.created")
|
|
||||||
// });
|
|
||||||
// })
|
|
||||||
// .catch(error => {
|
|
||||||
// notification["error"]({
|
|
||||||
// message: t("joblines.errors.creating", {
|
|
||||||
// message: error.message
|
|
||||||
// })
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// } else {
|
|
||||||
// updateJobLine({
|
|
||||||
// variables: {
|
|
||||||
// lineId: jobLineEditModal.context.id,
|
|
||||||
// line: values
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .then(r => {
|
|
||||||
// notification["success"]({
|
|
||||||
// message: t("joblines.successes.updated")
|
|
||||||
// });
|
|
||||||
// })
|
|
||||||
// .catch(error => {
|
|
||||||
// notification["success"]({
|
|
||||||
// message: t("joblines.errors.updating", {
|
|
||||||
// message: error.message
|
|
||||||
// })
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// if (jobLineEditModal.actions.refetch)
|
|
||||||
// jobLineEditModal.actions.refetch();
|
|
||||||
// toggleModalVisible();
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
@@ -97,9 +112,26 @@ function InvoiceEnterModalContainer({
|
|||||||
<InvoiceEnterModalComponent
|
<InvoiceEnterModalComponent
|
||||||
visible={invoiceEnterModal.visible}
|
visible={invoiceEnterModal.visible}
|
||||||
invoice={invoiceEnterModal.context}
|
invoice={invoiceEnterModal.context}
|
||||||
handleSubmit={handleSubmit}
|
handleFinish={handleFinish}
|
||||||
handleCancel={handleCancel}
|
handleCancel={handleCancel}
|
||||||
form={form}
|
handleRoSelect={handleRoSelect}
|
||||||
|
roAutoCompleteOptions={RoAutoCompleteData && RoAutoCompleteData.jobs}
|
||||||
|
handleVendorSelect={handleVendorSelect}
|
||||||
|
vendorAutoCompleteOptions={
|
||||||
|
VendorAutoCompleteData && VendorAutoCompleteData.vendors
|
||||||
|
}
|
||||||
|
linesState={linesState}
|
||||||
|
lineData={lineData ? lineData.joblines : null}
|
||||||
|
vendor={
|
||||||
|
vendorSearch.selectedId
|
||||||
|
? VendorAutoCompleteData &&
|
||||||
|
VendorAutoCompleteData.vendors.filter(
|
||||||
|
v => v.id === vendorSearch.selectedId
|
||||||
|
)[0]
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
job={invoiceEnterModal.context.job || null}
|
||||||
|
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -107,8 +139,4 @@ function InvoiceEnterModalContainer({
|
|||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(
|
)(InvoiceEnterModalContainer);
|
||||||
Form.create({ name: "InvoiceEnterModalContainer" })(
|
|
||||||
InvoiceEnterModalContainer
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
|
import { Button, Col, Form, Input, Row, Select, Tag } from "antd";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
|
|
||||||
|
export default function InvoiceEnterModalLinesComponent({
|
||||||
|
lineData,
|
||||||
|
discount,
|
||||||
|
form,
|
||||||
|
responsibilityCenters
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { setFieldsValue, getFieldsValue } = form;
|
||||||
|
|
||||||
|
const [amounts, setAmounts] = useState({ invoiceTotal: 0, enteredAmount: 0 });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.List name="invoicelines">
|
||||||
|
{(fields, { add, remove }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<Form.Item required={false} key={field.key}>
|
||||||
|
<div style={{ display: "flex" }}>
|
||||||
|
<Form.Item
|
||||||
|
label={t("invoicelines.fields.line_desc")}
|
||||||
|
key={`${index}joblinename`}
|
||||||
|
name={[field.name, "joblinename"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
autoFocus
|
||||||
|
name={`le${index}`}
|
||||||
|
style={{ width: "450px" }}
|
||||||
|
onSelect={(value, opt) => {
|
||||||
|
setFieldsValue({
|
||||||
|
invoicelines: getFieldsValue([
|
||||||
|
"invoicelines"
|
||||||
|
]).invoicelines.map((item, idx) => {
|
||||||
|
if (idx === index) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
joblineid: opt.key.includes("noline")
|
||||||
|
? null
|
||||||
|
: opt.key,
|
||||||
|
line_desc: opt.key.includes("noline")
|
||||||
|
? ""
|
||||||
|
: opt.value,
|
||||||
|
actual_price: opt.cost ? opt.cost : 0,
|
||||||
|
cost_center: opt.part_type
|
||||||
|
? responsibilityCenters.defaults[
|
||||||
|
opt.part_type
|
||||||
|
] || null
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
showSearch
|
||||||
|
>
|
||||||
|
<Select.Option
|
||||||
|
key={`${index}noline`}
|
||||||
|
value={t("invoicelines.labels.other")}
|
||||||
|
cost={0}
|
||||||
|
>
|
||||||
|
{t("invoicelines.labels.other")}
|
||||||
|
</Select.Option>
|
||||||
|
{lineData
|
||||||
|
? lineData.map(item => (
|
||||||
|
<Select.Option
|
||||||
|
key={item.id}
|
||||||
|
value={item.line_desc}
|
||||||
|
cost={item.act_price ? item.act_price : 0}
|
||||||
|
part_type={item.part_type}
|
||||||
|
>
|
||||||
|
<Row justify="center" align="middle">
|
||||||
|
<Col span={12}> {item.line_desc}</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Tag color="blue">{item.oem_partno}</Tag>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Tag color="green">
|
||||||
|
<CurrencyFormatter>
|
||||||
|
{item.act_price}
|
||||||
|
</CurrencyFormatter>
|
||||||
|
</Tag>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Select.Option>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
{getFieldsValue("invoicelines").invoicelines[index] &&
|
||||||
|
getFieldsValue("invoicelines").invoicelines[index]
|
||||||
|
.joblinename &&
|
||||||
|
!getFieldsValue("invoicelines").invoicelines[index]
|
||||||
|
.joblineid ? (
|
||||||
|
<Form.Item
|
||||||
|
label={t("invoicelines.fields.line_desc")}
|
||||||
|
key={`${index}line_desc`}
|
||||||
|
name={[field.name, "line_desc"]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("invoicelines.fields.actual")}
|
||||||
|
key={`${index}actual_price`}
|
||||||
|
name={[field.name, "actual_price"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput
|
||||||
|
onBlur={e => {
|
||||||
|
setFieldsValue({
|
||||||
|
invoicelines: getFieldsValue(
|
||||||
|
"invoicelines"
|
||||||
|
).invoicelines.map((item, idx) => {
|
||||||
|
if (idx === index) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
actual_cost:
|
||||||
|
parseFloat(e.target.value) * (1 - discount)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("invoicelines.fields.actual_cost")}
|
||||||
|
key={`${index}actual_cost`}
|
||||||
|
name={[field.name, "actual_cost"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput
|
||||||
|
onBlur={() =>
|
||||||
|
setAmounts({
|
||||||
|
invoiceTotal: getFieldsValue().total,
|
||||||
|
enteredTotal: getFieldsValue("invoicelines")
|
||||||
|
.invoicelines
|
||||||
|
? getFieldsValue(
|
||||||
|
"invoicelines"
|
||||||
|
).invoicelines.reduce(
|
||||||
|
(acc, value) =>
|
||||||
|
acc +
|
||||||
|
(value && value.actual_cost
|
||||||
|
? value.actual_cost
|
||||||
|
: 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("invoicelines.fields.cost_center")}
|
||||||
|
key={`${index}cost_center`}
|
||||||
|
name={[field.name, "cost_center"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select style={{ width: "150px" }}>
|
||||||
|
{responsibilityCenters.costs.map(item => (
|
||||||
|
<Select.Option key={item}>{item}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<DeleteFilled
|
||||||
|
onClick={() => {
|
||||||
|
remove(field.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
onClick={() => {
|
||||||
|
add();
|
||||||
|
}}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
>
|
||||||
|
{t("invoicelines.actions.newline")}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.List>
|
||||||
|
<Row>
|
||||||
|
<Col span={4}>
|
||||||
|
{t("invoicelines.labels.entered")}
|
||||||
|
<CurrencyFormatter>{amounts.enteredTotal || 0}</CurrencyFormatter>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col span={4}>
|
||||||
|
{amounts.invoiceTotal - amounts.enteredTotal === 0 ? (
|
||||||
|
<Tag color="green">{t("invoicelines.labels.reconciled")}</Tag>
|
||||||
|
) : (
|
||||||
|
<Tag color="red">
|
||||||
|
{t("invoicelines.labels.unreconciled")}:
|
||||||
|
<CurrencyFormatter>
|
||||||
|
{amounts.invoiceTotal - amounts.enteredTotal}
|
||||||
|
</CurrencyFormatter>
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Table } from "antd";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
|
export default function InvoicesListTableComponent({ loading, invoices }) {
|
||||||
|
const [state, setState] = useState({
|
||||||
|
sortedInfo: {}
|
||||||
|
});
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("invoices.fields.vendorname"),
|
||||||
|
dataIndex: "vendorname",
|
||||||
|
key: "vendorname",
|
||||||
|
// onFilter: (value, record) => record.ro_number.includes(value),
|
||||||
|
// filteredValue: state.filteredInfo.text || null,
|
||||||
|
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
|
||||||
|
//ellipsis: true,
|
||||||
|
render: (text, record) => <span>{record.vendor.name}</span>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("invoices.fields.invoice_number"),
|
||||||
|
dataIndex: "invoice_number",
|
||||||
|
key: "invoice_number",
|
||||||
|
// onFilter: (value, record) => record.ro_number.includes(value),
|
||||||
|
// filteredValue: state.filteredInfo.text || null,
|
||||||
|
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "invoice_number" &&
|
||||||
|
state.sortedInfo.order
|
||||||
|
//ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("invoices.fields.date"),
|
||||||
|
dataIndex: "date",
|
||||||
|
key: "date",
|
||||||
|
// onFilter: (value, record) => record.ro_number.includes(value),
|
||||||
|
// filteredValue: state.filteredInfo.text || null,
|
||||||
|
sorter: (a, b) => a.date - b.date,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
||||||
|
//ellipsis: true,
|
||||||
|
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
|
||||||
|
const rowExpander = record => (
|
||||||
|
<div style={{ margin: 0 }}>Invoice details</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
loading={loading}
|
||||||
|
size="small"
|
||||||
|
expandedRowRender={rowExpander}
|
||||||
|
pagination={{ position: "top", defaultPageSize: 25 }}
|
||||||
|
columns={columns.map(item => ({ ...item }))}
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={invoices}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,12 +1,21 @@
|
|||||||
|
import {
|
||||||
|
EditFilled,
|
||||||
|
FileImageFilled,
|
||||||
|
PrinterFilled,
|
||||||
|
ShoppingFilled
|
||||||
|
} from "@ant-design/icons";
|
||||||
import { useQuery } from "@apollo/react-hooks";
|
import { useQuery } from "@apollo/react-hooks";
|
||||||
import { Button, Icon, PageHeader, Tag } from "antd";
|
import { Button, PageHeader, Tag } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries";
|
import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries";
|
||||||
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container";
|
import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container";
|
||||||
|
import ScheduleJobModalContainer from "../schedule-job-modal/schedule-job-modal.container";
|
||||||
//import JobDetailCardsHeaderComponent from "./job-detail-cards.header.component";
|
//import JobDetailCardsHeaderComponent from "./job-detail-cards.header.component";
|
||||||
import JobDetailCardsCustomerComponent from "./job-detail-cards.customer.component";
|
import JobDetailCardsCustomerComponent from "./job-detail-cards.customer.component";
|
||||||
import JobDetailCardsDamageComponent from "./job-detail-cards.damage.component";
|
import JobDetailCardsDamageComponent from "./job-detail-cards.damage.component";
|
||||||
@@ -17,9 +26,13 @@ import JobDetailCardsNotesComponent from "./job-detail-cards.notes.component";
|
|||||||
import JobDetailCardsPartsComponent from "./job-detail-cards.parts.component";
|
import JobDetailCardsPartsComponent from "./job-detail-cards.parts.component";
|
||||||
import "./job-detail-cards.styles.scss";
|
import "./job-detail-cards.styles.scss";
|
||||||
import JobDetailCardsTotalsComponent from "./job-detail-cards.totals.component";
|
import JobDetailCardsTotalsComponent from "./job-detail-cards.totals.component";
|
||||||
import ScheduleJobModalContainer from "../schedule-job-modal/schedule-job-modal.container";
|
|
||||||
|
|
||||||
export default function JobDetailCards({ selectedJob }) {
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
setInvoiceEnterContext: context =>
|
||||||
|
dispatch(setModalContext({ context: context, modal: "invoiceEnter" }))
|
||||||
|
});
|
||||||
|
|
||||||
|
function JobDetailCards({ selectedJob, setInvoiceEnterContext }) {
|
||||||
const { loading, error, data, refetch } = useQuery(QUERY_JOB_CARD_DETAILS, {
|
const { loading, error, data, refetch } = useQuery(QUERY_JOB_CARD_DETAILS, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
variables: { id: selectedJob },
|
variables: { id: selectedJob },
|
||||||
@@ -33,10 +46,10 @@ export default function JobDetailCards({ selectedJob }) {
|
|||||||
return <div>{t("jobs.errors.nojobselected")}</div>;
|
return <div>{t("jobs.errors.nojobselected")}</div>;
|
||||||
}
|
}
|
||||||
if (loading) return <LoadingSpinner />;
|
if (loading) return <LoadingSpinner />;
|
||||||
if (error) return <AlertComponent message={error.message} type='error' />;
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='job-cards-container'>
|
<div className="job-cards-container">
|
||||||
<NoteUpsertModal
|
<NoteUpsertModal
|
||||||
jobId={data.jobs_by_pk.id}
|
jobId={data.jobs_by_pk.id}
|
||||||
visible={noteModalVisible}
|
visible={noteModalVisible}
|
||||||
@@ -52,9 +65,9 @@ export default function JobDetailCards({ selectedJob }) {
|
|||||||
ghost={false}
|
ghost={false}
|
||||||
onBack={() => window.history.back()}
|
onBack={() => window.history.back()}
|
||||||
tags={
|
tags={
|
||||||
<span key='job-status'>
|
<span key="job-status">
|
||||||
{data.jobs_by_pk.status ? (
|
{data.jobs_by_pk.status ? (
|
||||||
<Tag color='blue'>{data.jobs_by_pk.status}</Tag>
|
<Tag color="blue">{data.jobs_by_pk.status}</Tag>
|
||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -73,39 +86,53 @@ export default function JobDetailCards({ selectedJob }) {
|
|||||||
}
|
}
|
||||||
extra={[
|
extra={[
|
||||||
<Button
|
<Button
|
||||||
key='schedule'
|
key="schedule"
|
||||||
//TODO Enabled logic based on status.
|
//TODO Enabled logic based on status.
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
scheduleModalState[1](true);
|
scheduleModalState[1](true);
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{t("jobs.actions.schedule")}
|
{t("jobs.actions.schedule")}
|
||||||
</Button>,
|
</Button>,
|
||||||
<Link
|
<Link
|
||||||
key='documents'
|
key="documents"
|
||||||
to={`/manage/jobs/${data.jobs_by_pk.id}#documents`}>
|
to={`/manage/jobs/${data.jobs_by_pk.id}#documents`}
|
||||||
|
>
|
||||||
<Button>
|
<Button>
|
||||||
<Icon type='file-image' />
|
<FileImageFilled />
|
||||||
{t("jobs.actions.addDocuments")}
|
{t("jobs.actions.addDocuments")}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>,
|
</Link>,
|
||||||
<Button key='printing'>
|
<Button key="printing">
|
||||||
<Icon type='printer' />
|
<PrinterFilled />
|
||||||
{t("jobs.actions.printCenter")}
|
{t("jobs.actions.printCenter")}
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button
|
<Button
|
||||||
key='notes'
|
key="notes"
|
||||||
actiontype='addNote'
|
actiontype="addNote"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setNoteModalVisible(!noteModalVisible);
|
setNoteModalVisible(!noteModalVisible);
|
||||||
}}>
|
}}
|
||||||
<Icon type='edit' />
|
>
|
||||||
|
<EditFilled />
|
||||||
{t("jobs.actions.addNote")}
|
{t("jobs.actions.addNote")}
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button key='postinvoices'>
|
<Button
|
||||||
<Icon type='shopping-cart' />
|
key="postinvoices"
|
||||||
|
onClick={() => {
|
||||||
|
setInvoiceEnterContext({
|
||||||
|
actions: { refetch: null },
|
||||||
|
context: {
|
||||||
|
job: data.jobs_by_pk
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ShoppingFilled />
|
||||||
{t("jobs.actions.postInvoices")}
|
{t("jobs.actions.postInvoices")}
|
||||||
</Button>
|
</Button>
|
||||||
]}>
|
]}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
// loading ? (
|
// loading ? (
|
||||||
// <LoadingSkeleton />
|
// <LoadingSkeleton />
|
||||||
@@ -126,7 +153,7 @@ export default function JobDetailCards({ selectedJob }) {
|
|||||||
// )
|
// )
|
||||||
}
|
}
|
||||||
|
|
||||||
<section className='job-cards'>
|
<section className="job-cards">
|
||||||
<JobDetailCardsCustomerComponent
|
<JobDetailCardsCustomerComponent
|
||||||
loading={loading}
|
loading={loading}
|
||||||
data={data ? data.jobs_by_pk : null}
|
data={data ? data.jobs_by_pk : null}
|
||||||
@@ -171,3 +198,4 @@ export default function JobDetailCards({ selectedJob }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export default connect(null, mapDispatchToProps)(JobDetailCards);
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ export default function JobDetailCardsCustomerComponent({ loading, data }) {
|
|||||||
<CardTemplate
|
<CardTemplate
|
||||||
loading={loading}
|
loading={loading}
|
||||||
title={t("jobs.labels.cards.customer")}
|
title={t("jobs.labels.cards.customer")}
|
||||||
extraLink={data && data.owner ? `/manage/owners/${data.owner.id}` : null}>
|
extraLink={data && data.owner ? `/manage/owners/${data.owner.id}` : null}
|
||||||
|
>
|
||||||
{data ? (
|
{data ? (
|
||||||
<span>
|
<span>
|
||||||
<div>
|
<div>
|
||||||
@@ -34,14 +35,22 @@ export default function JobDetailCardsCustomerComponent({ loading, data }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>{`${(data.owner && data.owner.preferred_contact) || ""}`}</div>
|
<div>{`${(data.owner && data.owner.preferred_contact) || ""}`}</div>
|
||||||
{data.vehicle ? (
|
<div>
|
||||||
<Link to={`/manage/vehicles/${data.vehicle.id}`}>
|
{data.vehicle ? (
|
||||||
{`${data.vehicle.v_model_yr || ""} ${data.vehicle.v_make_desc ||
|
<Link to={`/manage/vehicles/${data.vehicleid}`}>
|
||||||
""} ${data.vehicle.v_model_desc || ""}`}
|
{`${data.v_model_yr || ""} ${data.v_make_desc ||
|
||||||
</Link>
|
""} ${data.v_model_desc || ""}`}
|
||||||
) : (
|
a
|
||||||
<span>{t("jobs.errors.novehicle")}</span>
|
</Link>
|
||||||
)}
|
) : (
|
||||||
|
<span>
|
||||||
|
{`${data.v_model_yr || ""} ${data.v_make_desc ||
|
||||||
|
""} ${data.v_model_desc || ""}`}
|
||||||
|
b
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
e
|
||||||
|
</div>
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</CardTemplate>
|
</CardTemplate>
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ export default function JobDetailCardsDamageComponent({ loading, data }) {
|
|||||||
const { area_of_damage } = data;
|
const { area_of_damage } = data;
|
||||||
return (
|
return (
|
||||||
<CardTemplate loading={loading} title={t("jobs.labels.cards.damage")}>
|
<CardTemplate loading={loading} title={t("jobs.labels.cards.damage")}>
|
||||||
<Car dmg1={area_of_damage.impact1} dmg2={area_of_damage.impact2} />
|
{area_of_damage ? (
|
||||||
|
<Car
|
||||||
|
dmg1={area_of_damage.impact1 || null}
|
||||||
|
dmg2={area_of_damage.impact2 || null}
|
||||||
|
/>
|
||||||
|
) : t("jobs.errors.nodamage")}
|
||||||
</CardTemplate>
|
</CardTemplate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default function JobDetailCardsDocumentsComponent({ loading, data }) {
|
|||||||
<CardTemplate
|
<CardTemplate
|
||||||
loading={loading}
|
loading={loading}
|
||||||
title={t("jobs.labels.cards.documents")}
|
title={t("jobs.labels.cards.documents")}
|
||||||
extraLink={`/manage/jobs/${data.id}#documents`}
|
extraLink={`/manage/jobs/${data.id}?documents`}
|
||||||
>
|
>
|
||||||
{data.documents.length > 0 ? (
|
{data.documents.length > 0 ? (
|
||||||
<Carousel autoplay>
|
<Carousel autoplay>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { List, Icon } from "antd";
|
import { List } from "antd";
|
||||||
|
import { WarningFilled, EyeInvisibleFilled } from "@ant-design/icons";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import CardTemplate from "./job-detail-cards.template.component";
|
import CardTemplate from "./job-detail-cards.template.component";
|
||||||
@@ -16,21 +17,20 @@ export default function JobDetailCardsNotesComponent({ loading, data }) {
|
|||||||
<CardTemplate
|
<CardTemplate
|
||||||
loading={loading}
|
loading={loading}
|
||||||
title={t("jobs.labels.cards.notes")}
|
title={t("jobs.labels.cards.notes")}
|
||||||
extraLink={`/manage/jobs/${data.id}#notes`}>
|
extraLink={`/manage/jobs/${data.id}#notes`}
|
||||||
|
>
|
||||||
{data ? (
|
{data ? (
|
||||||
<Container>
|
<Container>
|
||||||
<List
|
<List
|
||||||
size='small'
|
size="small"
|
||||||
bordered
|
bordered
|
||||||
dataSource={data.notes}
|
dataSource={data.notes}
|
||||||
renderItem={item => (
|
renderItem={item => (
|
||||||
<List.Item>
|
<List.Item>
|
||||||
{item.critical ? (
|
{item.critical ? (
|
||||||
<Icon style={{ margin: 4, color: "red" }} type='warning' />
|
<EyeInvisibleFilled style={{ margin: 4, color: "red" }} />
|
||||||
) : null}
|
|
||||||
{item.private ? (
|
|
||||||
<Icon style={{ margin: 4 }} type='eye-invisible' />
|
|
||||||
) : null}
|
) : null}
|
||||||
|
{item.private ? <WarningFilled style={{ margin: 4 }} /> : null}
|
||||||
{item.text}
|
{item.text}
|
||||||
</List.Item>
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,41 +1,61 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import CardTemplate from "./job-detail-cards.template.component";
|
import CardTemplate from "./job-detail-cards.template.component";
|
||||||
import { Pie } from "react-chartjs-2";
|
import { Pie } from "@nivo/pie";
|
||||||
|
|
||||||
export default function JobDetailCardsPartsComponent({ loading, data }) {
|
export default function JobDetailCardsPartsComponent({ loading, data }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const p = {
|
const commonProperties = {
|
||||||
labels: ["Not Ordered", "Ordered", "Received", "Backordered"],
|
width: 225,
|
||||||
datasets: [
|
height: 225,
|
||||||
{
|
margin: { top: 20, right: 30, bottom: 20, left: 30 },
|
||||||
data: [5, 15, 10, 2],
|
animate: true
|
||||||
backgroundColor: [
|
|
||||||
"rgba(255, 99, 132, 0.2)",
|
|
||||||
"rgba(54, 162, 235, 0.2)",
|
|
||||||
"rgba(255, 206, 86, 0.2)",
|
|
||||||
"rgba(75, 192, 192, 0.2)",
|
|
||||||
"rgba(153, 102, 255, 0.2)",
|
|
||||||
"rgba(255, 159, 64, 0.2)"
|
|
||||||
],
|
|
||||||
borderColor: [
|
|
||||||
"rgba(255, 99, 132, 1)",
|
|
||||||
"rgba(54, 162, 235, 1)",
|
|
||||||
"rgba(255, 206, 86, 1)",
|
|
||||||
"rgba(75, 192, 192, 1)",
|
|
||||||
"rgba(153, 102, 255, 1)",
|
|
||||||
"rgba(255, 159, 64, 1)"
|
|
||||||
],
|
|
||||||
borderWidth: 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cdata = [
|
||||||
|
{
|
||||||
|
id: "elixir",
|
||||||
|
label: "elixir",
|
||||||
|
value: 558,
|
||||||
|
color: "hsl(21, 70%, 50%)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "erlang",
|
||||||
|
label: "erlang",
|
||||||
|
value: 443,
|
||||||
|
color: "hsl(91, 70%, 50%)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "css",
|
||||||
|
label: "css",
|
||||||
|
value: 161,
|
||||||
|
color: "hsl(271, 70%, 50%)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "python",
|
||||||
|
label: "python",
|
||||||
|
value: 305,
|
||||||
|
color: "hsl(33, 70%, 50%)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "php",
|
||||||
|
label: "php",
|
||||||
|
value: 360,
|
||||||
|
color: "hsl(296, 70%, 50%)"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<CardTemplate loading={loading} title={t("jobs.labels.cards.parts")}>
|
<CardTemplate loading={loading} title={t("jobs.labels.cards.parts")}>
|
||||||
{data ? <Pie data={p} /> : null}
|
<Pie
|
||||||
|
{...commonProperties}
|
||||||
|
data={cdata}
|
||||||
|
innerRadius={0.5}
|
||||||
|
padAngle={2}
|
||||||
|
cornerRadius={5}
|
||||||
|
enableRadialLabels={false}
|
||||||
|
/>
|
||||||
</CardTemplate>
|
</CardTemplate>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,69 +1,73 @@
|
|||||||
import { Modal, Form, Input, InputNumber } from "antd";
|
import { Form, Input, Modal } from "antd";
|
||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import ResetForm from "../form-items-formatted/reset-form-item.component";
|
import InputCurrency from "../form-items-formatted/currency-form-item.component";
|
||||||
|
|
||||||
export default function JobLinesUpsertModalComponent({
|
export default function JobLinesUpsertModalComponent({
|
||||||
visible,
|
visible,
|
||||||
jobLine,
|
jobLine,
|
||||||
handleOk,
|
|
||||||
handleCancel,
|
handleCancel,
|
||||||
handleSubmit,
|
handleFinish
|
||||||
form
|
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { getFieldDecorator, isFieldsTouched, resetFields } = form;
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.resetFields();
|
||||||
|
}, [visible, form]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Form
|
||||||
title={
|
onFinish={handleFinish}
|
||||||
jobLine && jobLine.id
|
initialValues={jobLine}
|
||||||
? t("joblines.labels.edit")
|
autoComplete="off"
|
||||||
: t("joblines.labels.new")
|
form={form}
|
||||||
}
|
|
||||||
visible={visible}
|
|
||||||
okText={t("general.labels.save")}
|
|
||||||
onOk={handleSubmit}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
>
|
>
|
||||||
{isFieldsTouched() ? <ResetForm resetFields={resetFields} /> : null}
|
<Modal
|
||||||
<Form onSubmit={handleSubmit} autoComplete={"off"}>
|
title={
|
||||||
<Form.Item label={t("joblines.fields.line_desc")}>
|
jobLine && jobLine.id
|
||||||
{getFieldDecorator("line_desc", {
|
? t("joblines.labels.edit")
|
||||||
initialValue: jobLine.line_desc
|
: t("joblines.labels.new")
|
||||||
})(<Input name="line_desc" />)}
|
}
|
||||||
|
visible={visible}
|
||||||
|
okText={t("general.actions.save")}
|
||||||
|
onOk={() => form.submit()}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label={t("joblines.fields.line_desc")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
name="line_desc"
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("joblines.fields.oem_partno")}>
|
<Form.Item label={t("joblines.fields.oem_partno")} name="oem_partno">
|
||||||
{getFieldDecorator("oem_partno", {
|
<Input />
|
||||||
initialValue: jobLine.oem_partno
|
|
||||||
})(<Input name="oem_partno" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("joblines.fields.part_type")}>
|
<Form.Item label={t("joblines.fields.part_type")} name="part_type">
|
||||||
{getFieldDecorator("part_type", {
|
<Input />
|
||||||
initialValue: jobLine.part_type
|
|
||||||
})(<Input name="part_type" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("joblines.fields.mod_lbr_ty")}>
|
<Form.Item label={t("joblines.fields.mod_lbr_ty")} name="mod_lbr_ty">
|
||||||
{getFieldDecorator("mod_lbr_ty", {
|
<Input />
|
||||||
initialValue: jobLine.mod_lbr_ty
|
|
||||||
})(<Input name="mod_lbr_ty" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("joblines.fields.op_code_desc")}>
|
<Form.Item
|
||||||
{getFieldDecorator("op_code_desc", {
|
label={t("joblines.fields.op_code_desc")}
|
||||||
initialValue: jobLine.op_code_desc
|
name="op_code_desc"
|
||||||
})(<Input name="op_code_desc" />)}
|
>
|
||||||
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("joblines.fields.mod_lb_hrs")}>
|
<Form.Item label={t("joblines.fields.mod_lb_hrs")} name="mod_lb_hrs">
|
||||||
{getFieldDecorator("mod_lb_hrs", {
|
<InputCurrency />
|
||||||
initialValue: jobLine.mod_lb_hrs
|
|
||||||
})(<InputNumber name="mod_lb_hrs" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("joblines.fields.act_price")}>
|
<Form.Item label={t("joblines.fields.act_price")} name="act_price">
|
||||||
{getFieldDecorator("act_price", {
|
<InputCurrency />
|
||||||
initialValue: jobLine.act_price
|
|
||||||
})(<InputNumber name="act_price" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Modal>
|
||||||
</Modal>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { Form, notification } from "antd";
|
import { notification } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useMutation } from "react-apollo";
|
import { useMutation } from "@apollo/react-hooks";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {
|
import { INSERT_NEW_JOB_LINE, UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
||||||
INSERT_NEW_JOB_LINE,
|
|
||||||
UPDATE_JOB_LINE
|
|
||||||
} from "../../graphql/jobs-lines.queries";
|
|
||||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
import { selectJobLineEditModal } from "../../redux/modals/modals.selectors";
|
import { selectJobLineEditModal } from "../../redux/modals/modals.selectors";
|
||||||
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
|
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
|
||||||
@@ -21,70 +18,56 @@ const mapDispatchToProps = dispatch => ({
|
|||||||
|
|
||||||
function JobLinesUpsertModalContainer({
|
function JobLinesUpsertModalContainer({
|
||||||
jobLineEditModal,
|
jobLineEditModal,
|
||||||
toggleModalVisible,
|
toggleModalVisible
|
||||||
form
|
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [insertJobLine] = useMutation(INSERT_NEW_JOB_LINE);
|
const [insertJobLine] = useMutation(INSERT_NEW_JOB_LINE);
|
||||||
const [updateJobLine] = useMutation(UPDATE_JOB_LINE);
|
const [updateJobLine] = useMutation(UPDATE_JOB_LINE);
|
||||||
|
|
||||||
const handleSubmit = e => {
|
const handleFinish = values => {
|
||||||
e.preventDefault();
|
if (!jobLineEditModal.context.id) {
|
||||||
|
insertJobLine({
|
||||||
form.validateFieldsAndScroll((err, values) => {
|
variables: {
|
||||||
if (err) {
|
lineInput: [{ jobid: jobLineEditModal.context.jobid, ...values }]
|
||||||
notification["error"]({
|
}
|
||||||
message: t("joblines.errors.validation"),
|
})
|
||||||
description: err.message
|
.then(r => {
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!err) {
|
|
||||||
if (!jobLineEditModal.context.id) {
|
|
||||||
insertJobLine({
|
|
||||||
variables: {
|
|
||||||
lineInput: [{ jobid: jobLineEditModal.context.jobid, ...values }]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(r => {
|
|
||||||
if (jobLineEditModal.actions.refetch)
|
|
||||||
jobLineEditModal.actions.refetch();
|
|
||||||
toggleModalVisible();
|
|
||||||
notification["success"]({
|
|
||||||
message: t("joblines.successes.created")
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
notification["error"]({
|
|
||||||
message: t("joblines.errors.creating", {
|
|
||||||
message: error.message
|
|
||||||
})
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
updateJobLine({
|
|
||||||
variables: {
|
|
||||||
lineId: jobLineEditModal.context.id,
|
|
||||||
line: values
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(r => {
|
|
||||||
notification["success"]({
|
|
||||||
message: t("joblines.successes.updated")
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
notification["success"]({
|
|
||||||
message: t("joblines.errors.updating", {
|
|
||||||
message: error.message
|
|
||||||
})
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (jobLineEditModal.actions.refetch)
|
if (jobLineEditModal.actions.refetch)
|
||||||
jobLineEditModal.actions.refetch();
|
jobLineEditModal.actions.refetch();
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
|
notification["success"]({
|
||||||
|
message: t("joblines.successes.created")
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("joblines.errors.creating", {
|
||||||
|
message: error.message
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateJobLine({
|
||||||
|
variables: {
|
||||||
|
lineId: jobLineEditModal.context.id,
|
||||||
|
line: values
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
});
|
.then(r => {
|
||||||
|
notification["success"]({
|
||||||
|
message: t("joblines.successes.updated")
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
notification["success"]({
|
||||||
|
message: t("joblines.errors.updating", {
|
||||||
|
message: error.message
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (jobLineEditModal.actions.refetch) jobLineEditModal.actions.refetch();
|
||||||
|
toggleModalVisible();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
@@ -95,9 +78,8 @@ function JobLinesUpsertModalContainer({
|
|||||||
<JobLinesUpdsertModal
|
<JobLinesUpdsertModal
|
||||||
visible={jobLineEditModal.visible}
|
visible={jobLineEditModal.visible}
|
||||||
jobLine={jobLineEditModal.context}
|
jobLine={jobLineEditModal.context}
|
||||||
handleSubmit={handleSubmit}
|
handleFinish={handleFinish}
|
||||||
handleCancel={handleCancel}
|
handleCancel={handleCancel}
|
||||||
form={form}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -105,6 +87,4 @@ function JobLinesUpsertModalContainer({
|
|||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(
|
)(JobLinesUpsertModalContainer);
|
||||||
Form.create({ name: "JobsDetailPageContainer" })(JobLinesUpsertModalContainer)
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Button, Icon, Input, notification, Table } from "antd";
|
import { DeleteFilled, PlusCircleFilled, SyncOutlined } from "@ant-design/icons";
|
||||||
|
import { Button, notification, Table } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import OwnerFindModalContainer from "../owner-find-modal/owner-find-modal.container";
|
import OwnerFindModalContainer from "../owner-find-modal/owner-find-modal.container";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
|
||||||
|
|
||||||
export default function JobsAvailableComponent({
|
export default function JobsAvailableComponent({
|
||||||
loading,
|
loading,
|
||||||
@@ -124,7 +125,7 @@ export default function JobsAvailableComponent({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon type="delete" />
|
<DeleteFilled />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -132,7 +133,7 @@ export default function JobsAvailableComponent({
|
|||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon type="plus" />
|
<PlusCircleFilled />
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@@ -169,19 +170,13 @@ export default function JobsAvailableComponent({
|
|||||||
title={() => {
|
title={() => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Input.Search
|
<strong>{t("jobs.labels.availablenew")}</strong>
|
||||||
placeholder="Search...//TODO Implement Search"
|
|
||||||
onSearch={value => {
|
|
||||||
console.log(value);
|
|
||||||
}}
|
|
||||||
enterButton
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon type="sync" />
|
<SyncOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { notification } from "antd";
|
import { notification } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useMutation, useQuery } from "react-apollo";
|
import { useMutation, useQuery } from "@apollo/react-hooks";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { withRouter } from "react-router-dom";
|
import { withRouter } from "react-router-dom";
|
||||||
import { DELETE_ALL_AVAILABLE_NEW_JOBS, QUERY_AVAILABLE_NEW_JOBS } from "../../graphql/available-jobs.queries";
|
import { DELETE_ALL_AVAILABLE_NEW_JOBS, QUERY_AVAILABLE_NEW_JOBS } from "../../graphql/available-jobs.queries";
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Input, Table, Button, Icon, notification } from "antd";
|
import { DeleteFilled, PlusCircleFilled, SyncOutlined } from "@ant-design/icons";
|
||||||
|
import { Button, notification, Table } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
|
||||||
import JobsFindModalContainer from "../jobs-find-modal/jobs-find-modal.container";
|
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
import JobsFindModalContainer from "../jobs-find-modal/jobs-find-modal.container";
|
||||||
|
|
||||||
export default function JobsAvailableSupplementComponent({
|
export default function JobsAvailableSupplementComponent({
|
||||||
loading,
|
loading,
|
||||||
@@ -138,7 +139,7 @@ export default function JobsAvailableSupplementComponent({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon type="delete" />
|
<DeleteFilled />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -146,7 +147,7 @@ export default function JobsAvailableSupplementComponent({
|
|||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon type="plus" />
|
<PlusCircleFilled />
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@@ -172,19 +173,13 @@ export default function JobsAvailableSupplementComponent({
|
|||||||
title={() => {
|
title={() => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Input.Search
|
<strong>{t("jobs.labels.availablesupplements")}</strong>
|
||||||
placeholder="Search..."
|
|
||||||
onSearch={value => {
|
|
||||||
console.log(value);
|
|
||||||
}}
|
|
||||||
enterButton
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon type="sync" />
|
<SyncOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { notification } from "antd";
|
import { notification } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useMutation, useQuery } from "react-apollo";
|
import { useMutation, useQuery } from "@apollo/react-hooks";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { withRouter } from "react-router-dom";
|
import { withRouter } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
@@ -9,8 +9,9 @@ import {
|
|||||||
} from "../../graphql/available-jobs.queries";
|
} from "../../graphql/available-jobs.queries";
|
||||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import JobsAvailableSupplementComponent from "./jobs-available-supplement.component";
|
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
|
import JobsAvailableSupplementComponent from "./jobs-available-supplement.component";
|
||||||
|
import HeaderFields from "./jobs-available-supplement.headerfields";
|
||||||
|
|
||||||
export default withRouter(function JobsAvailableSupplementContainer({
|
export default withRouter(function JobsAvailableSupplementContainer({
|
||||||
deleteJob,
|
deleteJob,
|
||||||
@@ -53,14 +54,14 @@ export default withRouter(function JobsAvailableSupplementContainer({
|
|||||||
} else {
|
} else {
|
||||||
//create upsert job
|
//create upsert job
|
||||||
let supp = estData.data.available_jobs_by_pk.est_data;
|
let supp = estData.data.available_jobs_by_pk.est_data;
|
||||||
|
console.log("supp before", supp);
|
||||||
delete supp.joblines;
|
delete supp.joblines;
|
||||||
//TODO How to update the estimate lines.
|
//TODO How to update the estimate lines.
|
||||||
delete supp.owner;
|
delete supp.owner;
|
||||||
delete supp.vehicle;
|
delete supp.vehicle;
|
||||||
|
|
||||||
if (!importOptions.overrideHeaders) {
|
if (importOptions.overrideHeaders) {
|
||||||
delete supp["ins_ea"];
|
HeaderFields.forEach(item => delete supp[item]);
|
||||||
//TODO Remove all required fields.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateJob({
|
updateJob({
|
||||||
@@ -102,11 +103,12 @@ export default withRouter(function JobsAvailableSupplementContainer({
|
|||||||
setSelectedJob(null);
|
setSelectedJob(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (error) return <AlertComponent type='error' message={error.message} />;
|
if (error) return <AlertComponent type="error" message={error.message} />;
|
||||||
return (
|
return (
|
||||||
<LoadingSpinner
|
<LoadingSpinner
|
||||||
loading={insertLoading}
|
loading={insertLoading}
|
||||||
message={t("jobs.labels.creating_new_job")}>
|
message={t("jobs.labels.creating_new_job")}
|
||||||
|
>
|
||||||
<JobsAvailableSupplementComponent
|
<JobsAvailableSupplementComponent
|
||||||
loading={loading}
|
loading={loading}
|
||||||
data={data}
|
data={data}
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
const headerFields = [
|
||||||
|
//AD1
|
||||||
|
"ins_co_id",
|
||||||
|
"ins_co_nm",
|
||||||
|
"ins_addr1",
|
||||||
|
"ins_addr2",
|
||||||
|
"ins_city",
|
||||||
|
"ins_st",
|
||||||
|
"ins_zip",
|
||||||
|
"ins_ctry",
|
||||||
|
"ins_ea",
|
||||||
|
"policy_no",
|
||||||
|
"ded_amt",
|
||||||
|
"ded_status",
|
||||||
|
"asgn_no",
|
||||||
|
"asgn_date",
|
||||||
|
"asgn_type",
|
||||||
|
"clm_no",
|
||||||
|
"clm_ofc_id",
|
||||||
|
"clm_ofc_nm",
|
||||||
|
"clm_addr1",
|
||||||
|
"clm_addr2",
|
||||||
|
"clm_city",
|
||||||
|
"clm_st",
|
||||||
|
"clm_zip",
|
||||||
|
"clm_ctry",
|
||||||
|
"clm_ph1",
|
||||||
|
"clm_ph1x",
|
||||||
|
"clm_ph2",
|
||||||
|
"clm_ph2x",
|
||||||
|
"clm_fax",
|
||||||
|
"clm_faxx",
|
||||||
|
"clm_ct_ln",
|
||||||
|
"clm_ct_fn",
|
||||||
|
"clm_title",
|
||||||
|
"clm_ct_ph",
|
||||||
|
"clm_ct_phx",
|
||||||
|
"clm_ea",
|
||||||
|
"payee_nms",
|
||||||
|
"pay_type",
|
||||||
|
"pay_date",
|
||||||
|
"pay_chknm",
|
||||||
|
"pay_amt",
|
||||||
|
"agt_co_id",
|
||||||
|
"agt_co_nm",
|
||||||
|
"agt_addr1",
|
||||||
|
"agt_addr2",
|
||||||
|
"agt_city",
|
||||||
|
"agt_st",
|
||||||
|
"agt_zip",
|
||||||
|
"agt_ctry",
|
||||||
|
"agt_ph1",
|
||||||
|
"agt_ph1x",
|
||||||
|
"agt_ph2",
|
||||||
|
"agt_ph2x",
|
||||||
|
"agt_fax",
|
||||||
|
"agt_faxx",
|
||||||
|
"agt_ct_ln",
|
||||||
|
"agt_ct_fn",
|
||||||
|
"agt_ct_ph",
|
||||||
|
"agt_ct_phx",
|
||||||
|
"agt_ea",
|
||||||
|
"agt_lic_no",
|
||||||
|
"loss_date",
|
||||||
|
"loss_type",
|
||||||
|
"loss_desc",
|
||||||
|
"theft_ind",
|
||||||
|
"cat_no",
|
||||||
|
"tlos_ind",
|
||||||
|
"cust_pr",
|
||||||
|
"insd_ln",
|
||||||
|
"insd_fn",
|
||||||
|
"insd_title",
|
||||||
|
"insd_co_nm",
|
||||||
|
"insd_addr1",
|
||||||
|
"insd_addr2",
|
||||||
|
"insd_city",
|
||||||
|
"insd_st",
|
||||||
|
"insd_zip",
|
||||||
|
"insd_ctry",
|
||||||
|
"insd_ph1",
|
||||||
|
"insd_ph1x",
|
||||||
|
"insd_ph2",
|
||||||
|
"insd_ph2x",
|
||||||
|
"insd_fax",
|
||||||
|
"insd_faxx",
|
||||||
|
"insd_ea",
|
||||||
|
"ownr_ln",
|
||||||
|
"ownr_fn",
|
||||||
|
"ownr_title",
|
||||||
|
"ownr_co_nm",
|
||||||
|
"ownr_addr1",
|
||||||
|
"ownr_addr2",
|
||||||
|
"ownr_city",
|
||||||
|
"ownr_st",
|
||||||
|
"ownr_zip",
|
||||||
|
"ownr_ctry",
|
||||||
|
"ownr_ph1",
|
||||||
|
"ownr_ph1x",
|
||||||
|
"ownr_ph2",
|
||||||
|
"ownr_ph2x",
|
||||||
|
"ownr_fax",
|
||||||
|
"ownr_faxx",
|
||||||
|
"ownr_ea",
|
||||||
|
"ins_ph1",
|
||||||
|
"ins_ph1x",
|
||||||
|
"ins_ph2",
|
||||||
|
"ins_ph2x",
|
||||||
|
"ins_fax",
|
||||||
|
"ins_faxx",
|
||||||
|
"ins_ct_ln",
|
||||||
|
"ins_ct_fn",
|
||||||
|
"ins_title",
|
||||||
|
"ins_ct_ph",
|
||||||
|
"ins_ct_phx",
|
||||||
|
"loss_cat",
|
||||||
|
//ad2
|
||||||
|
"clmt_ln",
|
||||||
|
"clmt_fn",
|
||||||
|
"clmt_title",
|
||||||
|
"clmt_co_nm",
|
||||||
|
"clmt_addr1",
|
||||||
|
"clmt_addr2",
|
||||||
|
"clmt_city",
|
||||||
|
"clmt_st",
|
||||||
|
"clmt_zip",
|
||||||
|
"clmt_ctry",
|
||||||
|
"clmt_ph1",
|
||||||
|
"clmt_ph1x",
|
||||||
|
"clmt_ph2",
|
||||||
|
"clmt_ph2x",
|
||||||
|
"clmt_fax",
|
||||||
|
"clmt_faxx",
|
||||||
|
"clmt_ea",
|
||||||
|
"est_co_id",
|
||||||
|
"est_co_nm",
|
||||||
|
"est_addr1",
|
||||||
|
"est_addr2",
|
||||||
|
"est_city",
|
||||||
|
"est_st",
|
||||||
|
"est_zip",
|
||||||
|
"est_ctry",
|
||||||
|
"est_ph1",
|
||||||
|
"est_ph1x",
|
||||||
|
"est_ph2",
|
||||||
|
"est_ph2x",
|
||||||
|
"est_fax",
|
||||||
|
"est_faxx",
|
||||||
|
"est_ct_ln",
|
||||||
|
"est_ct_fn",
|
||||||
|
"est_ea",
|
||||||
|
"est_lic_no",
|
||||||
|
"est_fileno",
|
||||||
|
"insp_ct_ln",
|
||||||
|
"insp_ct_fn",
|
||||||
|
"insp_addr1",
|
||||||
|
"insp_addr2",
|
||||||
|
"insp_city",
|
||||||
|
"insp_st",
|
||||||
|
"insp_zip",
|
||||||
|
"insp_ctry",
|
||||||
|
"insp_ph1",
|
||||||
|
"insp_ph1x",
|
||||||
|
"insp_ph2",
|
||||||
|
"insp_ph2x",
|
||||||
|
"insp_fax",
|
||||||
|
"insp_faxx",
|
||||||
|
"insp_ea",
|
||||||
|
"insp_code",
|
||||||
|
"insp_desc",
|
||||||
|
"insp_date",
|
||||||
|
"insp_time",
|
||||||
|
"rf_co_id",
|
||||||
|
"rf_co_nm",
|
||||||
|
"rf_addr1",
|
||||||
|
"rf_addr2",
|
||||||
|
"rf_city",
|
||||||
|
"rf_st",
|
||||||
|
"rf_zip",
|
||||||
|
"rf_ctry",
|
||||||
|
"rf_ph1",
|
||||||
|
"rf_ph1x",
|
||||||
|
"rf_ph2",
|
||||||
|
"rf_ph2x",
|
||||||
|
"rf_fax",
|
||||||
|
"rf_faxx",
|
||||||
|
"rf_ct_ln",
|
||||||
|
"rf_ct_fn",
|
||||||
|
"rf_ea",
|
||||||
|
"rf_tax_id",
|
||||||
|
"rf_lic_no",
|
||||||
|
"rf_bar_no",
|
||||||
|
"ro_in_date",
|
||||||
|
"ro_in_time",
|
||||||
|
"tar_date",
|
||||||
|
"tar_time",
|
||||||
|
"ro_cmpdate",
|
||||||
|
"ro_cmptime",
|
||||||
|
"date_out",
|
||||||
|
"time_out",
|
||||||
|
"rf_estimtr",
|
||||||
|
"mktg_type",
|
||||||
|
"mktg_src",
|
||||||
|
"loc_nm",
|
||||||
|
"loc_addr1",
|
||||||
|
"loc_addr2",
|
||||||
|
"loc_city",
|
||||||
|
"loc_st",
|
||||||
|
"loc_zip",
|
||||||
|
"loc_ctry",
|
||||||
|
"loc_ph1",
|
||||||
|
"loc_ph1x",
|
||||||
|
"loc_ph2",
|
||||||
|
"loc_ph2x",
|
||||||
|
"loc_fax",
|
||||||
|
"loc_faxx",
|
||||||
|
"loc_ct_ln",
|
||||||
|
"loc_ct_fn",
|
||||||
|
"loc_title",
|
||||||
|
"loc_ph",
|
||||||
|
"loc_phx",
|
||||||
|
"loc_ea"
|
||||||
|
];
|
||||||
|
|
||||||
|
export default headerFields;
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
import { Collapse, Form, Input, InputNumber, Switch, DatePicker } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import FormItemEmail from "../form-items-formatted/email-form-item.component";
|
||||||
|
import FormItemPhone from "../form-items-formatted/phone-form-item.component";
|
||||||
|
|
||||||
|
export default function JobsCreateJobsInfo({ form }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { getFieldValue } = form;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Collapse defaultActiveKey="insurance">
|
||||||
|
<Collapse.Panel
|
||||||
|
key="insurance"
|
||||||
|
header={t("menus.jobsdetail.insurance")}
|
||||||
|
>
|
||||||
|
<Form.Item label={t("jobs.fields.ins_co_id")} name="ins_co_id">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.policy_no")} name="policy_no">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.clm_no")} name="clm_no">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.regie_number")} name="regie_number">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
TODO: missing KOL field???
|
||||||
|
<Form.Item label={t("jobs.fields.loss_date")} name="loss_date">
|
||||||
|
<DatePicker />
|
||||||
|
</Form.Item>
|
||||||
|
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")} name="ins_co_nm">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.ins_addr1")} name="ins_addr1">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.ins_city")} name="ins_city">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.ins_ct_ln")} name="ins_ct_ln">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.ins_ct_fn")} name="ins_ct_fn">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.ins_ph1")} name="ins_ph1">
|
||||||
|
<FormItemPhone customInput={Input} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.ins_ea")}
|
||||||
|
name="ins_ea"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
type: "email",
|
||||||
|
message: "This is not a valid email address."
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<FormItemEmail email={getFieldValue("ins_ea")} />
|
||||||
|
</Form.Item>
|
||||||
|
Appraiser Info
|
||||||
|
<Form.Item label={t("jobs.fields.est_co_nm")} name="est_co_nm">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.est_ct_fn")} name="est_ct_fn">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.est_ct_ln")} name="est_ct_ln">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
TODO: Field is pay date but title is inspection date. Likely
|
||||||
|
incorrect?
|
||||||
|
<Form.Item label={t("jobs.fields.pay_date")} name="pay_date">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.est_ph1")} name="est_ph1">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.est_ea")}
|
||||||
|
name="est_ea"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
type: "email",
|
||||||
|
message: "This is not a valid email address."
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<FormItemEmail email={getFieldValue("est_ea")} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.selling_dealer")}
|
||||||
|
name="selling_dealer"
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.servicing_dealer")}
|
||||||
|
name="servicing_dealer"
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.selling_dealer_contact")}
|
||||||
|
name="selling_dealer_contact"
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.servicing_dealer_contact")}
|
||||||
|
name="servicing_dealer_contact"
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Collapse.Panel>
|
||||||
|
<Collapse.Panel key="claim" header={t("menus.jobsdetail.claimdetail")}>
|
||||||
|
<Form.Item label={t("jobs.fields.csr")} name="csr">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.loss_desc")} name="loss_desc">
|
||||||
|
<Input />
|
||||||
|
</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")} name="po_number">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.unitnumber")} name="unit_number">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.specialcoveragepolicy")}
|
||||||
|
valuePropName="checked"
|
||||||
|
name="special_coverage_policy"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.kmin")} name="kmin">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.kmout")} name="kmout">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.referralsource")}
|
||||||
|
name="referral_source"
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Collapse.Panel>
|
||||||
|
<Collapse.Panel
|
||||||
|
key="financial"
|
||||||
|
header={t("menus.jobsdetail.financials")}
|
||||||
|
>
|
||||||
|
<Form.Item label={t("jobs.fields.ded_amt")} name="ded_amt">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.ded_status")} name="ded_status">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.depreciation_taxes")}
|
||||||
|
name="depreciation_taxes"
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
TODO This is equivalent of GST payable.
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.federal_tax_payable")}
|
||||||
|
name="federal_tax_payable"
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
TODO equivalent of other customer amount
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.other_amount_payable")}
|
||||||
|
name="other_amount_payable"
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.towing_payable")}
|
||||||
|
name="towing_payable"
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.storage_payable")}
|
||||||
|
name="storage_payable"
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.adjustment_bottom_line")}
|
||||||
|
name="adjustment_bottom_line"
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
Totals Table
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.labor_rate_desc")}
|
||||||
|
name="labor_rate_desc"
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.rate_lab")} name="rate_lab">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.rate_lad")} name="rate_lad">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.rate_lae")} name="rate_lae">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.rate_lar")} name="rate_lar">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.rate_las")} name="rate_las">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.rate_laf")} name="rate_laf">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.rate_lam")} name="rate_lam">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.rate_lag")} name="rate_lag">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
Note //TODO Remove ATP rate?
|
||||||
|
<Form.Item label={t("jobs.fields.rate_atp")} name="rate_atp">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.rate_lau")} name="rate_lau">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.rate_la1")} name="rate_la1">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.rate_la2")} name="rate_la2">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.rate_la3")} name="rate_la3">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.rate_la4")} name="rate_la4">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.rate_mapa")} name="rate_mapa">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.rate_mash")} name="rate_mash">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.rate_mahw")} name="rate_mahw">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.rate_ma2s")} name="rate_ma2s">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.rate_ma3s")} name="rate_ma3s">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.rate_mabl")} name="rate_mabl">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.rate_macs")} name="rate_macs">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.rate_matd")} name="rate_matd">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.rate_laa")} name="rate_laa">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
</Collapse.Panel>
|
||||||
|
</Collapse>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Row, Col, Typography } from "antd";
|
||||||
|
import JobsCreateOwnerInfoNewComponent from "./jobs-create-owner-info.new.component";
|
||||||
|
import JobsCreateOwnerInfoSearchComponent from "./jobs-create-owner-info.search.component";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export default function JobsCreateOwnerInfoComponent({ loading, owners }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Row>
|
||||||
|
<Typography.Title>{t("jobs.labels.create.ownerinfo")}</Typography.Title>
|
||||||
|
</Row>
|
||||||
|
<Row gutter={4}>
|
||||||
|
<Col span={16}>
|
||||||
|
<JobsCreateOwnerInfoSearchComponent
|
||||||
|
loading={loading}
|
||||||
|
owners={owners}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col span={8}>
|
||||||
|
<JobsCreateOwnerInfoNewComponent />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import React, { useContext } from "react";
|
||||||
|
import JobsCreateOwnerInfoComponent from "./jobs-create-owner-info.component";
|
||||||
|
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
||||||
|
import { QUERY_SEARCH_OWNER_BY_IDX } from "../../graphql/owners.queries";
|
||||||
|
import { useQuery } from "@apollo/react-hooks";
|
||||||
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
|
||||||
|
export default function JobsCreateOwnerContainer() {
|
||||||
|
const [state] = useContext(JobCreateContext);
|
||||||
|
const { loading, error, data } = useQuery(QUERY_SEARCH_OWNER_BY_IDX, {
|
||||||
|
variables: { search: `%${state.owner.search}%` },
|
||||||
|
skip: !state.owner.search
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
return (
|
||||||
|
<JobsCreateOwnerInfoComponent
|
||||||
|
loading={loading}
|
||||||
|
owners={data ? data.search_owner : null}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { Form, Input, Checkbox, Switch } from "antd";
|
||||||
|
import React, { useContext } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
||||||
|
import FormItemEmail from "../form-items-formatted/email-form-item.component";
|
||||||
|
import FormItemPhone from "../form-items-formatted/phone-form-item.component";
|
||||||
|
|
||||||
|
export default function JobsCreateOwnerInfoNewComponent() {
|
||||||
|
const [state, setState] = useContext(JobCreateContext);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Checkbox
|
||||||
|
defaultChecked={state.owner.new}
|
||||||
|
checked={state.owner.new}
|
||||||
|
onChange={() => {
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
owner: {
|
||||||
|
...state.owner,
|
||||||
|
new: !state.owner.new,
|
||||||
|
selectedid: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("jobs.labels.create.newowner")}
|
||||||
|
</Checkbox>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("owners.fields.ownr_ln")}
|
||||||
|
name={["owner", "data", "ownr_ln"]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.owner.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("owners.fields.ownr_fn")}
|
||||||
|
name={["owner", "data", "ownr_fn"]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.owner.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("owners.fields.allow_text_message")}
|
||||||
|
valuePropName="checked"
|
||||||
|
name={["owner", "data", "allow_text_message"]}
|
||||||
|
>
|
||||||
|
<Switch disabled={!state.owner.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("owners.fields.ownr_addr1")}
|
||||||
|
name={["owner", "data", "ownr_addr1"]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.owner.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("owners.fields.ownr_addr2")}
|
||||||
|
name={["owner", "data", "ownr_addr2"]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.owner.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("owners.fields.ownr_city")}
|
||||||
|
name={["owner", "data", "ownr_city"]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.owner.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("owners.fields.ownr_ctry")}
|
||||||
|
name={["owner", "data", "ownr_ctry"]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.owner.new} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("owners.fields.ownr_ea")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
type: "email",
|
||||||
|
message: "This is not a valid email address."
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
name={["owner", "data", "ownr_ea"]}
|
||||||
|
>
|
||||||
|
<FormItemEmail
|
||||||
|
//TODO Fix this email={getFieldValue("ownr_ea")}
|
||||||
|
disabled={!state.owner.new}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("owners.fields.ownr_ph1")}
|
||||||
|
name={["owner", "data", "ownr_ph1"]}
|
||||||
|
>
|
||||||
|
<FormItemPhone customInput={Input} disabled={!state.owner.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("owners.fields.ownr_st")}
|
||||||
|
name={["owner", "data", "ownr_st"]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.owner.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("owners.fields.ownr_zip")}
|
||||||
|
name={["owner", "data", "ownr_zip"]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.owner.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("owners.fields.preferred_contact")}
|
||||||
|
name={["owner", "data", "preferred_contact"]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.owner.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("owners.fields.ownr_title")}
|
||||||
|
name={["owner", "data", "ownr_title"]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.owner.new} />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { Input, Table } from "antd";
|
||||||
|
import React, { useContext, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
||||||
|
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
|
||||||
|
export default function JobsCreateOwnerInfoSearchComponent({
|
||||||
|
loading,
|
||||||
|
owners
|
||||||
|
}) {
|
||||||
|
const [state, setState] = useContext(JobCreateContext);
|
||||||
|
const [tableState, setTableState] = useState({
|
||||||
|
sortedInfo: {},
|
||||||
|
filteredInfo: { text: "" }
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("owners.fields.ownr_ln"),
|
||||||
|
dataIndex: "ownr_ln",
|
||||||
|
key: "ownr_ln",
|
||||||
|
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||||
|
sortOrder:
|
||||||
|
tableState.sortedInfo.columnKey === "ownr_ln" &&
|
||||||
|
tableState.sortedInfo.order
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("owners.fields.ownr_fn"),
|
||||||
|
dataIndex: "ownr_fn",
|
||||||
|
key: "ownr_fn",
|
||||||
|
sorter: (a, b) => alphaSort(a.ownr_fn, b.ownr_fn),
|
||||||
|
sortOrder:
|
||||||
|
tableState.sortedInfo.columnKey === "ownr_fn" &&
|
||||||
|
tableState.sortedInfo.order
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("owners.fields.ownr_addr1"),
|
||||||
|
dataIndex: "ownr_addr1",
|
||||||
|
key: "ownr_addr1",
|
||||||
|
sorter: (a, b) => alphaSort(a.ownr_addr1, b.ownr_addr1),
|
||||||
|
sortOrder:
|
||||||
|
tableState.sortedInfo.columnKey === "ownr_addr1" &&
|
||||||
|
tableState.sortedInfo.order
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("owners.fields.ownr_city"),
|
||||||
|
dataIndex: "ownr_city",
|
||||||
|
key: "ownr_city",
|
||||||
|
sorter: (a, b) => alphaSort(a.ownr_city, b.ownr_city),
|
||||||
|
sortOrder:
|
||||||
|
tableState.sortedInfo.columnKey === "ownr_city" &&
|
||||||
|
tableState.sortedInfo.order
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("owners.fields.ownr_ea"),
|
||||||
|
dataIndex: "ownr_ea",
|
||||||
|
key: "ownr_ea",
|
||||||
|
sorter: (a, b) => alphaSort(a.ownr_ea, b.ownr_ea),
|
||||||
|
sortOrder:
|
||||||
|
tableState.sortedInfo.columnKey === "ownr_ea" &&
|
||||||
|
tableState.sortedInfo.order
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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.ownr_ph1, b.ownr_ph1),
|
||||||
|
sortOrder:
|
||||||
|
tableState.sortedInfo.columnKey === "ownr_ph1" &&
|
||||||
|
tableState.sortedInfo.order
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setTableState({ ...tableState, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
//TODO Implement searching & pagination
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
loading={loading}
|
||||||
|
title={() => {
|
||||||
|
return (
|
||||||
|
<Input.Search
|
||||||
|
placeholder="Search..."
|
||||||
|
onSearch={value => {
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
owner: { ...state.owner, search: value }
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
enterButton
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
pagination={{ position: "top" }}
|
||||||
|
columns={columns.map(item => ({ ...item }))}
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={owners}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
rowSelection={{
|
||||||
|
onSelect: props => {
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
owner: { ...state.owner, new: false, selectedid: props.id }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
type: "radio",
|
||||||
|
selectedRowKeys: [state.owner.selectedid]
|
||||||
|
}}
|
||||||
|
onRow={(record, rowIndex) => {
|
||||||
|
return {
|
||||||
|
onClick: event => {
|
||||||
|
if (record) {
|
||||||
|
if (record.id) {
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
owner: {
|
||||||
|
...state.owner,
|
||||||
|
new: false,
|
||||||
|
selectedid: record.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
owner: { ...state.owner, selectedid: null }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Col, Row, Typography } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import JobsCreateVehicleInfoNewComponent from "./jobs-create-vehicle-info.new.component";
|
||||||
|
import JobsCreateVehicleInfoSearchComponent from "./jobs-create-vehicle-info.search.component";
|
||||||
|
|
||||||
|
export default function JobsCreateVehicleInfoComponent({ loading, vehicles }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Typography.Title>{t("jobs.labels.create.vehicleinfo")}</Typography.Title>
|
||||||
|
<Row>
|
||||||
|
<Col span={16}>
|
||||||
|
<JobsCreateVehicleInfoSearchComponent
|
||||||
|
loading={loading}
|
||||||
|
vehicles={vehicles}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<JobsCreateVehicleInfoNewComponent />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import React, { useContext } from "react";
|
||||||
|
import JobsCreateVehicleInfoComponent from "./jobs-create-vehicle-info.component";
|
||||||
|
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
||||||
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
import { SEARCH_VEHICLE_BY_VIN } from "../../graphql/vehicles.queries";
|
||||||
|
import { useQuery } from "@apollo/react-hooks";
|
||||||
|
|
||||||
|
export default function JobsCreateVehicleInfoContainer({ form }) {
|
||||||
|
const [state] = useContext(JobCreateContext);
|
||||||
|
const { loading, error, data } = useQuery(SEARCH_VEHICLE_BY_VIN, {
|
||||||
|
variables: { vin: `%${state.vehicle.search}%` },
|
||||||
|
skip: !state.vehicle.search
|
||||||
|
});
|
||||||
|
if (error) return <AlertComponent message={error.message} type='error' />;
|
||||||
|
return (
|
||||||
|
<JobsCreateVehicleInfoComponent
|
||||||
|
loading={loading}
|
||||||
|
vehicles={data ? data.vehicles : null}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
import { DatePicker, Form, Input, Checkbox } from "antd";
|
||||||
|
import React, { useContext } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
||||||
|
|
||||||
|
export default function JobsCreateVehicleInfoNewComponent() {
|
||||||
|
const [state, setState] = useContext(JobCreateContext);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Checkbox
|
||||||
|
defaultChecked={state.vehicle.new}
|
||||||
|
checked={state.vehicle.new}
|
||||||
|
onChange={() => {
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
vehicle: {
|
||||||
|
...state.vehicle,
|
||||||
|
new: !state.vehicle.new,
|
||||||
|
selectedid: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("jobs.labels.create.newvehicle")}
|
||||||
|
</Checkbox>
|
||||||
|
<Form.Item
|
||||||
|
label={t("vehicles.fields.v_vin")}
|
||||||
|
name={["vehicle", "data", "v_vin"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: state.vehicle.new,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.vehicle.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("vehicles.fields.plate_no")}
|
||||||
|
name={["vehicle", "data", "plate_no"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: state.vehicle.new,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.vehicle.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("vehicles.fields.plate_st")}
|
||||||
|
name={["vehicle", "data", "plate_st"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: state.vehicle.new,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.vehicle.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("vehicles.fields.v_type")}
|
||||||
|
name={["vehicle", "data", "v_type"]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.vehicle.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("vehicles.fields.v_trimcode")}
|
||||||
|
name={["vehicle", "data", "v_trimcode"]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.vehicle.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("vehicles.fields.v_tone")}
|
||||||
|
name={["vehicle", "data", "v_tone"]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.vehicle.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("vehicles.fields.v_bstyle")}
|
||||||
|
name={["vehicle", "data", "v_bstyle"]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.vehicle.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("vehicles.fields.v_stage")}
|
||||||
|
name={["vehicle", "data", "v_stage"]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.vehicle.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("vehicles.fields.v_prod_dt")}
|
||||||
|
name={["vehicle", "data", "v_prod_dt"]}
|
||||||
|
>
|
||||||
|
<DatePicker disabled={!state.vehicle.new} />
|
||||||
|
</Form.Item>
|
||||||
|
{
|
||||||
|
//TODO Add handling for paint code json
|
||||||
|
}
|
||||||
|
<Form.Item
|
||||||
|
label={t("vehicles.fields.v_paint_codes")}
|
||||||
|
name={["vehicle", "data", "v_paint_codes"]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.vehicle.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("vehicles.fields.v_options")}
|
||||||
|
name={["vehicle", "data", "v_options"]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.vehicle.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("vehicles.fields.v_model_yr")}
|
||||||
|
name={["vehicle", "data", "v_model_yr"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: state.vehicle.new,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.vehicle.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("vehicles.fields.v_model_desc")}
|
||||||
|
name={["vehicle", "data", "v_model_desc"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: state.vehicle.new,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.vehicle.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("vehicles.fields.trim_color")}
|
||||||
|
name={["vehicle", "data", "trim_color"]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.vehicle.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("vehicles.fields.v_mldgcode")}
|
||||||
|
name={["vehicle", "data", "v_mldgcode"]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.vehicle.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("vehicles.fields.v_makecode")}
|
||||||
|
name={["vehicle", "data", "v_makecode"]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.vehicle.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("vehicles.fields.v_make_desc")}
|
||||||
|
name={["vehicle", "data", "v_make_desc"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: state.vehicle.new,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.vehicle.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("vehicles.fields.v_engine")}
|
||||||
|
name={["vehicle", "data", "v_engine"]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.vehicle.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("vehicles.fields.v_cond")}
|
||||||
|
name={["vehicle", "data", "v_cond"]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.vehicle.new} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("vehicles.fields.v_color")}
|
||||||
|
name={["vehicle", "data", "v_color"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: state.vehicle.new,
|
||||||
|
message: t("general.validation.required")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input disabled={!state.vehicle.new} />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import React, { useContext, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Table, Input } from "antd";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
||||||
|
|
||||||
|
export default function JobsCreateVehicleInfoSearchComponent({
|
||||||
|
loading,
|
||||||
|
vehicles
|
||||||
|
}) {
|
||||||
|
const [state, setState] = useContext(JobCreateContext);
|
||||||
|
const [tableState, setTableState] = useState({
|
||||||
|
sortedInfo: {},
|
||||||
|
filteredInfo: { text: "" }
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("vehicles.fields.v_vin"),
|
||||||
|
dataIndex: "v_vin",
|
||||||
|
key: "v_vin",
|
||||||
|
sorter: (a, b) => alphaSort(a.v_vin, b.v_vin),
|
||||||
|
sortOrder:
|
||||||
|
tableState.sortedInfo.columnKey === "v_vin" &&
|
||||||
|
tableState.sortedInfo.order,
|
||||||
|
render: (text, record) => (
|
||||||
|
<Link to={"/manage/vehicles/" + record.id}>{record.v_vin}</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("vehicles.fields.description"),
|
||||||
|
dataIndex: "description",
|
||||||
|
key: "description",
|
||||||
|
render: (text, record) => {
|
||||||
|
return (
|
||||||
|
<span>{`${record.v_model_yr} ${record.v_make_desc} ${record.v_model_desc} ${record.v_color}`}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("vehicles.fields.plate_no"),
|
||||||
|
dataIndex: "plate",
|
||||||
|
key: "plate",
|
||||||
|
render: (text, record) => {
|
||||||
|
return <span>{`${record.plate_st} | ${record.plate_no}`}</span>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setTableState({ ...tableState, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
//TODO Implement searching & pagination
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
loading={loading}
|
||||||
|
title={() => {
|
||||||
|
return (
|
||||||
|
<Input.Search
|
||||||
|
placeholder="Search..."
|
||||||
|
onSearch={value => {
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
vehicle: { ...state.vehicle, search: value }
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
enterButton
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
pagination={{ position: "top" }}
|
||||||
|
columns={columns.map(item => ({ ...item }))}
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={vehicles}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
rowSelection={{
|
||||||
|
onSelect: props => {
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
vehicle: {
|
||||||
|
...state.vehicle,
|
||||||
|
new: false,
|
||||||
|
selectedid: props.id,
|
||||||
|
vehicleObj: props
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
type: "radio",
|
||||||
|
selectedRowKeys: [state.vehicle.selectedid]
|
||||||
|
}}
|
||||||
|
onRow={(record, rowIndex) => {
|
||||||
|
return {
|
||||||
|
onClick: event => {
|
||||||
|
if (record) {
|
||||||
|
if (record.id) {
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
vehicle: {
|
||||||
|
...state.vehicle,
|
||||||
|
new: false,
|
||||||
|
selectedid: record.id,
|
||||||
|
vehicleObj: record
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
vehicle: { ...state.vehicle, selectedid: null, vehicleObj: null }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,24 +1,17 @@
|
|||||||
import { Form, Input, Switch } from "antd";
|
import { Form, Input, Switch } from "antd";
|
||||||
import React, { useContext } from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import JobDetailFormContext from "../../pages/jobs-detail/jobs-detail.page.context";
|
|
||||||
|
|
||||||
export default function JobsDetailClaims({ job }) {
|
export default function JobsDetailClaims({ job }) {
|
||||||
const form = useContext(JobDetailFormContext);
|
|
||||||
const { getFieldDecorator } = form;
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Form.Item label={t("jobs.fields.csr")}>
|
<Form.Item label={t("jobs.fields.csr")} name="csr">
|
||||||
{getFieldDecorator("csr", {
|
<Input />
|
||||||
initialValue: job.csr
|
|
||||||
})(<Input name='csr' />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.loss_desc")}>
|
<Form.Item label={t("jobs.fields.loss_desc")} name="loss_desc">
|
||||||
{getFieldDecorator("loss_desc", {
|
<Input />
|
||||||
initialValue: job.loss_desc
|
|
||||||
})(<Input name='loss_desc' />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
TODO How to handle different taxes and marking them as exempt?
|
TODO How to handle different taxes and marking them as exempt?
|
||||||
{
|
{
|
||||||
@@ -28,36 +21,27 @@ export default function JobsDetailClaims({ job }) {
|
|||||||
// })(<Input name='exempt' />)}
|
// })(<Input name='exempt' />)}
|
||||||
// </Form.Item>
|
// </Form.Item>
|
||||||
}
|
}
|
||||||
<Form.Item label={t("jobs.fields.ponumber")}>
|
<Form.Item label={t("jobs.fields.ponumber")} name="po_number">
|
||||||
{getFieldDecorator("po_number", {
|
<Input />
|
||||||
initialValue: job.po_number
|
|
||||||
})(<Input name='po_number' />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.unitnumber")}>
|
<Form.Item label={t("jobs.fields.unitnumber")} name="unit_number">
|
||||||
{getFieldDecorator("unit_number", {
|
<Input />
|
||||||
initialValue: job.unit_number
|
|
||||||
})(<Input name='unit_number' />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.specialcoveragepolicy")}>
|
<Form.Item
|
||||||
{getFieldDecorator("special_coverage_policy", {
|
label={t("jobs.fields.specialcoveragepolicy")}
|
||||||
initialValue: job.special_coverage_policy,
|
valuePropName="checked"
|
||||||
valuePropName: "checked"
|
name="special_coverage_policy"
|
||||||
})(<Switch name='special_coverage_policy' />)}
|
>
|
||||||
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.kmin")}>
|
<Form.Item label={t("jobs.fields.kmin")} name="kmin">
|
||||||
{getFieldDecorator("kmin", {
|
<Input />
|
||||||
initialValue: job.kmin
|
|
||||||
})(<Input name='kmin' />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.kmout")}>
|
<Form.Item label={t("jobs.fields.kmout")} name="kmout">
|
||||||
{getFieldDecorator("kmout", {
|
<Input />
|
||||||
initialValue: job.kmout
|
|
||||||
})(<Input name='kmout' />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.referralsource")}>
|
<Form.Item label={t("jobs.fields.referralsource")} name="referral_source">
|
||||||
{getFieldDecorator("referral_source", {
|
<Input />
|
||||||
initialValue: job.referral_source
|
|
||||||
})(<Input name='referral_source' />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,89 +1,78 @@
|
|||||||
import { DatePicker, Form } from "antd";
|
import { DatePicker, Form } from "antd";
|
||||||
import moment from "moment";
|
import React from "react";
|
||||||
import React, { useContext } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import JobDetailFormContext from "../../pages/jobs-detail/jobs-detail.page.context";
|
|
||||||
|
|
||||||
export default function JobsDetailDatesComponent({ job }) {
|
export default function JobsDetailDatesComponent({ job }) {
|
||||||
const form = useContext(JobDetailFormContext);
|
|
||||||
const { getFieldDecorator } = form;
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// initialValue: job.loss_date ? moment(job.loss_date) : null
|
||||||
|
// initialValue: job.date_estimated ? moment(job.date_estimated) : null
|
||||||
|
// initialValue: job.date_open ? moment(job.date_open) : null
|
||||||
|
// initialValue: job.date_scheduled ? moment(job.date_scheduled) : null
|
||||||
|
// initialValue: job.scheduled_in ? moment(job.scheduled_in) : null
|
||||||
|
// initialValue: job.actual_in ? moment(job.actual_in) : null
|
||||||
|
// initialValue: job.scheduled_completion ? moment(job.scheduled_completion) : null
|
||||||
|
// initialValue: job.actual_completion ? moment(job.actual_completion) : null
|
||||||
|
// initialValue: job.scheduled_delivery ? moment(job.scheduled_delivery) : null
|
||||||
|
// initialValue: job.actual_delivery ? moment(job.actual_delivery) : null
|
||||||
|
// initialValue: job.date_invoiced ? moment(job.date_invoiced) : null
|
||||||
|
// initialValue: job.date_closed ? moment(job.date_closed) : null
|
||||||
|
// initialValue: job.date_exported ? moment(job.date_exported) : null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Form.Item label={t("jobs.fields.loss_date")}>
|
<Form.Item label={t("jobs.fields.loss_date")} name="loss_date">
|
||||||
{getFieldDecorator("loss_date", {
|
<DatePicker />
|
||||||
initialValue: job.loss_date ? moment(job.loss_date) : null
|
|
||||||
})(<DatePicker name="loss_date" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
DAMAGE {JSON.stringify(job.area_of_damage)}
|
|
||||||
CAA # seems not correct based on field mapping Class seems not correct
|
CAA # seems not correct based on field mapping Class seems not correct
|
||||||
based on field mapping
|
based on field mapping
|
||||||
<Form.Item label={t("jobs.fields.date_estimated")}>
|
<Form.Item label={t("jobs.fields.date_estimated")} name="date_estimated">
|
||||||
{getFieldDecorator("date_estimated", {
|
<DatePicker />
|
||||||
initialValue: job.date_estimated ? moment(job.date_estimated) : null
|
|
||||||
})(<DatePicker name="date_estimated" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.date_open")}>
|
<Form.Item label={t("jobs.fields.date_open")} name="date_open">
|
||||||
{getFieldDecorator("date_open", {
|
<DatePicker />
|
||||||
initialValue: job.date_open ? moment(job.date_open) : null
|
|
||||||
})(<DatePicker name="date_open" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.date_scheduled")}>
|
<Form.Item label={t("jobs.fields.date_scheduled")} name="date_scheduled">
|
||||||
{getFieldDecorator("date_scheduled", {
|
<DatePicker />
|
||||||
initialValue: job.date_scheduled ? moment(job.date_scheduled) : null
|
|
||||||
})(<DatePicker name="date_scheduled" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.scheduled_in")}>
|
<Form.Item label={t("jobs.fields.scheduled_in")} name="scheduled_in">
|
||||||
{getFieldDecorator("scheduled_in", {
|
<DatePicker />
|
||||||
initialValue: job.scheduled_in ? moment(job.scheduled_in) : null
|
|
||||||
})(<DatePicker name="scheduled_in" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.actual_in")}>
|
<Form.Item label={t("jobs.fields.actual_in")} name="actual_in">
|
||||||
{getFieldDecorator("actual_in", {
|
<DatePicker />
|
||||||
initialValue: job.actual_in ? moment(job.actual_in) : null
|
|
||||||
})(<DatePicker name="actual_in" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.scheduled_completion")}>
|
<Form.Item
|
||||||
{getFieldDecorator("scheduled_completion", {
|
label={t("jobs.fields.scheduled_completion")}
|
||||||
initialValue: job.scheduled_completion
|
name="scheduled_completion"
|
||||||
? moment(job.scheduled_completion)
|
>
|
||||||
: null
|
<DatePicker />
|
||||||
})(<DatePicker name="scheduled_completion" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.actual_completion")}>
|
<Form.Item
|
||||||
{getFieldDecorator("actual_completion", {
|
label={t("jobs.fields.actual_completion")}
|
||||||
initialValue: job.actual_completion
|
name="actual_completion"
|
||||||
? moment(job.actual_completion)
|
>
|
||||||
: null
|
<DatePicker />
|
||||||
})(<DatePicker name="actual_completion" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.scheduled_delivery")}>
|
<Form.Item
|
||||||
{getFieldDecorator("scheduled_delivery", {
|
label={t("jobs.fields.scheduled_delivery")}
|
||||||
initialValue: job.scheduled_delivery
|
name="scheduled_delivery"
|
||||||
? moment(job.scheduled_delivery)
|
>
|
||||||
: null
|
<DatePicker />
|
||||||
})(<DatePicker name="scheduled_delivery" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.actual_delivery")}>
|
<Form.Item
|
||||||
{getFieldDecorator("actual_delivery", {
|
label={t("jobs.fields.actual_delivery")}
|
||||||
initialValue: job.actual_delivery ? moment(job.actual_delivery) : null
|
name="actual_delivery"
|
||||||
})(<DatePicker name="actual_delivery" />)}
|
>
|
||||||
|
<DatePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.date_invoiced")}>
|
<Form.Item label={t("jobs.fields.date_invoiced")} name="date_invoiced">
|
||||||
{getFieldDecorator("date_invoiced", {
|
<DatePicker />
|
||||||
initialValue: job.date_invoiced ? moment(job.date_invoiced) : null
|
|
||||||
})(<DatePicker name="date_invoiced" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.date_closed")}>
|
<Form.Item label={t("jobs.fields.date_closed")} name="date_closed">
|
||||||
{getFieldDecorator("date_closed", {
|
<DatePicker />
|
||||||
initialValue: job.date_closed ? moment(job.date_closed) : null
|
|
||||||
})(<DatePicker name="date_closed" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.date_exported")}>
|
<Form.Item label={t("jobs.fields.date_exported")} name="date_exported">
|
||||||
{getFieldDecorator("date_exported", {
|
<DatePicker />
|
||||||
initialValue: job.date_exported ? moment(job.date_exported) : null
|
|
||||||
})(<DatePicker name="date_exported" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,181 +1,131 @@
|
|||||||
import { Form, Input, InputNumber, Divider } from "antd";
|
import { Divider, Form, Input, InputNumber } from "antd";
|
||||||
import React, { useContext } from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import JobDetailFormContext from "../../pages/jobs-detail/jobs-detail.page.context";
|
|
||||||
|
|
||||||
export default function JobsDetailFinancials({ job }) {
|
export default function JobsDetailFinancials({ job }) {
|
||||||
const form = useContext(JobDetailFormContext);
|
|
||||||
const { getFieldDecorator } = form;
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Form.Item label={t("jobs.fields.ded_amt")}>
|
<Form.Item label={t("jobs.fields.ded_amt")} name="ded_amt">
|
||||||
{getFieldDecorator("ded_amt", {
|
<InputNumber />
|
||||||
initialValue: job.ded_amt
|
|
||||||
})(<InputNumber name="ded_amt" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.ded_status")}>
|
<Form.Item label={t("jobs.fields.ded_status")} name="ded_status">
|
||||||
{getFieldDecorator("ded_status", {
|
<Input />
|
||||||
initialValue: job.ded_status
|
|
||||||
})(<Input name="ded_status" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.depreciation_taxes")}>
|
<Form.Item
|
||||||
{getFieldDecorator("depreciation_taxes", {
|
label={t("jobs.fields.depreciation_taxes")}
|
||||||
initialValue: job.depreciation_taxes
|
name="depreciation_taxes"
|
||||||
})(<InputNumber name="depreciation_taxes" />)}
|
>
|
||||||
|
<InputNumber />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
TODO This is equivalent of GST payable.
|
TODO This is equivalent of GST payable.
|
||||||
<Form.Item label={t("jobs.fields.federal_tax_payable")}>
|
<Form.Item
|
||||||
{getFieldDecorator("federal_tax_payable", {
|
label={t("jobs.fields.federal_tax_payable")}
|
||||||
initialValue: job.federal_tax_payable
|
name="federal_tax_payable"
|
||||||
})(<InputNumber name="federal_tax_payable" />)}
|
>
|
||||||
|
<InputNumber />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
TODO equivalent of other customer amount
|
TODO equivalent of other customer amount
|
||||||
<Form.Item label={t("jobs.fields.other_amount_payable")}>
|
<Form.Item
|
||||||
{getFieldDecorator("other_amount_payable", {
|
label={t("jobs.fields.other_amount_payable")}
|
||||||
initialValue: job.other_amount_payable
|
name="other_amount_payable"
|
||||||
})(<InputNumber name="other_amount_payable" />)}
|
>
|
||||||
|
<InputNumber />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.towing_payable")}>
|
<Form.Item label={t("jobs.fields.towing_payable")} name="towing_payable">
|
||||||
{getFieldDecorator("towing_payable", {
|
<InputNumber />
|
||||||
initialValue: job.towing_payable
|
|
||||||
})(<InputNumber name="towing_payable" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.storage_payable")}>
|
<Form.Item
|
||||||
{getFieldDecorator("storage_payable", {
|
label={t("jobs.fields.storage_payable")}
|
||||||
initialValue: job.storage_payable
|
name="storage_payable"
|
||||||
})(<InputNumber name="storage_payable" />)}
|
>
|
||||||
|
<InputNumber />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.adjustment_bottom_line")}>
|
<Form.Item
|
||||||
{getFieldDecorator("adjustment_bottom_line", {
|
label={t("jobs.fields.adjustment_bottom_line")}
|
||||||
initialValue: job.adjustment_bottom_line
|
name="adjustment_bottom_line"
|
||||||
})(<InputNumber name="adjustment_bottom_line" />)}
|
>
|
||||||
|
<InputNumber />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Divider />
|
<Divider />
|
||||||
Totals Table
|
Totals Table
|
||||||
<Form.Item label={t("jobs.fields.labor_rate_desc")}>
|
<Form.Item
|
||||||
{getFieldDecorator("labor_rate_desc", {
|
label={t("jobs.fields.labor_rate_desc")}
|
||||||
initialValue: job.labor_rate_desc
|
name="labor_rate_desc"
|
||||||
})(<Input name="labor_rate_desc" />)}
|
>
|
||||||
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_lab")}>
|
<Form.Item label={t("jobs.fields.rate_lab")} name="rate_lab">
|
||||||
{getFieldDecorator("rate_lab", {
|
<InputNumber />
|
||||||
initialValue: job.rate_lab
|
|
||||||
})(<InputNumber name="rate_lab" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_lad")}>
|
<Form.Item label={t("jobs.fields.rate_lad")} name="rate_lad">
|
||||||
{getFieldDecorator("rate_lad", {
|
<InputNumber />
|
||||||
initialValue: job.rate_lad
|
|
||||||
})(<InputNumber name="rate_lad" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_lae")}>
|
<Form.Item label={t("jobs.fields.rate_lae")} name="rate_lae">
|
||||||
{getFieldDecorator("rate_lae", {
|
<InputNumber />
|
||||||
initialValue: job.rate_lae
|
|
||||||
})(<InputNumber name="rate_lae" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_lar")}>
|
<Form.Item label={t("jobs.fields.rate_lar")} name="rate_lar">
|
||||||
{getFieldDecorator("rate_lar", {
|
<InputNumber />
|
||||||
initialValue: job.rate_lar
|
|
||||||
})(<InputNumber name="rate_lar" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_las")}>
|
<Form.Item label={t("jobs.fields.rate_las")} name="rate_las">
|
||||||
{getFieldDecorator("rate_las", {
|
<InputNumber />
|
||||||
initialValue: job.rate_las
|
|
||||||
})(<InputNumber name="rate_las" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_laf")}>
|
<Form.Item label={t("jobs.fields.rate_laf")} name="rate_laf">
|
||||||
{getFieldDecorator("rate_laf", {
|
<InputNumber />
|
||||||
initialValue: job.rate_laf
|
|
||||||
})(<InputNumber name="rate_laf" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_lam")}>
|
<Form.Item label={t("jobs.fields.rate_lam")} name="rate_lam">
|
||||||
{getFieldDecorator("rate_lam", {
|
<InputNumber />
|
||||||
initialValue: job.rate_lam
|
|
||||||
})(<InputNumber name="rate_lam" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_lag")}>
|
<Form.Item label={t("jobs.fields.rate_lag")} name="rate_lag">
|
||||||
{getFieldDecorator("rate_lag", {
|
<InputNumber />
|
||||||
initialValue: job.rate_lag
|
|
||||||
})(<InputNumber name="rate_lag" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
Note //TODO Remove ATP rate?
|
Note //TODO Remove ATP rate?
|
||||||
<Form.Item label={t("jobs.fields.rate_atp")}>
|
<Form.Item label={t("jobs.fields.rate_atp")} name="rate_atp">
|
||||||
{getFieldDecorator("rate_atp", {
|
<InputNumber />
|
||||||
initialValue: job.rate_atp
|
|
||||||
})(<InputNumber name="rate_atp" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_lau")}>
|
<Form.Item label={t("jobs.fields.rate_lau")} name="rate_lau">
|
||||||
{getFieldDecorator("rate_lau", {
|
<InputNumber />
|
||||||
initialValue: job.rate_lau
|
|
||||||
})(<InputNumber name="rate_lau" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_la1")}>
|
<Form.Item label={t("jobs.fields.rate_la1")} name="rate_la1">
|
||||||
{getFieldDecorator("rate_la1", {
|
<InputNumber />
|
||||||
initialValue: job.rate_la1
|
|
||||||
})(<InputNumber name="rate_la1" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_la2")}>
|
<Form.Item label={t("jobs.fields.rate_la2")} name="rate_la2">
|
||||||
{getFieldDecorator("rate_la2", {
|
<InputNumber />
|
||||||
initialValue: job.rate_la2
|
|
||||||
})(<InputNumber name="rate_la2" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_la3")}>
|
<Form.Item label={t("jobs.fields.rate_la3")} name="rate_la3">
|
||||||
{getFieldDecorator("rate_la3", {
|
<InputNumber />
|
||||||
initialValue: job.rate_la3
|
|
||||||
})(<InputNumber name="rate_la3" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_la4")}>
|
<Form.Item label={t("jobs.fields.rate_la4")} name="rate_la4">
|
||||||
{getFieldDecorator("rate_la4", {
|
<InputNumber />
|
||||||
initialValue: job.rate_la4
|
|
||||||
})(<InputNumber name="rate_la4" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_mapa")}>
|
<Form.Item label={t("jobs.fields.rate_mapa")} name="rate_mapa">
|
||||||
{getFieldDecorator("rate_mapa", {
|
<InputNumber />
|
||||||
initialValue: job.rate_mapa
|
|
||||||
})(<InputNumber name="rate_mapa" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_mash")}>
|
<Form.Item label={t("jobs.fields.rate_mash")} name="rate_mash">
|
||||||
{getFieldDecorator("rate_mash", {
|
<InputNumber />
|
||||||
initialValue: job.rate_mash
|
|
||||||
})(<InputNumber name="rate_mash" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_mahw")}>
|
<Form.Item label={t("jobs.fields.rate_mahw")} name="rate_mahw">
|
||||||
{getFieldDecorator("rate_mahw", {
|
<InputNumber />
|
||||||
initialValue: job.rate_mahw
|
|
||||||
})(<InputNumber name="rate_mahw" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_ma2s")}>
|
<Form.Item label={t("jobs.fields.rate_ma2s")} name="rate_ma2s">
|
||||||
{getFieldDecorator("rate_ma2s", {
|
<InputNumber />
|
||||||
initialValue: job.rate_ma2s
|
|
||||||
})(<InputNumber name="rate_ma2s" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_ma3s")}>
|
<Form.Item label={t("jobs.fields.rate_ma3s")} name="rate_ma3s">
|
||||||
{getFieldDecorator("rate_ma3s", {
|
<InputNumber />
|
||||||
initialValue: job.rate_ma3s
|
|
||||||
})(<InputNumber name="rate_ma3s" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_mabl")}>
|
<Form.Item label={t("jobs.fields.rate_mabl")} name="rate_mabl">
|
||||||
{getFieldDecorator("rate_mabl", {
|
<InputNumber />
|
||||||
initialValue: job.rate_mabl
|
|
||||||
})(<InputNumber name="rate_mabl" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_macs")}>
|
<Form.Item label={t("jobs.fields.rate_macs")} name="rate_macs">
|
||||||
{getFieldDecorator("rate_macs", {
|
<InputNumber />
|
||||||
initialValue: job.rate_macs
|
|
||||||
})(<InputNumber name="rate_macs" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_matd")}>
|
<Form.Item label={t("jobs.fields.rate_matd")} name="rate_matd">
|
||||||
{getFieldDecorator("rate_matd", {
|
<InputNumber />
|
||||||
initialValue: job.rate_matd
|
</Form.Item>
|
||||||
})(<InputNumber name="rate_matd" />)}
|
<Form.Item label={t("jobs.fields.rate_laa")} name="rate_laa">
|
||||||
|
<InputNumber />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_laa")}>
|
|
||||||
{getFieldDecorator("rate_laa", {
|
|
||||||
initialValue: job.rate_laa
|
|
||||||
})(<InputNumber name="rate_laa" />)}
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Menu, Dropdown, Button } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { DownCircleFilled } from "@ant-design/icons";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function JobsDetailHeaderActions({ job }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const statusmenu = (
|
||||||
|
<Menu key="popovermenu">
|
||||||
|
<Menu.Item key="cccontract">
|
||||||
|
<Link
|
||||||
|
to={{
|
||||||
|
pathname: "/manage/courtesycars/contracts/new",
|
||||||
|
state: { jobId: job.id }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("menus.jobsactions.newcccontract")}
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Dropdown overlay={statusmenu} key="changestatus">
|
||||||
|
<Button>
|
||||||
|
{t("general.labels.actions")} <DownCircleFilled />
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DownCircleFilled } from "@ant-design/icons";
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Badge,
|
Badge,
|
||||||
@@ -5,7 +6,6 @@ import {
|
|||||||
Checkbox,
|
Checkbox,
|
||||||
Descriptions,
|
Descriptions,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
Icon,
|
|
||||||
Menu,
|
Menu,
|
||||||
notification,
|
notification,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
@@ -21,6 +21,9 @@ import CarImage from "../../assets/car.svg";
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import BarcodePopup from "../barcode-popup/barcode-popup.component";
|
import BarcodePopup from "../barcode-popup/barcode-popup.component";
|
||||||
|
import OwnerTagPopoverComponent from "../owner-tag-popover/owner-tag-popover.component";
|
||||||
|
import VehicleTagPopoverComponent from "../vehicle-tag-popover/vehicle-tag-popover.component";
|
||||||
|
import JobsDetailHeaderActions from "../jobs-detail-header-actions/jobs-detail-header-actions.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -33,7 +36,6 @@ export default connect(
|
|||||||
job,
|
job,
|
||||||
mutationConvertJob,
|
mutationConvertJob,
|
||||||
refetch,
|
refetch,
|
||||||
handleSubmit,
|
|
||||||
scheduleModalState,
|
scheduleModalState,
|
||||||
bodyshop,
|
bodyshop,
|
||||||
updateJobStatus
|
updateJobStatus
|
||||||
@@ -43,10 +45,10 @@ export default connect(
|
|||||||
|
|
||||||
const tombstoneTitle = (
|
const tombstoneTitle = (
|
||||||
<div>
|
<div>
|
||||||
<Avatar size='large' alt='Vehicle Image' src={CarImage} />
|
<Avatar size="large" alt="Vehicle Image" src={CarImage} />
|
||||||
{`${t("jobs.fields.ro_number")} ${
|
{job.ro_number
|
||||||
job.ro_number ? job.ro_number : t("general.labels.na")
|
? `${t("jobs.fields.ro_number")} ${job.ro_number}`
|
||||||
}`}
|
: `EST-${job.est_number}`}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -54,7 +56,8 @@ export default connect(
|
|||||||
<Menu
|
<Menu
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
updateJobStatus(e.key);
|
updateJobStatus(e.key);
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{bodyshop.md_ro_statuses.statuses.map(item => (
|
{bodyshop.md_ro_statuses.statuses.map(item => (
|
||||||
<Menu.Item key={item}>{item}</Menu.Item>
|
<Menu.Item key={item}>{item}</Menu.Item>
|
||||||
))}
|
))}
|
||||||
@@ -62,23 +65,24 @@ export default connect(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const menuExtra = [
|
const menuExtra = [
|
||||||
<Dropdown overlay={statusmenu} key='changestatus'>
|
<Dropdown overlay={statusmenu} key="changestatus">
|
||||||
<Button>
|
<Button>
|
||||||
{t("jobs.actions.changestatus")} <Icon type='down' />
|
{t("jobs.actions.changestatus")} <DownCircleFilled />
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>,
|
</Dropdown>,
|
||||||
<Badge key='schedule' count={job.appointments_aggregate.aggregate.count}>
|
<Badge key="schedule" count={job.appointments_aggregate.aggregate.count}>
|
||||||
<Button
|
<Button
|
||||||
//TODO Enabled logic based on status.
|
//TODO Enabled logic based on status.
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setscheduleModalVisible(true);
|
setscheduleModalVisible(true);
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{t("jobs.actions.schedule")}
|
{t("jobs.actions.schedule")}
|
||||||
</Button>
|
</Button>
|
||||||
</Badge>,
|
</Badge>,
|
||||||
<Button
|
<Button
|
||||||
key='convert'
|
key="convert"
|
||||||
type='dashed'
|
type="dashed"
|
||||||
disabled={job.converted}
|
disabled={job.converted}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutationConvertJob({
|
mutationConvertJob({
|
||||||
@@ -90,15 +94,13 @@ export default connect(
|
|||||||
message: t("jobs.successes.converted")
|
message: t("jobs.successes.converted")
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{t("jobs.actions.convert")}
|
{t("jobs.actions.convert")}
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button
|
<JobsDetailHeaderActions key="actions" job={job} />,
|
||||||
type='primary'
|
<Button type="primary" key="submit" htmlType="submit">
|
||||||
key='submit'
|
{t("general.actions.save")}
|
||||||
htmlType='button'
|
|
||||||
onClick={handleSubmit}>
|
|
||||||
{t("general.labels.save")}
|
|
||||||
</Button>
|
</Button>
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -110,54 +112,53 @@ export default connect(
|
|||||||
title={tombstoneTitle}
|
title={tombstoneTitle}
|
||||||
//subTitle={tombstoneSubtitle}
|
//subTitle={tombstoneSubtitle}
|
||||||
tags={
|
tags={
|
||||||
<span key='job-status'>
|
<span key="job-status">
|
||||||
{job.status ? <Tag color='blue'>{job.status}</Tag> : null}
|
{job.status ? <Tag color="blue">{job.status}</Tag> : null}
|
||||||
<Tag color='red'>
|
<OwnerTagPopoverComponent job={job} />
|
||||||
{job.owner ? (
|
<VehicleTagPopoverComponent job={job} />
|
||||||
<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>
|
|
||||||
<BarcodePopup value={job.id} />
|
<BarcodePopup value={job.id} />
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
extra={menuExtra}>
|
extra={menuExtra}
|
||||||
<Descriptions size='small' column={5}>
|
>
|
||||||
<Descriptions.Item label={t("jobs.fields.repairtotal")}>
|
<Descriptions size="small" column={5}>
|
||||||
|
<Descriptions.Item key="total" label={t("jobs.fields.repairtotal")}>
|
||||||
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
|
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
|
||||||
<Descriptions.Item label={t("jobs.fields.customerowing")}>
|
<Descriptions.Item
|
||||||
|
key="custowing"
|
||||||
|
label={t("jobs.fields.customerowing")}
|
||||||
|
>
|
||||||
##NO BINDING YET##
|
##NO BINDING YET##
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
|
||||||
<Descriptions.Item label={t("jobs.fields.specialcoveragepolicy")}>
|
<Descriptions.Item
|
||||||
|
key="scp"
|
||||||
|
label={t("jobs.fields.specialcoveragepolicy")}
|
||||||
|
>
|
||||||
<Checkbox checked={job.special_coverage_policy} />
|
<Checkbox checked={job.special_coverage_policy} />
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
|
||||||
<Descriptions.Item label={t("jobs.fields.scheduled_completion")}>
|
<Descriptions.Item
|
||||||
|
key="sched_comp"
|
||||||
|
label={t("jobs.fields.scheduled_completion")}
|
||||||
|
>
|
||||||
{job.scheduled_completion ? (
|
{job.scheduled_completion ? (
|
||||||
<Moment format='MM/DD/YYYY'>{job.scheduled_completion}</Moment>
|
<Moment format="MM/DD/YYYY">{job.scheduled_completion}</Moment>
|
||||||
) : null}
|
) : null}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
|
||||||
<Descriptions.Item label={t("jobs.fields.servicecar")}>
|
<Descriptions.Item key="servicecar" label={t("jobs.fields.servicecar")}>
|
||||||
{job.service_car}
|
{job.cccontracts &&
|
||||||
|
job.cccontracts.map(item => (
|
||||||
|
<Link
|
||||||
|
key={item.id}
|
||||||
|
to={`/manage/courtesycars/contracts/${item.id}`}
|
||||||
|
>
|
||||||
|
<div>{`${item.agreementnumber} - ${item.start} - ${item.scheduledreturn}`}</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|||||||
@@ -1,146 +1,115 @@
|
|||||||
import { Divider, Form, Input, DatePicker } from "antd";
|
import { DatePicker, Divider, Form, Input } from "antd";
|
||||||
import React, { useContext } from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 FormItemEmail from "../form-items-formatted/email-form-item.component";
|
||||||
import FormItemPhone from "../form-items-formatted/phone-form-item.component";
|
import FormItemPhone from "../form-items-formatted/phone-form-item.component";
|
||||||
import moment from "moment";
|
|
||||||
|
|
||||||
export default function JobsDetailInsurance({ job }) {
|
export default function JobsDetailInsurance({ job, form }) {
|
||||||
const form = useContext(JobDetailFormContext);
|
const { getFieldValue } = form;
|
||||||
const { getFieldDecorator, getFieldValue } = form;
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
//initialValue: job.loss_date ? moment(job.loss_date) : null
|
||||||
|
console.log("job", job);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Form.Item label={t("jobs.fields.ins_co_id")}>
|
<Form.Item label={t("jobs.fields.ins_co_id")} name="ins_co_id">
|
||||||
{getFieldDecorator("ins_co_id", {
|
<Input />
|
||||||
initialValue: job.ins_co_id
|
|
||||||
})(<Input name="ins_co_id" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.policy_no")}>
|
<Form.Item label={t("jobs.fields.policy_no")} name="policy_no">
|
||||||
{getFieldDecorator("policy_no", {
|
<Input />
|
||||||
initialValue: job.policy_no
|
|
||||||
})(<Input name="policy_no" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.clm_no")}>
|
<Form.Item label={t("jobs.fields.clm_no")} name="clm_no">
|
||||||
{getFieldDecorator("clm_no", {
|
<Input />
|
||||||
initialValue: job.clm_no
|
|
||||||
})(<Input name="clm_no" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.regie_number")}>
|
<Form.Item label={t("jobs.fields.regie_number")} name="regie_number">
|
||||||
{getFieldDecorator("regie_number", {
|
<Input />
|
||||||
initialValue: job.regie_number
|
|
||||||
})(<Input name="regie_number" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
TODO: missing KOL field???
|
TODO: missing KOL field???
|
||||||
<Form.Item label={t("jobs.fields.loss_date")}>
|
<Form.Item label={t("jobs.fields.loss_date")} name="loss_date">
|
||||||
{getFieldDecorator("loss_date", {
|
<DatePicker />
|
||||||
initialValue: job.loss_date ? moment(job.loss_date) : null
|
|
||||||
})(<DatePicker name="loss_date" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
DAMAGE {JSON.stringify(job.area_of_damage)}
|
DAMAGE {JSON.stringify(job.area_of_damage)}
|
||||||
CAA # seems not correct based on field mapping Class seems not correct
|
CAA # seems not correct based on field mapping Class seems not correct
|
||||||
based on field mapping
|
based on field mapping
|
||||||
<Form.Item label={t("jobs.fields.ins_co_nm")}>
|
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
|
||||||
{getFieldDecorator("ins_co_nm", {
|
<Input />
|
||||||
initialValue: job.ins_co_nm
|
|
||||||
})(<Input name="ins_co_nm" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.ins_addr1")}>
|
<Form.Item label={t("jobs.fields.ins_addr1")} name="ins_addr1">
|
||||||
{getFieldDecorator("ins_addr1", {
|
<Input />
|
||||||
initialValue: job.ins_addr1
|
|
||||||
})(<Input name="ins_addr1" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.ins_city")}>
|
<Form.Item label={t("jobs.fields.ins_city")} name="ins_city">
|
||||||
{getFieldDecorator("ins_city", {
|
<Input />
|
||||||
initialValue: job.ins_city
|
|
||||||
})(<Input name="ins_city" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.ins_ct_ln")}>
|
<Form.Item label={t("jobs.fields.ins_ct_ln")} name="ins_ct_ln">
|
||||||
{getFieldDecorator("ins_ct_ln", {
|
<Input />
|
||||||
initialValue: job.ins_ct_ln
|
|
||||||
})(<Input name="ins_ct_ln" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.ins_ct_fn")}>
|
<Form.Item label={t("jobs.fields.ins_ct_fn")} name="ins_ct_fn">
|
||||||
{getFieldDecorator("ins_ct_fn", {
|
<Input />
|
||||||
initialValue: job.ins_ct_fn
|
|
||||||
})(<Input name="ins_ct_fn" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.ins_ph1")}>
|
<Form.Item label={t("jobs.fields.ins_ph1")} name="ins_ph1">
|
||||||
{getFieldDecorator("ins_ph1", {
|
<FormItemPhone customInput={Input} />
|
||||||
initialValue: job.ins_ph1
|
|
||||||
})(<FormItemPhone customInput={Input} name="ins_ph1" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.ins_ea")}>
|
<Form.Item
|
||||||
{getFieldDecorator("ins_ea", {
|
label={t("jobs.fields.ins_ea")}
|
||||||
initialValue: job.ins_ea,
|
name="ins_ea"
|
||||||
rules: [
|
rules={[
|
||||||
{
|
{
|
||||||
type: "email",
|
type: "email",
|
||||||
message: "This is not a valid email address."
|
message: "This is not a valid email address."
|
||||||
}
|
}
|
||||||
]
|
]}
|
||||||
})(<FormItemEmail name="ins_ea" email={getFieldValue("ins_ea")} />)}
|
>
|
||||||
|
<FormItemEmail email={getFieldValue("ins_ea")} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Divider />
|
<Divider />
|
||||||
Appraiser Info
|
Appraiser Info
|
||||||
<Form.Item label={t("jobs.fields.est_co_nm")}>
|
<Form.Item label={t("jobs.fields.est_co_nm")} name="est_co_nm">
|
||||||
{getFieldDecorator("est_co_nm", {
|
<Input />
|
||||||
initialValue: job.est_co_nm
|
|
||||||
})(<Input name="est_co_nm" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.est_ct_fn")}>
|
<Form.Item label={t("jobs.fields.est_ct_fn")} name="est_ct_fn">
|
||||||
{getFieldDecorator("est_ct_fn", {
|
<Input />
|
||||||
initialValue: job.est_ct_fn
|
|
||||||
})(<Input name="est_ct_fn" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.est_ct_ln")}>
|
<Form.Item label={t("jobs.fields.est_ct_ln")} name="est_ct_ln">
|
||||||
{getFieldDecorator("est_ct_ln", {
|
<Input />
|
||||||
initialValue: job.est_ct_ln
|
|
||||||
})(<Input name="est_ct_ln" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
TODO: Field is pay date but title is inspection date. Likely incorrect?
|
TODO: Field is pay date but title is inspection date. Likely incorrect?
|
||||||
<Form.Item label={t("jobs.fields.pay_date")}>
|
<Form.Item label={t("jobs.fields.pay_date")} name="pay_date">
|
||||||
{getFieldDecorator("pay_date", {
|
<Input />
|
||||||
initialValue: job.pay_date
|
|
||||||
})(<Input name="pay_date" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.est_ph1")}>
|
<Form.Item label={t("jobs.fields.est_ph1")} name="est_ph1">
|
||||||
{getFieldDecorator("est_ph1", {
|
<Input />
|
||||||
initialValue: job.est_ph1
|
|
||||||
})(<Input name="est_ph1" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.est_ea")}>
|
<Form.Item
|
||||||
{getFieldDecorator("est_ea", {
|
label={t("jobs.fields.est_ea")}
|
||||||
initialValue: job.est_ea,
|
name="est_ea"
|
||||||
rules: [
|
rules={[
|
||||||
{
|
{
|
||||||
type: "email",
|
type: "email",
|
||||||
message: "This is not a valid email address."
|
message: "This is not a valid email address."
|
||||||
}
|
}
|
||||||
]
|
]}
|
||||||
})(<FormItemEmail name="est_ea" email={getFieldValue("est_ea")} />)}
|
>
|
||||||
|
<FormItemEmail email={getFieldValue("est_ea")} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.selling_dealer")}>
|
<Form.Item label={t("jobs.fields.selling_dealer")} name="selling_dealer">
|
||||||
{getFieldDecorator("selling_dealer", {
|
<Input />
|
||||||
initialValue: job.selling_dealer
|
|
||||||
})(<Input name="selling_dealer" />)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.servicing_dealer")}>
|
<Form.Item
|
||||||
{getFieldDecorator("servicing_dealer", {
|
label={t("jobs.fields.servicing_dealer")}
|
||||||
initialValue: job.servicing_dealer
|
name="servicing_dealer"
|
||||||
})(<Input name="servicing_dealer" />)}
|
>
|
||||||
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.selling_dealer_contact")}>
|
<Form.Item
|
||||||
{getFieldDecorator("selling_dealer_contact", {
|
label={t("jobs.fields.selling_dealer_contact")}
|
||||||
initialValue: job.selling_dealer_contact
|
name="selling_dealer_contact"
|
||||||
})(<Input name="selling_dealer_contact" />)}
|
>
|
||||||
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.servicing_dealer_contact")}>
|
<Form.Item
|
||||||
{getFieldDecorator("servicing_dealer_contact", {
|
label={t("jobs.fields.servicing_dealer_contact")}
|
||||||
initialValue: job.servicing_dealer_contact
|
name="servicing_dealer_contact"
|
||||||
})(<Input name="servicing_dealer_contact" />)}
|
>
|
||||||
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
TODO: Adding servicing/selling dealer contact info?
|
TODO: Adding servicing/selling dealer contact info?
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
|
import { Button } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
toggleModalVisible,
|
import InvoicesListTableComponent from "../invoices-list-table/invoices-list-table.component";
|
||||||
setModalContext
|
import AlertComponent from "../alert/alert.component";
|
||||||
} from "../../redux/modals/modals.actions";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("invoiceEnter")),
|
|
||||||
setInvoiceEnterContext: context =>
|
setInvoiceEnterContext: context =>
|
||||||
dispatch(setModalContext({ context: context, modal: "invoiceEnter" }))
|
dispatch(setModalContext({ context: context, modal: "invoiceEnter" }))
|
||||||
});
|
});
|
||||||
@@ -18,13 +17,13 @@ export default connect(
|
|||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(function JobsDetailPliComponent({
|
)(function JobsDetailPliComponent({
|
||||||
toggleModalVisible,
|
|
||||||
setInvoiceEnterContext,
|
setInvoiceEnterContext,
|
||||||
job
|
job,
|
||||||
|
invoicesQuery
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setInvoiceEnterContext({
|
setInvoiceEnterContext({
|
||||||
actions: { refetch: null },
|
actions: { refetch: null },
|
||||||
@@ -35,7 +34,14 @@ export default connect(
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Enter Invoice
|
Enter Invoice
|
||||||
</div>
|
</Button>
|
||||||
|
{invoicesQuery.error ? (
|
||||||
|
<AlertComponent message={invoicesQuery.error.message} type="error" />
|
||||||
|
) : null}
|
||||||
|
<InvoicesListTableComponent
|
||||||
|
loading={invoicesQuery.loading}
|
||||||
|
invoices={invoicesQuery.data ? invoicesQuery.data.invoices : null}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { useQuery } from "@apollo/react-hooks";
|
||||||
import JobsDetailPliComponent from "./jobs-detail-pli.component";
|
import JobsDetailPliComponent from "./jobs-detail-pli.component";
|
||||||
|
import { QUERY_INVOICES_BY_JOBID } from "../../graphql/invoices.queries";
|
||||||
export default function JobsDetailPliContainer({ job }) {
|
export default function JobsDetailPliContainer({ job }) {
|
||||||
console.log("job", job);
|
const invoicesQuery = useQuery(QUERY_INVOICES_BY_JOBID, {
|
||||||
return <JobsDetailPliComponent job={job} />;
|
variables: { jobid: job.id },
|
||||||
|
fetchPolicy: "network-only"
|
||||||
|
});
|
||||||
|
|
||||||
|
return <JobsDetailPliComponent job={job} invoicesQuery={invoicesQuery} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import Gallery from "react-grid-gallery";
|
||||||
|
//import { Document, Page, pdfjs } from "react-pdf";
|
||||||
|
import DocumentsUploadContainer from "../documents-upload/documents-upload.container";
|
||||||
|
//import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
|
import { Collapse } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
//pdfjs.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.1.266/pdf.worker.min.js`;
|
||||||
|
|
||||||
|
function JobsDocumentsComponent({ data, jobId, refetch }) {
|
||||||
|
const [galleryImages, setgalleryImages] = useState([]);
|
||||||
|
const [pdfDocuments, setPdfDocuments] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setgalleryImages(
|
||||||
|
data
|
||||||
|
.filter(item => item.thumb_url !== "application/pdf")
|
||||||
|
.reduce((acc, value) => {
|
||||||
|
acc.push({
|
||||||
|
src: value.url,
|
||||||
|
thumbnail: value.thumb_url,
|
||||||
|
thumbnailHeight: 150,
|
||||||
|
thumbnailWidth: 150,
|
||||||
|
isSelected: false
|
||||||
|
});
|
||||||
|
return acc;
|
||||||
|
}, [])
|
||||||
|
);
|
||||||
|
}, [data, setgalleryImages]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPdfDocuments(
|
||||||
|
data
|
||||||
|
.filter(item => item.thumb_url === "application/pdf")
|
||||||
|
.reduce((acc, value) => {
|
||||||
|
acc.push({
|
||||||
|
src: value.url,
|
||||||
|
thumbnail: value.thumb_url,
|
||||||
|
thumbnailHeight: 150,
|
||||||
|
thumbnailWidth: 150,
|
||||||
|
isSelected: false
|
||||||
|
});
|
||||||
|
return acc;
|
||||||
|
}, [])
|
||||||
|
);
|
||||||
|
}, [data, setPdfDocuments]);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div className="clearfix">
|
||||||
|
<DocumentsUploadContainer jobId={jobId} callbackAfterUpload={refetch} />
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
axios
|
||||||
|
.get("/downloadImages", {
|
||||||
|
images: galleryImages.map(i => i.src)
|
||||||
|
})
|
||||||
|
.then(r => console.log("r", r));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Dl
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Collapse defaultActiveKey="photos">
|
||||||
|
<Collapse.Panel key="t" header="t">
|
||||||
|
<Gallery
|
||||||
|
images={pdfDocuments}
|
||||||
|
onSelectImage={(index, image) => {
|
||||||
|
setPdfDocuments(
|
||||||
|
pdfDocuments.map((g, idx) =>
|
||||||
|
index === idx ? { ...g, isSelected: !g.isSelected } : g
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Collapse.Panel>
|
||||||
|
<Collapse.Panel key="photos" header={t("documents.labels.photos")}>
|
||||||
|
<Gallery
|
||||||
|
images={galleryImages}
|
||||||
|
onSelectImage={(index, image) => {
|
||||||
|
setgalleryImages(
|
||||||
|
galleryImages.map((g, idx) =>
|
||||||
|
index === idx ? { ...g, isSelected: !g.isSelected } : g
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
></Gallery>
|
||||||
|
</Collapse.Panel>
|
||||||
|
{
|
||||||
|
// <Collapse.Panel
|
||||||
|
// key="documents"
|
||||||
|
// header={t("documents.labels.documents")}
|
||||||
|
// >
|
||||||
|
// {pdfDocuments.map((doc, idx) => (
|
||||||
|
// <Document key={idx} loading={<LoadingSpinner />} file={doc.src}>
|
||||||
|
// <Page pageIndex={0} />
|
||||||
|
// </Document>
|
||||||
|
// ))}
|
||||||
|
// </Collapse.Panel>
|
||||||
|
}
|
||||||
|
</Collapse>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default JobsDocumentsComponent;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user