Merge pull request #15 from snaptsoft/heroku-dev

Heroku dev
This commit is contained in:
2020-02-28 18:11:59 -08:00
committed by GitHub
527 changed files with 33800 additions and 4494 deletions

View File

@@ -1,12 +1,14 @@
React App:
React Hooks are used for Authentication ONLY to ensure the correct web token is passed.
React App:
Yarn Dependency Management:
To force upgrades for some packages: yarn upgrade-interactive --latest
GraphQL API:
Hasura is hosted on another dyno. Several environmental variables are required, including disabling the console.
ALL CHANGES MUST BE MADE USING LOCAL CONSOLE TO ENSURE DATABASE MIGRATION FILES ARE CREATED.
Hasura is hosted on another dyno. Several environmental variables are required, including disabling the console.
ALL CHANGES MUST BE MADE USING LOCAL CONSOLE TO ENSURE DATABASE MIGRATION FILES ARE CREATED.
To Start Hasura CLI:
npx hasura console --admin-secret Dev-BodyShopAppBySnaptSoftware!
Migrating to Staging:
npx hasura migrate apply --up 10 --endpoint https://bodyshop-staging-db.herokuapp.com/ --admin-secret Staging-BodyShopAppBySnaptSoftware!
Migrating to Staging:
npx hasura migrate apply --up 10 --endpoint https://bodyshop-staging-db.herokuapp.com/ --admin-secret Staging-BodyShopAppBySnaptSoftware!

View File

@@ -1,5 +1,51 @@
**Required items**
-Bodyshop Record
-Counter Record - type: ronum
..\*Include the statuses file in the format of:
```json
{
"statuses": [
"Open",
"Scheduled",
"Arrived",
"Repair Plan",
"Parts",
"Body",
"Prep",
"Paint",
"Reassembly",
"Sublet",
"Detail",
"Completed",
"Delivered",
"Invoiced",
"Exported"
],
"open_statuses": [
"Open",
"Scheduled",
"Arrived",
"Repair Plan",
"Parts",
"Body",
"Prep",
"Paint",
"Reassembly",
"Sublet",
"Detail",
"Completed"
],
"default_arrived": "Arrived",
"default_exported": "Exported",
"default_imported": "Open",
"default_invoiced": "Invoiced",
"default_completed": "Completed",
"default_delivered": "Delivered",
"default_scheduled": "Scheduled"
}
```
--\* Set the region for the shop.
-Counter Record - type: ronum

View File

@@ -0,0 +1,192 @@
{
"OP0": {
"desc": "REMOVE / REPLACE PARTIAL",
"opcode": "OP11",
"partcode": "PAA"
},
"OP1": {
"desc": "REFINISH / REPAIR",
"opcode": "OP1",
"partcode": "PAE"
},
"OP10": {
"desc": "REPAIR , PARTIAL",
"opcode": "OP9",
"partcode": "PAE"
},
"OP100": {
"desc": "REPLACE PRE-PRICED",
"opcode": "OP11",
"partcode": "PAA"
},
"OP101": {
"desc": "REMOVE/REPLACE RECYCLED PART",
"opcode": "OP11",
"partcode": "PAL"
},
"OP103": {
"desc": "REMOVE / REPLACE PARTIAL",
"opcode": "OP11",
"partcode": "PAA"
},
"OP104": {
"desc": "REMOVE / REPLACE PARTIAL LABOUR",
"opcode": "OP11",
"partcode": "PAA"
},
"OP105": {
"desc": "!!ADJUST MANUALLY!!",
"opcode": "OP99",
"partcode": "PAE"
},
"OP106": {
"desc": "REPAIR , PARTIAL",
"opcode": "OP9",
"partcode": "PAE"
},
"OP107": {
"desc": "CHIPGUARD",
"opcode": "OP6",
"partcode": "PAE"
},
"OP108": {
"desc": "MULTI TONE",
"opcode": "OP6",
"partcode": "PAE"
},
"OP109": {
"desc": "REPLACE PRE-PRICED",
"opcode": "OP11",
"partcode": "PAA"
},
"OP11": {
"desc": "REMOVE / REPLACE",
"opcode": "OP11",
"partcode": "PAN"
},
"OP110": {
"desc": "REFINISH / REPAIR",
"opcode": "OP1",
"partcode": "PAE"
},
"OP111": {
"desc": "REMOVE / REPLACE",
"opcode": "OP11",
"partcode": "PAN"
},
"OP112": {
"desc": "REMOVE / REPLACE",
"opcode": "OP11",
"partcode": "PAA"
},
"OP113": {
"desc": "REPLACE PRE-PRICED",
"opcode": "OP11",
"partcode": "PAA"
},
"OP114": {
"desc": "REPLACE PRE-PRICED",
"opcode": "OP11",
"partcode": "PAA"
},
"OP12": {
"desc": "REMOVE / REPLACE PARTIAL",
"opcode": "OP11",
"partcode": "PAN"
},
"OP120": {
"desc": "REPAIR , PARTIAL",
"opcode": "OP9",
"partcode": "PAE"
},
"OP13": {
"desc": "ADDITIONAL COSTS",
"opcode": "OP13",
"partcode": "PAE"
},
"OP14": {
"desc": "ADDITIONAL OPERATIONS",
"opcode": "OP14",
"partcode": "PAE"
},
"OP15": {
"desc": "BLEND",
"opcode": "OP15",
"partcode": "PAE"
},
"OP16": {
"desc": "SUBLET",
"opcode": "OP16",
"partcode": "PAS"
},
"OP17": {
"desc": "POLICY LIMIT ADJUSTMENT",
"opcode": "OP9",
"partcode": "PAE"
},
"OP18": {
"desc": "APPEAR ALLOWANCE",
"opcode": "OP7",
"partcode": "PAE"
},
"OP2": {
"desc": "REMOVE / INSTALL",
"opcode": "OP2",
"partcode": "PAE"
},
"OP24": {
"desc": "CHIPGUARD",
"opcode": "OP6",
"partcode": "PAE"
},
"OP25": {
"desc": "TWO TONE",
"opcode": "OP6",
"partcode": "PAE"
},
"OP26": {
"desc": "PAINTLESS DENT REPAIR",
"opcode": "OP16",
"partcode": "PAE"
},
"OP260": {
"desc": "SUBLET",
"opcode": "OP16",
"partcode": "PAE"
},
"OP3": {
"desc": "ADDITIONAL LABOR",
"opcode": "OP9",
"partcode": "PAE"
},
"OP4": {
"desc": "ALIGNMENT",
"opcode": "OP4",
"partcode": "PAS"
},
"OP5": {
"desc": "OVERHAUL",
"opcode": "OP5",
"partcode": "PAE"
},
"OP6": {
"desc": "REFINISH",
"opcode": "OP6",
"partcode": "PAE"
},
"OP7": {
"desc": "INSPECT",
"opcode": "OP7",
"partcode": "PAE"
},
"OP8": {
"desc": "CHECK / ADJUST",
"opcode": "OP8",
"partcode": "PAE"
},
"OP9": {
"desc": "REPAIR",
"opcode": "OP9",
"partcode": "PAE"
}
}

View File

@@ -8,4 +8,7 @@ Bucket=
__React Based__
REACT_APP_GRAPHQL_ENDPOINT
REACT_APP_GRAPHQL_ENDPOINT_WS
REACT_APP_GRAPHQL_ENDPOINT_WS
__MetaData__
Region based OpCodes

File diff suppressed because it is too large Load Diff

View File

@@ -4,33 +4,44 @@
"private": true,
"proxy": "https://localhost:5000",
"dependencies": {
"antd": "^3.26.0",
"@ckeditor/ckeditor5-build-classic": "^16.0.0",
"@ckeditor/ckeditor5-react": "^2.1.0",
"antd": "^3.26.8",
"apollo-boost": "^0.4.4",
"apollo-link-context": "^1.0.19",
"apollo-link-error": "^1.1.12",
"apollo-link-logger": "^1.2.3",
"apollo-link-ws": "^1.0.19",
"axios": "^0.19.1",
"axios": "^0.19.2",
"chart.js": "^2.9.3",
"dotenv": "^8.2.0",
"firebase": "^7.5.0",
"graphql": "^14.5.8",
"i18next": "^19.0.2",
"node-sass": "^4.13.0",
"firebase": "^7.8.1",
"graphql": "^14.6.0",
"i18next": "^19.1.0",
"node-sass": "^4.13.1",
"react": "^16.12.0",
"react-apollo": "^3.1.3",
"react-chartjs-2": "^2.8.0",
"react-barcode": "^1.4.0",
"react-big-calendar": "^0.23.0",
"react-chartjs-2": "^2.9.0",
"react-dom": "^16.12.0",
"react-i18next": "^11.2.7",
"react-icons": "^3.8.0",
"react-html-email": "^3.0.0",
"react-i18next": "^11.3.1",
"react-icons": "^3.9.0",
"react-image-file-resizer": "^0.2.1",
"react-moment": "^0.9.7",
"react-number-format": "^4.3.1",
"react-redux": "^7.1.3",
"react-router-dom": "^5.1.2",
"react-scripts": "3.2.0",
"react-trello": "^2.2.3",
"styled-components": "^4.4.1",
"subscriptions-transport-ws": "^0.9.16"
"react-scripts": "3.3.1",
"redux": "^4.0.5",
"redux-logger": "^3.0.6",
"redux-persist": "^6.0.0",
"redux-saga": "^1.1.3",
"reselect": "^4.0.0",
"styled-components": "^5.0.1",
"subscriptions-transport-ws": "^0.9.16",
"twilio": "^3.39.5"
},
"scripts": {
"start": "react-scripts start",

View File

@@ -1,6 +1,7 @@
{
"short_name": "Bodyshop",
"short_name": "Bodyshop.app",
"name": "Bodyshop Management System",
"description": "The ultimate bodyshop management system",
"icons": [
{
"src": "favicon.ico",
@@ -20,6 +21,6 @@
],
"start_url": ".",
"display": "standalone",
"theme_color": "#002366",
"background_color": "#000000"
"theme_color": "#fff",
"background_color": "#fff"
}

View File

@@ -1,23 +1,18 @@
import React, { Component } from "react";
import App from "./App";
import Spin from "../components/loading-spinner/loading-spinner.component";
import { ApolloLink } from "apollo-boost";
import { InMemoryCache } from "apollo-cache-inmemory";
import ApolloClient from "apollo-client";
import { split } from "apollo-link";
import { setContext } from "apollo-link-context";
import { HttpLink } from "apollo-link-http";
import apolloLogger from "apollo-link-logger";
import { WebSocketLink } from "apollo-link-ws";
import { getMainDefinition } from "apollo-utilities";
import { InMemoryCache } from "apollo-cache-inmemory";
import { setContext } from "apollo-link-context";
import { resolvers, typeDefs } from "../graphql/resolvers";
import apolloLogger from "apollo-link-logger";
import { ApolloLink } from "apollo-boost";
import React, { Component } from "react";
import { ApolloProvider } from "react-apollo";
import { persistCache } from "apollo-cache-persist";
import initialState from "../graphql/initial-state";
import SpinnerComponent from "../components/loading-spinner/loading-spinner.component";
//import { shouldRefreshToken, refreshToken } from "../graphql/middleware";
import errorLink from "../graphql/apollo-error-handling";
import App from "./App";
class AppContainer extends Component {
state = {
@@ -69,14 +64,8 @@ class AppContainer extends Component {
);
const authLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
const token = localStorage.getItem("token");
// return the headers to the context so httpLink can read them
if (token) {
// if (shouldRefreshToken) {
// refreshToken();
// }
return {
headers: {
...headers,
@@ -99,31 +88,13 @@ class AppContainer extends Component {
const client = new ApolloClient({
link: ApolloLink.from(middlewares),
cache,
typeDefs,
resolvers,
connectToDevTools: true
});
client.writeData({
data: initialState
});
try {
await persistCache({
cache,
storage: window.sessionStorage,
debug: true
});
} catch (error) {
console.error("Error restoring Apollo cache", error);
}
this.setState({
client,
loaded: true
});
//Init local state.
}
componentWillUnmount() {}
@@ -132,7 +103,7 @@ class AppContainer extends Component {
const { client, loaded } = this.state;
if (!loaded) {
return <Spin />;
return <SpinnerComponent />;
}
return (

View File

@@ -1,113 +1,50 @@
import React, { useEffect, Suspense, lazy, useState } from "react";
import { useApolloClient, useQuery } from "@apollo/react-hooks";
import { Switch, Route, Redirect } from "react-router-dom";
import firebase from "../firebase/firebase.utils";
import i18next from "i18next";
import "./App.css";
import React, { lazy, Suspense, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Route, Switch } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
//Component Imports
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
import AlertComponent from "../components/alert/alert.component";
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
import { auth } from "../firebase/firebase.utils";
import { UPSERT_USER } from "../graphql/user.queries";
import { GET_CURRENT_USER, GET_LANGUAGE } from "../graphql/local.queries";
import { checkUserSession } from "../redux/user/user.actions";
import { selectCurrentUser } from "../redux/user/user.selectors";
// import { QUERY_BODYSHOP } from "../graphql/bodyshop.queries";
import PrivateRoute from "../utils/private-route";
import "./App.css";
const LandingPage = lazy(() => import("../pages/landing/landing.page"));
const ManagePage = lazy(() => import("../pages/manage/manage.page"));
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
const Unauthorized = lazy(() =>
import("../pages/unauthorized/unauthorized.component")
);
export default () => {
const apolloClient = useApolloClient();
const [loaded, setloaded] = useState(false);
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser
});
const mapDispatchToProps = dispatch => ({
checkUserSession: () => dispatch(checkUserSession())
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(({ checkUserSession, currentUser }) => {
useEffect(() => {
//Run the auth code only on the first render.
const unsubscribeFromAuth = auth.onAuthStateChanged(async user => {
console.log("Auth State Changed.");
setloaded(true);
if (user) {
let token;
token = await user.getIdToken();
const idTokenResult = await user.getIdTokenResult();
const hasuraClaim =
idTokenResult.claims["https://hasura.io/jwt/claims"];
if (!hasuraClaim) {
// Check if refresh is required.
const metadataRef = firebase
.database()
.ref("metadata/" + user.uid + "/refreshTime");
metadataRef.on("value", async () => {
// Force refresh to pick up the latest custom claims changes.
token = await user.getIdToken(true);
});
}
//add the bearer token to the headers.
localStorage.setItem("token", token);
const now = new Date();
window.sessionStorage.setItem(`lastTokenRefreshTime`, now);
// window.sessionStorage.setItem("user", user);
apolloClient
.mutate({
mutation: UPSERT_USER,
variables: { authEmail: user.email, authToken: user.uid }
})
.then()
.catch(error => {
console.log("User login upsert error.", error);
});
apolloClient.writeData({
data: {
currentUser: {
email: user.email,
displayName: user.displayName,
token,
uid: user.uid,
photoUrl: user.photoURL,
__typename: "currentUser"
}
}
});
} else {
apolloClient.writeData({ data: { currentUser: null } });
localStorage.removeItem("token");
}
});
return function cleanup() {
unsubscribeFromAuth();
};
}, [apolloClient]);
const HookCurrentUser = useQuery(GET_CURRENT_USER);
const HookLanguage = useQuery(GET_LANGUAGE);
if (!loaded) return <LoadingSpinner />;
if (HookCurrentUser.loading || HookLanguage.loading)
return <LoadingSpinner />;
if (HookCurrentUser.error || HookLanguage.error)
return (
<AlertComponent
message={HookCurrentUser.error.message || HookLanguage.error.message}
/>
);
if (HookLanguage.data.language)
i18next.changeLanguage(HookLanguage.data.language, (err, t) => {
checkUserSession();
return () => {};
}, [checkUserSession]);
const { t } = useTranslation();
if (currentUser && currentUser.language)
i18next.changeLanguage(currentUser.language, (err, t) => {
if (err)
return console.log("Error encountered when changing languages.", err);
});
if (currentUser.authorized === null) {
return <LoadingSpinner message={t("general.labels.loggingin")} />;
}
return (
<div>
<Switch>
@@ -115,19 +52,12 @@ export default () => {
<Suspense fallback={<LoadingSpinner />}>
<Route exact path='/' component={LandingPage} />
<Route exact path='/unauthorized' component={Unauthorized} />
<Route
exact
path='/signin'
render={() =>
HookCurrentUser.data.currentUser ? (
<Redirect to='/manage' />
) : (
<SignInPage />
)
}
/>
<Route exact path='/signin' component={SignInPage} />
<PrivateRoute
isAuthorized={HookCurrentUser.data.currentUser ? true : false}
//isAuthorized={HookCurrentUser.data.currentUser ? true : false}
isAuthorized={currentUser.authorized}
path='/manage'
component={ManagePage}
/>
@@ -136,4 +66,4 @@ export default () => {
</Switch>
</div>
);
};
});

View File

@@ -0,0 +1,787 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1668"
height="1160"
id="svg2"
version="1.1"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="unfolded_car.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.71043165"
inkscape:cx="463.20424"
inkscape:cy="602.99002"
inkscape:document-units="px"
inkscape:current-layer="primary"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-global="false"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="car"
inkscape:label="CAR"
transform="translate(253.99998,-253.99995)"
style="display:inline">
<g
id="g4113"
transform="translate(-13.779768,3.524026)">
<path
sodipodi:nodetypes="csssscccsssscsccscccsscccssc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3070"
d="M 748.57143,752.85714 C 790,737.14285 888.57143,741.42857 940,740 c 51.42857,-1.42857 160.4745,10.23062 201.4286,27.14286 40.9958,16.92944 134.7843,67.65586 151.4285,72.85714 22.8572,7.14286 41.4286,7.14286 80,20 38.5715,12.85714 25.7143,32.85714 25.7143,32.85714 l -30,-4.28571 -5.7562,52.92008 c 0,0 37.1848,1.36563 41.4705,15.65135 4.2857,14.28571 5.7143,31.42857 -2.8571,41.42857 -8.5715,9.99997 -14.2857,-1.42857 -18.5715,12.85717 -4.2857,14.2857 -2.8571,28.5714 -27.1428,27.1428 -24.2857,-1.4285 -98.5715,0 -98.5715,0 0,0 -15.7142,-108.5714 -98.5714,-105.71426 -82.8571,2.85715 -95.7143,105.71426 -95.7143,105.71426 H 562.85714 c 0,0 -5.71428,-104.28569 -97.14286,-105.71426 -91.42857,-1.42857 -98.57142,105.71426 -98.57142,105.71426 H 301.42857 L 282.85714,1000 c -0.51524,0 -26.24328,-10e-6 -21.42857,-17.14285 4.65143,-16.56149 -4.28571,-41.42858 17.14286,-41.42858 21.42857,0 47.14286,1.42857 47.14286,1.42857 L 341.42857,898.57143 300,895.71429 c 0,0 34.28571,-24.28572 118.57143,-32.85715 84.28571,-8.57143 157.14286,-8.57143 192.85714,-31.42857 35.71429,-22.85714 137.14286,-78.57143 137.14286,-78.57143 z"
style="fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3846"
d="m 282.85714,1000 h 92.85715"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3850"
d="M 555.71429,1000 H 1072.8571"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3852"
d="m 1245.7143,1000 h 151.4286"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="csccc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3884"
d="M 618.57143,847.14286 C 634.28572,828.57143 741.94515,765.61839 770,758.57143 c 29.50156,-7.41035 103.00398,-7.14286 103.00398,-7.14286 l -7.14285,95.71429 z"
style="fill:#f0ffeb;fill-opacity:1;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3888"
d="m 658.57143,817.14286 v 28.57143"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="ccccc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3892"
d="m 898.69729,752.83617 -4.28572,94.32767 h 207.16383 c -11.3076,-20.75266 -46.6124,-74.9056 -72.8572,-88.57143 -14.2857,-10 -82.87805,-4.32767 -130.02091,-5.75624 z"
style="fill:#f0ffeb;fill-opacity:1;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cscsc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3896"
d="m 1065.7143,760 c 0,0 80,84.28571 85.7143,87.14286 5.7143,2.85714 115.7143,1.42857 115.7143,1.42857 0,0 -77.1429,-47.14286 -102.8572,-58.57143 -25.7143,-11.42857 -90,-31.42857 -98.5714,-30 z"
style="fill:#f0ffeb;fill-opacity:1;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="csc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3900"
d="m 599.63544,837.66076 c -14.07595,30.96709 -18.29873,71.78734 -18.29873,94.30886 0,22.52152 4.22279,91.49368 22.52152,105.56958"
style="fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cccc"
inkscape:connector-curvature="0"
id="path3900-6"
d="m 632.78482,993.12906 c -1.40759,5.63038 -4.04683,81.90444 -6.51012,106.93324 -3.67029,18.7146 -4.98821,51.2184 -6.15823,87.3149 0,22.5215 7.03798,80.2329 12.66836,105.5696"
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3920"
d="m 357.52911,998.12658 c 0,0 15.48355,-85.86329 106.97722,-85.86329 91.49367,0 109.7924,87.27089 109.7924,87.27089"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path3920-5"
d="m 800.28863,1253.5341 c 0,0 15.48355,-85.8633 106.97722,-85.8633 91.49364,0 109.79245,87.2709 109.79245,87.2709"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3940"
d="M 323.74684,943.23038 H 387.0886"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cssc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3942"
d="m 1033.1747,746.16708 c 18.2988,12.66836 56.3038,50.67343 92.9012,104.16203 36.5975,53.48861 8.4456,59.11899 -18.2987,74.60254 -26.7443,15.48354 -49.2658,42.22784 -67.5645,112.60755"
style="fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cc"
inkscape:connector-curvature="0"
id="path3944"
d="M 1112.7747,1196.0455 H 983.27594"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3946"
d="M 540.51646,941.82278 H 1085.2557"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3948"
d="m 1062.7342,791.21013 v 54.8962"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
rx="2.9330556"
ry="7.3789682"
y="1144.9166"
x="558.65344"
height="14.541238"
width="41.285542"
id="rect3950"
style="display:inline;fill:#e6e6e6;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
rx="2.9330556"
ry="7.3789682"
y="1144.9166"
x="816.24335"
height="14.541238"
width="41.285542"
id="rect3950-4"
style="display:inline;fill:#e6e6e6;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
ry="7.691968"
rx="1.6302098"
y="1146.318"
x="259.53336"
height="11.738417"
width="18.776392"
id="rect4014"
style="fill:#ffcb00;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
<g
id="g4451"
transform="translate(-13.779768,15.524026)">
<path
inkscape:transform-center-x="-1.6185511"
transform="translate(-37.23036,423.94932)"
d="m 99.997791,388.63797 -11.711946,16.12011 -18.950327,-6.15733 0,-19.92556 18.950327,-6.15733 z"
inkscape:randomized="0"
inkscape:rounded="0"
inkscape:flatsided="true"
sodipodi:arg2="0.62831853"
sodipodi:arg1="0"
sodipodi:r2="13.712585"
sodipodi:r1="16.949688"
sodipodi:cy="388.63797"
sodipodi:cx="83.048103"
sodipodi:sides="5"
id="path4141"
style="fill:none;stroke:#000000;stroke-width:5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="star" />
<path
sodipodi:nodetypes="cc"
inkscape:connector-curvature="0"
id="path4143"
d="m 31.741795,745.02273 315.301265,-111.2"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cc"
inkscape:connector-curvature="0"
id="path4145"
d="M 30.3342,888.59742 345.63546,1004.0202"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="ccssccccc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path4165"
d="m 595.41266,378.78481 140.75949,38.00506 c 0,0 -8.44557,49.22222 -8.44557,71.9633 0,20.14574 0,124.39619 0,147.62151 0,30.96708 8.44557,78.82532 8.44557,78.82532 l -140.7595,33.78228 c 0,0 -17.24303,-61.90221 -17.24303,-92.90127 0.38411,-33.80191 1.75909,-154.79945 2.1114,-185.80253 -1.4076,-30.96709 15.13164,-91.49367 15.13164,-91.49367 z"
style="fill:#f0ffeb;fill-opacity:1;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="csccsc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path4175"
d="m 736.17216,416.78987 c 0,0 94.30885,5.63038 152.02025,5.63038 106.97721,0 201.28609,-5.63038 201.28609,-5.63038 m -1.4076,297.00254 c 0,0 -77.4177,-5.63039 -199.87849,-5.63039 -68.97215,0 -152.02026,7.03798 -152.02026,7.03798"
style="fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
ry="34.43626"
rx="31.189682"
y="729.20502"
x="528.6228"
height="181.57974"
width="106.97722"
id="rect4177"
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cccccccc"
inkscape:connector-curvature="0"
id="path4135-9"
d="M 345.77724,632.78476 H 51.589892 l -14.07595,102.75443 -7.03797,11.26076 v 142.16709 l 7.03797,14.07595 15.48355,101.34681 H 341.55445"
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="csscccssccc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path4203"
d="m 1086.6633,416.78987 c 0,0 -12.6684,47.85823 -12.6684,73.19494 0,25.33671 0,121.05316 0,146.38987 0,25.33671 12.6684,77.41773 12.6684,77.41773 l 205.5089,38.00505 108.3848,10e-6 c 0,0 14.0759,-81.64051 14.0759,-115.42279 0,-33.78227 0,-109.7924 0,-147.79746 0,-38.00507 -14.0759,-109.79241 -14.0759,-109.79241 h -108.3848 z"
style="fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="csscsccsc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path4205"
d="m 1097.9241,435.08861 c 0,0 -8.4456,40.82025 -8.4456,56.3038 0,15.48354 0,125.27594 0,144.98227 0,19.70633 7.038,57.7114 7.038,57.7114 0,0 94.3088,26.7443 123.8683,26.7443 29.5595,0 42.2279,0 42.2279,0 V 408.3443 c 0,0 -22.5216,0 -47.8583,0 -25.3367,0 -116.8303,26.74431 -116.8303,26.74431 z"
style="fill:#f0ffeb;fill-opacity:1;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path4207"
d="m 1292.1722,378.78481 c 0,0 30.967,40.82025 30.967,111.2 0,70.37975 0,81.64051 0,146.38987 0,64.74937 -30.967,116.83038 -30.967,116.83038"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path4209"
d="m 578.52152,489.98481 32.37468,-32.37468 v -40.82026 112.6076"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path4209-7"
d="m 323.81774,865.03792 32.37468,-32.37468 v -40.82026 112.6076"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path4209-76"
d="m 1005.0937,787.62021 -32.37469,-32.37468 v -40.82026 112.6076"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cssc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path4243"
d="m 595.41266,378.78481 c 0,0 -42.22785,78.82532 -42.22785,111.2 0,32.37468 0,105.56962 0,146.38987 0,40.82026 42.22785,114.01519 42.22785,114.01519"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
<path
style="fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m -106.79241,740.91638 1.4076,-91.49367 c 0,0 5.630382,-23.92911 -25.33671,-25.3367 -30.96709,-1.4076 -26.74431,2.81518 -26.74431,2.81518 l 1.40759,415.24051 c 0,0 9.85317,0 28.1519,0 28.151917,0 22.52153,-22.5216 22.52153,-22.5216 l -1e-5,-95.71637 c -12.67694,0 -11.26075,-9.61416 -11.26075,-16.89115 0,-16.94968 -10e-6,-136.5367 -10e-6,-149.20506 0.45035,-18.89009 9.85317,-16.89114 9.85317,-16.89114 z"
id="path4245"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccsccsccccc" />
<path
style="fill:none;stroke:#000000;stroke-width:2.20000005;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m -155.98734,646.16195 h 49.26582"
id="path4247"
inkscape:connector-curvature="0" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:2.20000005;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m -155.13671,1020.4303 h 49.26582"
id="path4247-2"
inkscape:connector-curvature="0" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:2.20000005;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m -156.69114,705.05816 h 49.26582"
id="path4247-1"
inkscape:connector-curvature="0" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:2.20000005;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m -153.87595,964.87082 h 49.26582"
id="path4247-0"
inkscape:connector-curvature="0" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M -147.09621,703.82272 V 964.22778"
id="path4281-1"
inkscape:connector-curvature="0" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M -137.24304,705.38727 V 965.79233"
id="path4281-8"
inkscape:connector-curvature="0" />
<g
id="g4428"
transform="translate(-13.779768,15.524026)">
<path
sodipodi:nodetypes="csssscccsssscsccscccsscccssc"
inkscape:connector-curvature="0"
id="path3070-9"
d="m 494.57145,641.6137 c 41.42857,15.71429 140,11.42857 191.42857,12.85714 51.42857,1.42857 160.4745,-10.23062 201.4286,-27.14286 40.9958,-16.92944 134.78428,-67.65586 151.42848,-72.85714 22.8572,-7.14286 41.4286,-7.14286 80,-20 38.5715,-12.85714 25.7143,-32.85714 25.7143,-32.85714 l -30,4.28571 -5.7562,-52.92008 c 0,0 37.1848,-1.36563 41.4705,-15.65135 4.2857,-14.28571 5.7143,-31.42857 -2.8571,-41.42857 -8.5715,-9.99997 -14.2857,1.42857 -18.5715,-12.85717 -4.2857,-14.2857 -2.8571,-28.5714 -27.1428,-27.1428 -24.2857,1.4285 -98.5715,0 -98.5715,0 0,0 -15.71418,108.5714 -98.57138,105.71426 -82.8571,-2.85715 -95.7143,-105.71426 -95.7143,-105.71426 H 308.85716 c 0,0 -5.71428,104.28569 -97.14286,105.71426 -91.42857,1.42857 -98.57142,-105.71426 -98.57142,-105.71426 H 47.428587 l -18.57143,38.5714 c -0.51524,0 -26.243277,10e-6 -21.428567,17.14285 4.65143,16.56149 -4.28571,41.42858 17.14286,41.42858 21.428567,0 47.142857,-1.42857 47.142857,-1.42857 l 15.71428,44.28571 -41.42857,2.85714 c 0,0 34.28571,24.28572 118.571433,32.85715 84.28571,8.57143 157.14286,8.57143 192.85714,31.42857 35.71429,22.85714 137.14286,78.57143 137.14286,78.57143 z"
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path3846-2"
d="M 28.857157,394.47084 H 121.71431"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path3850-6"
d="M 301.71431,394.47084 H 818.85712"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path3852-6"
d="M 991.71432,394.47084 H 1143.1429"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="csccc"
inkscape:connector-curvature="0"
id="path3884-4"
d="m 364.57145,547.32798 c 15.71429,18.57143 123.37372,81.52447 151.42857,88.57143 29.50156,7.41035 103.00398,7.14286 103.00398,7.14286 l -7.14285,-95.71429 z"
style="display:inline;fill:#f0ffeb;fill-opacity:1;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path3888-9"
d="M 404.57145,577.32798 V 548.75655"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0"
id="path3892-5"
d="M 644.69731,641.63467 640.41159,547.307 h 207.16383 c -11.3076,20.75266 -46.6124,74.9056 -72.8572,88.57143 -14.2857,10 -82.87805,4.32767 -130.02091,5.75624 z"
style="display:inline;fill:#f0ffeb;fill-opacity:1;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cscsc"
inkscape:connector-curvature="0"
id="path3896-0"
d="m 811.71432,634.47084 c 0,0 80,-84.28571 85.7143,-87.14286 5.7143,-2.85714 115.71428,-1.42857 115.71428,-1.42857 0,0 -77.14288,47.14286 -102.85718,58.57143 -25.7143,11.42857 -90,31.42857 -98.5714,30 z"
style="display:inline;fill:#f0ffeb;fill-opacity:1;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="csc"
inkscape:connector-curvature="0"
id="path3900-4"
d="m 345.63546,556.81008 c -14.07595,-30.96709 -18.29873,-71.78734 -18.29873,-94.30886 0,-22.52152 4.22279,-91.49368 22.52152,-105.56958"
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cccc"
inkscape:connector-curvature="0"
id="path3900-6-8"
d="m 632.78482,655.34173 c -1.40759,-5.63038 -4.04683,-81.90444 -6.51012,-106.93324 -3.67029,-18.7146 -4.98821,-51.2184 -6.15823,-87.3149 0,-22.5215 7.03798,-80.2329 12.66836,-105.5696"
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path3920-7"
d="m 103.52913,396.34426 c 0,0 15.48355,85.86329 106.97722,85.86329 91.49367,0 109.7924,-87.27089 109.7924,-87.27089"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path3920-5-1"
d="m 800.28863,394.93669 c 0,0 15.48355,85.8633 106.97722,85.8633 91.49364,0 109.79245,-87.2709 109.79245,-87.2709"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cc"
inkscape:connector-curvature="0"
id="path3940-7"
d="M 69.746857,451.24046 H 133.08862"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cssc"
inkscape:connector-curvature="0"
id="path3942-2"
d="m 779.17472,648.30376 c 18.2988,-12.66836 56.3038,-50.67343 92.9012,-104.16203 36.5975,-53.48861 8.4456,-59.11899 -18.2987,-74.60254 -26.7443,-15.48354 -49.2658,-42.22784 -67.5645,-112.60755"
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cc"
inkscape:connector-curvature="0"
id="path3944-7"
d="M 1112.7747,452.42529 H 983.27594"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path3946-2"
d="M 286.51648,452.64806 H 831.25572"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path3948-2"
d="m 808.73422,603.26071 v -54.8962"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
transform="scale(1,-1)"
rx="2.9330556"
ry="7.3789682"
y="-503.55417"
x="558.65344"
height="14.541238"
width="41.285542"
id="rect3950-6"
style="display:inline;fill:#e6e6e6;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
transform="scale(1,-1)"
rx="2.9330556"
ry="7.3789682"
y="-503.55417"
x="816.24329"
height="14.541238"
width="41.285542"
id="rect3950-4-1"
style="display:inline;fill:#e6e6e6;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
transform="scale(1,-1)"
ry="7.691968"
rx="1.6302098"
y="-502.1528"
x="259.53333"
height="11.738417"
width="18.776392"
id="rect4014-0"
style="display:inline;fill:#ffcb00;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
transform="translate(941.34179,284.00501)"
id="path4335"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cx="59.118988"
cy="211.28101"
r="16.89114" />
</g>
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M -126.57469,704.93158 V 965.33664"
id="path4281-8-0"
inkscape:connector-curvature="0" />
<path
style="fill:#ffffc0;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m -153.87595,992.4894 v -26.0405 h 22.52152 22.52152 v 26.0405 26.0405 h -22.52152 -22.52152 z"
id="path4361"
inkscape:connector-curvature="0" />
<path
style="fill:#ffffc0;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m -153.87595,675.78054 v -27.4481 h 22.52152 22.52152 v 27.4481 27.4481 h -22.52152 -22.52152 z"
id="path4363"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m -157.39494,624.37968 c 0,0 -4.22278,-12.66836 -18.29873,-12.66836 -14.07595,0 -14.07595,8.44557 -14.07595,8.44557 v 423.68601 c 0,0 1.40759,9.8532 14.07595,9.8532 12.66835,0 18.29873,-9.8532 18.29873,-9.8532"
id="path4377"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csccsc" />
<path
style="fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m -191.17722,624.37967 c 0,0 -35.18987,-1.40759 -35.18987,21.11393 0,22.52152 0,349.08354 0,371.605 0,22.5215 36.59747,25.3367 36.59747,25.3367"
id="path4381"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cssc" />
<rect
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4467"
width="16.891144"
height="35.189873"
x="-216.51393"
y="648.30878"
rx="7.7417746"
ry="6.2843671" />
<rect
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4467-6"
width="16.891144"
height="35.189873"
x="-216.51393"
y="985.57465"
rx="7.7417746"
ry="6.2843671" />
<rect
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4487"
width="30.967089"
height="377.23544"
x="-62.939251"
y="645.49365" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1200.9342,633.85815 v 73.19494 h 67.5645 v -83.0481 l -25.3367,-14.07595 z"
id="path4499"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccc" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1200.9342,963.23538 h 67.5645 v 78.82532 l -23.9291,14.0759 -43.6354,-18.2988 z"
id="path4501"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccc" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 1200.9342,692.97714 V 977.31132"
id="path4503"
inkscape:connector-curvature="0" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 1268.4987,693.56955 V 977.90373"
id="path4505"
inkscape:connector-curvature="0" />
<rect
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4534"
width="14.075921"
height="147.79749"
x="1216.5645"
y="759.50879" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 1224.1949,758.1012 V 704.61259"
id="path4536"
inkscape:connector-curvature="0" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 1224.1949,961.97968 V 908.49107"
id="path4536-0"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 1254.5696,707.42778 V 963.61006"
id="path4556"
inkscape:connector-curvature="0" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1268.757,624.97209 c 0,0 4.2228,-12.66836 18.2987,-12.66836 14.076,0 14.076,8.44557 14.076,8.44557 v 423.686 c 0,0 -1.4076,9.8532 -14.076,9.8532 -12.6683,0 -18.2987,-9.8532 -18.2987,-9.8532"
id="path4377-2"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csccsc" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1302.5393,624.97208 c 0,0 35.1898,-1.40759 35.1898,21.11393 0,22.52152 0,349.08354 0,371.60499 0,22.5215 -36.5974,25.3367 -36.5974,25.3367"
id="path4381-0"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cssc" />
<rect
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4467-5"
width="16.891144"
height="35.189873"
x="-1329.876"
y="648.90118"
rx="7.7417746"
ry="6.2843671"
transform="scale(-1,1)" />
<rect
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4467-6-5"
width="16.891144"
height="35.189873"
x="-1329.876"
y="986.16711"
rx="7.7417746"
ry="6.2843671"
transform="scale(-1,1)" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1199.6734,632.82525 67.5645,74.60253"
id="path4585"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1199.6734,708.02018 67.5645,-73.19493"
id="path4587"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1199.8962,962.97968 67.5645,74.60252"
id="path4585-9"
inkscape:connector-curvature="0" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1199.8962,1038.1746 67.5645,-73.19492"
id="path4587-0"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<circle
style="fill:#c8c8c8;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4610"
transform="translate(78.488602,1074.6683)"
cx="119.64557"
cy="202.83545"
r="76.010124" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4612"
transform="translate(94.675942,1204.8708)"
cx="103.45823"
cy="72.632912"
r="44.339241" />
<circle
style="fill:#c8c8c8;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4610-9"
transform="translate(78.488602,187.66068)"
cx="119.64557"
cy="202.83545"
r="76.010124" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4612-4"
transform="translate(94.675942,317.86322)"
cx="103.45823"
cy="72.632912"
r="44.339241" />
<circle
style="fill:#c8c8c8;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4610-5"
transform="translate(773.02535,187.66068)"
cx="119.64557"
cy="202.83545"
r="76.010124" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4612-1"
transform="translate(789.21269,317.86322)"
cx="103.45823"
cy="72.632912"
r="44.339241" />
<circle
style="fill:#c8c8c8;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4610-4"
transform="translate(773.02535,1074.6683)"
cx="119.64557"
cy="202.83545"
r="76.010124" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4612-3"
transform="translate(789.21269,1204.8708)"
cx="103.45823"
cy="72.632912"
r="44.339241" />
<path
style="fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1338.5088,829.07334 h 40.8203"
id="path3083"
inkscape:connector-curvature="0" />
<circle
style="fill:#3c3c3c;fill-opacity:1;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path3853"
transform="translate(1441.2633,600.30879)"
cx="-59.118988"
cy="229.57974"
r="4.222785" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 778.06325,845.37208 -38.7088,-38.70886"
id="path3855"
inkscape:connector-curvature="0" />
<g
id="g3952"
transform="translate(-13.779768,15.524026)">
<circle
transform="translate(-79.458203,449.80248)"
id="path3857"
style="fill:#3c3c3c;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
cx="-81.640511"
cy="188.75949"
r="4.222785" />
<circle
transform="translate(-79.458203,569.9172)"
id="path3857-2"
style="display:inline;fill:#3c3c3c;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
cx="-81.640511"
cy="188.75949"
r="4.222785" />
<circle
transform="translate(-79.458203,690.03203)"
id="path3857-3"
style="display:inline;fill:#3c3c3c;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
cx="-81.640511"
cy="188.75949"
r="4.222785" />
<circle
transform="translate(-79.458203,810.14679)"
id="path3857-7"
style="display:inline;fill:#3c3c3c;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
cx="-81.640511"
cy="188.75949"
r="4.222785" />
</g>
<g
id="g3946"
transform="translate(-13.779768,17.524026)">
<circle
transform="translate(1381.6254,448.24805)"
id="path3857-97"
style="display:inline;fill:#3c3c3c;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
cx="-81.640511"
cy="188.75949"
r="4.222785" />
<circle
transform="translate(1381.6254,568.36277)"
id="path3857-2-3"
style="display:inline;fill:#3c3c3c;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
cx="-81.640511"
cy="188.75949"
r="4.222785" />
<circle
transform="translate(1381.6254,688.4776)"
id="path3857-3-6"
style="display:inline;fill:#3c3c3c;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
cx="-81.640511"
cy="188.75949"
r="4.222785" />
<circle
transform="translate(1381.6254,808.59236)"
id="path3857-7-1"
style="display:inline;fill:#3c3c3c;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
cx="-81.640511"
cy="188.75949"
r="4.222785" />
</g>
</g>
<g
inkscape:groupmode="layer"
id="primary"
inkscape:label="primary">
<circle
style="fill:#a02c2c"
id="01"
cx="320.93164"
cy="-388.63797"
r="66.15696"
transform="scale(1,-1)"
inkscape:label="01" />
<circle
style="fill:#a02c2c"
id="02"
cx="320.93164"
cy="-757.42786"
r="66.15696"
transform="scale(1,-1)"
inkscape:label="01" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 43 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,144 @@
<svg id="svg166" version="1.1" viewBox="0 0 1668 1160" xmlns="http://www.w3.org/2000/svg">
<g id="g158" transform="translate(254 -254)">
<g id="g34" transform="translate(-13.78 3.524)" stroke="#000">
<path id="path10" d="m494.57 1006.9c41.429-15.714 140-11.427 191.43-12.857 51.429-1.429 160.48 10.23 201.43 27.143 40.995 16.93 134.78 67.656 151.43 72.857 22.857 7.143 41.429 7.143 80 20 38.572 12.857 25.714 32.857 25.714 32.857l-30-4.286-5.756 52.92s37.185 1.366 41.47 15.652c4.286 14.286 5.715 31.428-2.856 41.428-8.572 10-14.286-1.428-18.572 12.858-4.286 14.285-2.857 28.571-27.143 27.142-24.285-1.428-98.571 0-98.571 0s-15.714-108.57-98.573-105.71c-82.857 2.857-95.714 105.71-95.714 105.71h-500s-5.714-104.28-97.143-105.71c-91.428-1.428-98.571 105.71-98.571 105.71h-65.713l-18.572-38.571c-0.515 0-26.243 0-21.428-17.143 4.651-16.561-4.286-41.428 17.142-41.428 21.429 0 47.143 1.428 47.143 1.428l15.715-44.286-41.429-2.857s34.286-24.285 118.57-32.857c84.286-8.571 157.14-8.571 192.86-31.428 35.714-22.858 137.14-78.572 137.14-78.572z" fill="none" stroke-width="5"/>
<path id="path12" d="m28.857 1254h92.857m180 0h517.14m172.86 0h151.43" fill="none" stroke-width="2"/>
<path id="path14" d="m364.57 1101.1c15.715-18.572 123.37-81.525 151.43-88.572 29.502-7.41 103-7.142 103-7.142l-7.143 95.714z" fill="#f0ffeb" stroke-width="5"/>
<path id="path16" d="m404.57 1071.1v28.571" fill="none" stroke-width="2"/>
<path id="path18" d="m644.7 1006.8-4.285 94.328h207.16c-11.307-20.753-46.612-74.906-72.857-88.572-14.285-10-82.878-4.327-130.02-5.756zm167.02 7.164s80 84.286 85.715 87.143c5.714 2.857 115.71 1.428 115.71 1.428s-77.143-47.141-102.86-58.571c-25.715-11.429-90-31.429-98.572-30z" fill="#f0ffeb" stroke-width="5"/>
<g fill="none">
<path id="path20" d="m345.64 1091.7c-14.075 30.968-18.298 71.788-18.298 94.31 0 22.521 4.223 91.493 22.521 105.57m282.93-298.41c-1.408 5.63-4.047 81.903-6.51 106.93-3.67 18.715-4.989 51.219-6.159 87.315 0 22.522 7.038 80.233 12.669 105.57" stroke-width="5"/>
<path id="path22" d="m103.53 1252.1s15.483-85.864 106.98-85.864c91.494 0 109.79 87.271 109.79 87.271m479.99 0s15.483-85.863 106.98-85.863c91.493 0 109.79 87.27 109.79 87.27m-947.31-57.711h63.342" stroke-width="2"/>
<path id="path24" d="m779.18 1000.2c18.299 12.668 56.304 50.673 92.9 104.16 36.598 53.489 8.447 59.12-18.298 74.603-26.744 15.483-49.266 42.227-67.564 112.61" stroke-width="5"/>
<path id="path26" d="m1112.8 1196h-129.5m-696.76-0.222h544.74m-22.522-150.61v54.896" stroke-width="2"/>
</g>
<g stroke-width="2">
<rect id="rect28" x="558.65" y="1144.9" width="41.286" height="14.541" rx="2.933" ry="7.379" fill="#e6e6e6"/>
<rect id="rect30" x="816.24" y="1144.9" width="41.286" height="14.541" rx="2.933" ry="7.379" fill="#e6e6e6"/>
<rect id="rect32" x="259.53" y="1146.3" width="18.776" height="11.738" rx="1.63" ry="7.692" fill="#ffcb00"/>
</g>
</g>
<g id="g54" transform="translate(-13.78 15.524)" stroke="#000">
<path id="path36" d="m62.767 812.59-11.712 16.12-18.95-6.157v-19.925l18.95-6.158z" fill="none" stroke-width="5"/>
<path id="path38" d="m31.742 745.02 315.3-111.2m-316.71 254.77 315.3 115.42" fill="none" stroke-width="2"/>
<path id="path40" d="m341.41 632.78 140.76 38.005s-8.446 49.222-8.446 71.963v147.62c0 30.967 8.445 78.825 8.445 78.825l-140.76 33.782s-17.242-61.902-17.242-92.901l2.111-185.8c-1.408-30.967 15.132-91.493 15.132-91.493z" fill="#f0ffeb" stroke-width="5"/>
<g fill="none">
<path id="path42" d="m482.17 670.79s94.309 5.63 152.02 5.63c106.98 0 201.29-5.63 201.29-5.63m-1.408 297s-77.418-5.63-199.88-5.63c-68.972 0-152.02 7.038-152.02 7.038" stroke-width="5"/>
<rect id="rect44" x="528.62" y="729.2" width="106.98" height="181.58" rx="31.19" ry="34.436" stroke-width="2"/>
<path id="path46" d="m345.78 632.78h-294.19l-14.076 102.75-7.038 11.26v142.17l7.038 14.076 15.483 101.35h288.56m491.11-333.6s-12.668 47.858-12.668 73.195v146.39c0 25.336 12.668 77.417 12.668 77.417l205.51 38.005h108.38s14.076-81.64 14.076-115.42v-147.8c0-38.005-14.076-109.79-14.076-109.79h-108.38z" stroke-width="5"/>
</g>
<path id="path48" d="m843.92 689.09s-8.445 40.82-8.445 56.303v144.98c0 19.706 7.038 57.711 7.038 57.711s94.308 26.744 123.87 26.744h42.228v-312.49h-47.859c-25.336 0-116.83 26.745-116.83 26.745z" fill="#f0ffeb" stroke-width="5"/>
<path id="path50" d="m1038.2 632.78s30.967 40.82 30.967 111.2v146.39c0 64.749-30.967 116.83-30.967 116.83m-713.65-263.22 32.374-32.375v-40.82 112.61m-33.078 81.641 32.374-32.375v-40.82 112.61m648.9-116.83-32.375-32.374v-40.82 112.61" fill="none" stroke-width="2"/>
<path id="path52" d="m341.41 632.78s-42.228 78.825-42.228 111.2v146.39c0 40.82 42.228 114.02 42.228 114.02" fill="none" stroke-width="2"/>
</g>
<g fill="none" stroke="#000">
<path id="path56" d="m-106.79 740.92 1.407-91.493s5.63-23.93-25.337-25.337c-30.967-1.408-26.744 2.815-26.744 2.815l1.408 415.24h28.152c28.152 0 22.521-22.52 22.521-22.52v-95.717c-12.677 0-11.26-9.614-11.26-16.891v-149.2c0.45-18.89 9.853-16.892 9.853-16.892z" stroke-width="5"/>
<path id="path58" d="m-155.99 646.16h49.265m-48.415 374.27h49.266m-50.82-315.37h49.266m-46.451 259.81h49.266" stroke-width="2.2"/>
<path id="path60" d="m-147.1 703.82v260.4m9.853-258.84v260.4" stroke-width="2"/>
</g>
<g id="g88" transform="translate(-13.78 15.524)" stroke="#000">
<path id="path62" d="m494.57 641.61c41.429 15.714 140 11.428 191.43 12.856s160.48-10.23 201.43-27.143c40.995-16.93 134.78-67.656 151.43-72.857 22.857-7.143 41.429-7.143 80-20 38.572-12.857 25.714-32.857 25.714-32.857l-30 4.285-5.756-52.92s37.185-1.365 41.47-15.651c4.286-14.286 5.715-31.429-2.856-41.429-8.572-10-14.286 1.429-18.572-12.857-4.286-14.285-2.857-28.571-27.143-27.143-24.285 1.429-98.571 0-98.571 0s-15.714 108.57-98.572 105.72c-82.857-2.857-95.714-105.72-95.714-105.72h-500s-5.714 104.29-97.143 105.72c-91.428 1.428-98.571-105.72-98.571-105.72h-65.714l-18.572 38.572c-0.515 0-26.243 0-21.428 17.143 4.651 16.561-4.286 41.428 17.142 41.428 21.429 0 47.143-1.428 47.143-1.428l15.715 44.285-41.429 2.859s34.286 24.285 118.57 32.857c84.286 8.571 157.14 8.571 192.86 31.428 35.714 22.857 137.14 78.572 137.14 78.572z" fill="none" stroke-width="5"/>
<path id="path64" d="m28.857 394.47h92.857m180 0h517.14m172.86 0h151.43" fill="none" stroke-width="2"/>
<path id="path66" d="m364.57 547.33c15.715 18.571 123.37 81.524 151.43 88.571 29.502 7.41 103 7.143 103 7.143l-7.143-95.714z" fill="#f0ffeb" stroke-width="5"/>
<path id="path68" d="m404.57 577.33v-28.571" fill="none" stroke-width="2"/>
<path id="path70" d="m644.7 641.64-4.285-94.328h207.16c-11.307 20.753-46.612 74.906-72.857 88.571-14.285 10-82.878 4.328-130.02 5.757zm167.02-7.165s80-84.285 85.715-87.142c5.714-2.857 115.71-1.429 115.71-1.429s-77.143 47.143-102.86 58.572c-25.715 11.428-90 31.428-98.572 30z" fill="#f0ffeb" stroke-width="5"/>
<g fill="none">
<path id="path72" d="m345.64 556.81c-14.075-30.967-18.298-71.787-18.298-94.309 0-22.521 4.223-91.493 22.521-105.57m282.93 298.41c-1.408-5.63-4.047-81.905-6.51-106.93-3.67-18.714-4.989-51.218-6.159-87.314 0-22.522 7.038-80.233 12.669-105.57" stroke-width="5"/>
<path id="path74" d="m103.53 396.34s15.483 85.864 106.98 85.864c91.494 0 109.79-87.271 109.79-87.271m479.99 0s15.483 85.863 106.98 85.863c91.493 0 109.79-87.27 109.79-87.27m-947.31 57.71h63.342" stroke-width="2"/>
<path id="path76" d="m779.18 648.3c18.299-12.669 56.304-50.674 92.9-104.16 36.598-53.489 8.447-59.12-18.298-74.603-26.744-15.483-49.266-42.228-67.564-112.61" stroke-width="5"/>
<path id="path78" d="m1112.8 452.42h-129.5m-696.76 0.223h544.74m-22.522 150.61v-54.895" stroke-width="2"/>
</g>
<g stroke-width="2">
<rect id="rect80" transform="scale(1 -1)" x="558.65" y="-503.55" width="41.286" height="14.541" rx="2.933" ry="7.379" fill="#e6e6e6"/>
<rect id="rect82" transform="scale(1 -1)" x="816.24" y="-503.55" width="41.286" height="14.541" rx="2.933" ry="7.379" fill="#e6e6e6"/>
<rect id="rect84" transform="scale(1 -1)" x="259.53" y="-502.15" width="18.776" height="11.738" rx="1.63" ry="7.692" fill="#ffcb00"/>
<circle id="circle86" transform="translate(941.34 284)" cx="59.119" cy="211.28" r="16.891" fill="#fff"/>
</g>
</g>
<path id="path90" d="m-126.58 704.93v260.4" fill="none" stroke="#000" stroke-width="2"/>
<path id="path92" d="m-153.88 992.49v-26.041h45.043v52.08h-45.043zm0-316.71v-27.448h45.043v54.898h-45.043z" fill="#ffffc0"/>
<g fill="none" stroke="#000">
<path id="path94" d="m-157.4 624.38s-4.223-12.669-18.299-12.669-14.076 8.446-14.076 8.446v423.69s1.408 9.853 14.076 9.853c12.669 0 18.3-9.853 18.3-9.853" stroke-width="5"/>
<path id="path96" d="m-191.18 624.38s-35.19-1.408-35.19 21.114v371.6c0 22.521 36.597 25.336 36.597 25.336" stroke-width="5"/>
<g stroke-width="2">
<rect id="rect98" x="-216.51" y="648.31" width="16.891" height="35.19" rx="7.742" ry="6.284"/>
<rect id="rect100" x="-216.51" y="985.58" width="16.891" height="35.19" rx="7.742" ry="6.284"/>
<path id="path102" d="m-62.939 645.49h30.967v377.24h-30.967z"/>
</g>
<path id="path104" d="m1200.9 633.86v73.195h67.565v-83.048l-25.337-14.076zm0 329.38h67.565v78.826l-23.93 14.076-43.635-18.3zm0-270.26v284.33m67.565-283.74v284.33" stroke-width="5"/>
<path id="path106" d="m1216.6 759.51h14.076v147.8h-14.076zm7.631-1.408v-53.488m0 257.37v-53.49m30.375-201.06v256.18" stroke-width="2"/>
<path id="path108" d="m1268.8 624.97s4.223-12.668 18.299-12.668 14.076 8.445 14.076 8.445v423.69s-1.408 9.853-14.076 9.853c-12.669 0-18.299-9.853-18.299-9.853m33.783-419.46s35.19-1.408 35.19 21.114v371.6c0 22.521-36.598 25.337-36.598 25.337" stroke-width="5"/>
<g stroke-width="2">
<rect id="rect110" transform="scale(-1 1)" x="-1329.9" y="648.9" width="16.891" height="35.19" rx="7.742" ry="6.284"/>
<rect id="rect112" transform="scale(-1 1)" x="-1329.9" y="986.17" width="16.891" height="35.19" rx="7.742" ry="6.284"/>
<path id="path114" d="m1199.7 632.82 67.565 74.603m-67.565 0.592 67.565-73.195m-67.342 328.16 67.565 74.602m-67.565 0.593 67.565-73.195"/>
</g>
</g>
<g stroke="#000">
<g stroke-width="2">
<circle id="circle116" transform="translate(78.489 1074.7)" cx="119.65" cy="202.84" r="76.01" fill="#c8c8c8"/>
<circle id="circle118" transform="translate(94.676 1204.9)" cx="103.46" cy="72.633" r="44.339" fill="#fff"/>
<circle id="circle120" transform="translate(78.489 187.66)" cx="119.65" cy="202.84" r="76.01" fill="#c8c8c8"/>
<circle id="circle122" transform="translate(94.676 317.86)" cx="103.46" cy="72.633" r="44.339" fill="#fff"/>
<circle id="circle124" transform="translate(773.02 187.66)" cx="119.65" cy="202.84" r="76.01" fill="#c8c8c8"/>
<circle id="circle126" transform="translate(789.21 317.86)" cx="103.46" cy="72.633" r="44.339" fill="#fff"/>
<circle id="circle128" transform="translate(773.02 1074.7)" cx="119.65" cy="202.84" r="76.01" fill="#c8c8c8"/>
<circle id="circle130" transform="translate(789.21 1204.9)" cx="103.46" cy="72.633" r="44.339" fill="#fff"/>
</g>
<path id="path132" d="m1338.5 829.07h40.82" fill="none" stroke-width="5"/>
<circle id="circle134" transform="translate(1441.3 600.31)" cx="-59.119" cy="229.58" r="4.223" fill="#3c3c3c" stroke-width="5"/>
<g stroke-width="2">
<path id="path136" d="m778.06 845.37-38.709-38.709" fill="none"/>
<g id="g146" transform="translate(-13.78 15.524)" fill="#3c3c3c">
<circle id="circle138" transform="translate(-79.458 449.8)" cx="-81.641" cy="188.76" r="4.223"/>
<circle id="circle140" transform="translate(-79.458 569.92)" cx="-81.641" cy="188.76" r="4.223"/>
<circle id="circle142" transform="translate(-79.458 690.03)" cx="-81.641" cy="188.76" r="4.223"/>
<circle id="circle144" transform="translate(-79.458 810.15)" cx="-81.641" cy="188.76" r="4.223"/>
</g>
<g id="g156" transform="translate(-13.78 17.524)" fill="#3c3c3c">
<circle id="circle148" transform="translate(1381.6 448.25)" cx="-81.641" cy="188.76" r="4.223"/>
<circle id="circle150" transform="translate(1381.6 568.36)" cx="-81.641" cy="188.76" r="4.223"/>
<circle id="circle152" transform="translate(1381.6 688.48)" cx="-81.641" cy="188.76" r="4.223"/>
<circle id="circle154" transform="translate(1381.6 808.59)" cx="-81.641" cy="188.76" r="4.223"/>
</g>
</g>
</g>
</g>
<g id="layer2" fill="#d00000">
<circle id="p02" cx="503.65" cy="248.75" r="61.935" />
<circle id="p03" cx="863.41" cy="248.75" r="61.935"/>
<circle id="p04" cx="1181.5" cy="248.75" r="61.935"/>
<circle id="p05" cx="1378.4" cy="151.16" r="61.935"/>
<circle id="p06" cx="1535.1" cy="581.37" r="61.935"/>
<circle id="p07" cx="1378.4" cy="997.9" r="61.935"/>
<circle id="p08" cx="1181.5" cy="914.24" r="61.935"/>
<circle id="p09" transform="scale(1,-1)" cx="863.41" cy="-914.24" r="61.935"/>
<circle id="p10" cx="503.65" cy="914.24" r="61.935"/>
<circle id="p11" cx="297.77" cy="997.9" r="61.935"/>
<circle id="p12" cx="93.269" cy="581.37" r="61.935"/>
<circle id="p25" cx="424.31" cy="581.37" r="61.935"/>
<circle id="p27" cx="972.84" cy="581.37" r="61.935"/>
<circle id="p01" cx="297.77" cy="151.16" r="61.935"/>
<circle id="p26" cx="1339.4" cy="581.37" r="61.935"/>
</g>
<g id="g4994" fill="#ffef00">
<circle id="s02" cx="503.65" cy="248.75" r="61.935"/>
<circle id="s03" cx="863.41" cy="248.75" r="61.935"/>
<circle id="s04" cx="1181.5" cy="248.75" r="61.935"/>
<circle id="s05" cx="1378.4" cy="151.16" r="61.935"/>
<circle id="s06" cx="1535.1" cy="581.37" r="61.935"/>
<circle id="s07" cx="1378.4" cy="997.9" r="61.935"/>
<circle id="s08" cx="1181.5" cy="914.24" r="61.935"/>
<circle id="s09" transform="scale(1,-1)" cx="863.41" cy="-914.24" r="61.935"/>
<circle id="s10" cx="503.65" cy="914.24" r="61.935"/>
<circle id="s11" cx="297.77" cy="997.9" r="61.935"/>
<circle id="s12" cx="93.269" cy="581.37" r="61.935"/>
<circle id="s25" cx="424.31" cy="581.37" r="61.935"/>
<circle id="s27" cx="972.84" cy="581.37" r="61.935"/>
<circle id="s01" cx="297.77" cy="151.16" r="61.935"/>
<circle id="s26" cx="1339.4" cy="581.37" r="61.935"/>
</g>
<g id="layer3">
<text id="p15" opacity="0" x="382.62802" y="1034.3463" fill="#fd0000" font-family="sans-serif" font-size="1696.9px" letter-spacing="0px" stroke-width="17.676" word-spacing="0px" style="line-height:5.25" xml:space="preserve"><tspan id="tspan4997" x="382.62802" y="1034.3463" fill="#fd0000" stroke-width="17.676">x</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,37 @@
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setEmailOptions } from "../../redux/email/email.actions";
import T from "../../emails/parts-order/parts-order.email";
import { REPORT_QUERY_PARTS_ORDER_BY_PK } from "../../emails/parts-order/parts-order.query";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = dispatch => ({
setEmailOptions: e => dispatch(setEmailOptions(e))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(function Test({ setEmailOptions }) {
return (
<button
onClick={() =>
setEmailOptions({
messageOptions: {
from: { name: "Kavia Autobdoy", address: "noreply@bodyshop.app" },
to: "patrickwf@gmail.com",
replyTo: "snaptsoft@gmail.com"
},
template: T,
queryConfig: [
REPORT_QUERY_PARTS_ORDER_BY_PK,
{ variables: { id: "46f3aa34-c3bd-46c8-83fc-c93b7ce84f46" } }
]
})
}>
Set email config.
</button>
);
});

View File

@@ -2,10 +2,12 @@ import React from "react";
import ReactDOM from "react-dom";
import Alert from "./alert.component";
import { MockedProvider } from "@apollo/react-testing";
import { shallow } from "enzyme";
import { shallow, mount } from "enzyme";
const div = document.createElement("div");
it("renders without crashing", () => {
shallow(<Alert type="error" />);
const wrapper = mount(<Alert type="error" message="Test Error" />);
console.log("wrapper", wrapper);
// expect(wrapper.children()).to.have.lengthOf(1);
});

View File

@@ -0,0 +1,76 @@
import { Select, Button, Popover, InputNumber } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
export default connect(
mapStateToProps,
null
)(function AllocationsAssignmentComponent({
bodyshop,
handleAssignment,
assignment,
setAssignment,
visibilityState,
maxHours
}) {
const { t } = useTranslation();
const onChange = e => {
console.log("e", e);
setAssignment({ ...assignment, employeeid: e });
};
const [visibility, setVisibility] = visibilityState;
const popContent = (
<div>
<Select
showSearch
style={{ width: 200 }}
placeholder="Select a person"
optionFilterProp="children"
onChange={onChange}
filterOption={(input, option) =>
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
>
{bodyshop.employees.map(emp => (
<Select.Option value={emp.id} key={emp.id}>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select>
<InputNumber
defaultValue={assignment.hours}
placeholder={t("joblines.fields.mod_lb_hrs")}
max={parseFloat(maxHours)}
min={0}
onChange={e => setAssignment({ ...assignment, hours: e })}
/>
<Button
type="primary"
disabled={!assignment.employeeid}
onClick={handleAssignment}
>
Assign
</Button>
<Button onClick={() => setVisibility(false)}>Close</Button>
</div>
);
return (
<Popover content={popContent} visible={visibility}>
<Button onClick={() => setVisibility(true)}>
{t("allocations.actions.assign")}
</Button>
</Popover>
);
});

View File

@@ -0,0 +1,47 @@
import React, { useState } from "react";
import AllocationsAssignmentComponent from "./allocations-assignment.component";
import { useMutation } from "react-apollo";
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
import { useTranslation } from "react-i18next";
import { notification } from "antd";
export default function AllocationsAssignmentContainer({
jobLineId,
hours,
refetch
}) {
const visibilityState = useState(false);
const { t } = useTranslation();
const [assignment, setAssignment] = useState({
joblineid: jobLineId,
hours: parseFloat(hours),
employeeid: null
});
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
const handleAssignment = () => {
insertAllocation({ variables: { alloc: { ...assignment } } })
.then(r => {
notification["success"]({
message: t("allocations.successes.save")
});
visibilityState[1](false);
if (refetch) refetch();
})
.catch(error => {
notification["error"]({
message: t("employees.errors.saving", { message: error.message })
});
});
};
return (
<AllocationsAssignmentComponent
handleAssignment={handleAssignment}
maxHours={hours}
assignment={assignment}
setAssignment={setAssignment}
visibilityState={visibilityState}
/>
);
}

View File

@@ -0,0 +1,20 @@
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} />);
});
});

View File

@@ -0,0 +1,67 @@
import { Button, Popover, Select } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
export default connect(
mapStateToProps,
null
)(function AllocationsBulkAssignmentComponent({
disabled,
bodyshop,
handleAssignment,
assignment,
setAssignment,
visibilityState
}) {
const { t } = useTranslation();
const onChange = e => {
console.log("e", e);
setAssignment({ ...assignment, employeeid: e });
};
const [visibility, setVisibility] = visibilityState;
const popContent = (
<div>
<Select
showSearch
style={{ width: 200 }}
placeholder='Select a person'
optionFilterProp='children'
onChange={onChange}
filterOption={(input, option) =>
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}>
{bodyshop.employees.map(emp => (
<Select.Option value={emp.id} key={emp.id}>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select>
<Button
type='primary'
disabled={!assignment.employeeid}
onClick={handleAssignment}>
Assign
</Button>
<Button onClick={() => setVisibility(false)}>Close</Button>
</div>
);
return (
<Popover content={popContent} visible={visibility}>
<Button disabled={disabled} onClick={() => setVisibility(true)}>
{t("allocations.actions.assign")}
</Button>
</Popover>
);
});

View File

@@ -0,0 +1,47 @@
import React, { useState } from "react";
import AllocationsBulkAssignment from "./allocations-bulk-assignment.component";
import { useMutation } from "react-apollo";
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
import { useTranslation } from "react-i18next";
import { notification } from "antd";
export default function AllocationsBulkAssignmentContainer({
jobLines,
refetch
}) {
const visibilityState = useState(false);
const { t } = useTranslation();
const [assignment, setAssignment] = useState({
employeeid: null
});
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
const handleAssignment = () => {
const allocs = jobLines.reduce((acc, value) => {
acc.push({
joblineid: value.id,
hours: parseFloat(value.mod_lb_hrs) || 0,
employeeid: assignment.employeeid
});
return acc;
}, []);
insertAllocation({ variables: { alloc: allocs } }).then(r => {
notification["success"]({
message: t("employees.successes.save")
});
visibilityState[1](false);
if (refetch) refetch();
});
};
return (
<AllocationsBulkAssignment
disabled={jobLines.length > 0 ? false : true}
handleAssignment={handleAssignment}
assignment={assignment}
setAssignment={setAssignment}
visibilityState={visibilityState}
/>
);
}

View File

@@ -0,0 +1,19 @@
import { Icon } from "antd";
import React from "react";
import { MdRemoveCircleOutline } from "react-icons/md";
export default function AllocationsLabelComponent({ allocation, handleClick }) {
return (
<div style={{ display: "flex" }}>
<span>
{`${allocation.employee.first_name || ""} ${allocation.employee
.last_name || ""} (${allocation.hours || ""})`}
</span>
<Icon
style={{ color: "red", padding: "0px 4px" }}
component={MdRemoveCircleOutline}
onClick={handleClick}
/>
</div>
);
}

View File

@@ -0,0 +1,30 @@
import React from "react";
import { useMutation } from "react-apollo";
import { DELETE_ALLOCATION } from "../../graphql/allocations.queries";
import AllocationsLabelComponent from "./allocations-employee-label.component";
import { notification } from "antd";
import { useTranslation } from "react-i18next";
export default function AllocationsLabelContainer({ allocation, refetch }) {
const [deleteAllocation] = useMutation(DELETE_ALLOCATION);
const { t } = useTranslation();
const handleClick = e => {
e.preventDefault();
deleteAllocation({ variables: { id: allocation.id } })
.then(r => {
notification["success"]({
message: t("allocations.successes.deleted")
});
if (refetch) refetch();
})
.catch(error => {
notification["error"]({ message: t("allocations.errors.deleting") });
});
};
return (
<AllocationsLabelComponent
allocation={allocation}
handleClick={handleClick}
/>
);
}

View File

@@ -0,0 +1,22 @@
import { Tag, Popover } from "antd";
import React from "react";
import Barcode from "react-barcode";
import { useTranslation } from "react-i18next";
export default function BarcodePopupComponent({ value }) {
const { t } = useTranslation();
return (
<div>
<Popover
content={
<Barcode
value={value}
background="transparent"
displayValue={false}
/>
}
>
<Tag>{t("general.labels.barcode")}</Tag>
</Popover>
</div>
);
}

View File

@@ -0,0 +1,120 @@
import { Button, Card, Input, Icon } from "antd";
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
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(
"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,
toggleConversationVisible,
closeConversation
}) {
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 (
<div>
<Card
title={
conversation.open ? (
<div style={{ display: "flex" }}>
<div
onClick={() => toggleConversationVisible(conversation.phone)}
>
<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>
) : (
<div style={{ display: "flex" }}>
<div onClick={() => toggleConversationVisible(conversation.phone)}>
<PhoneFormatter>{conversation.phone}</PhoneFormatter>
</div>
<Button
type="dashed"
shape="circle-outline"
onClick={() => closeConversation(conversation.phone)}
>
X
</Button>
</div>
)}
</Card>
</div>
);
});

View File

@@ -0,0 +1,17 @@
import React from "react";
import ChatConversationComponent from "./chat-conversation.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = dispatch => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(function ChatConversationContainer({ conversation }) {
return <ChatConversationComponent conversation={conversation} />;
});

View File

@@ -0,0 +1,127 @@
.messages {
height: auto;
min-height: calc(100% - 93px);
max-height: calc(100% - 93px);
overflow-y: scroll;
overflow-x: hidden;
}
@media screen and (max-width: 735px) {
.messages {
max-height: calc(100% - 105px);
}
}
.messages::-webkit-scrollbar {
width: 8px;
background: transparent;
}
.messages::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.3);
}
.messages ul li {
display: inline-block;
clear: both;
//float: left;
margin: 5px 15px 5px 15px;
width: calc(100% - 25px);
font-size: 0.9em;
}
.messages ul li:nth-last-child(1) {
margin-bottom: 20px;
}
.messages ul li.sent img {
margin: 6px 8px 0 0;
}
.messages ul li.sent p {
background: #435f7a;
color: #f5f5f5;
}
.messages ul li.replies img {
float: right;
margin: 6px 0 0 8px;
}
.messages ul li.replies p {
background: #f5f5f5;
float: right;
}
.messages ul li img {
width: 22px;
border-radius: 50%;
float: left;
}
.messages ul li p {
display: inline-block;
padding: 10px 15px;
border-radius: 20px;
max-width: 205px;
line-height: 130%;
}
@media screen and (min-width: 735px) {
.messages ul li p {
max-width: 300px;
}
}
.message-input {
position: absolute;
bottom: 0;
width: 100%;
z-index: 99;
}
.message-input .wrap {
position: relative;
}
.message-input .wrap input {
font-family: "proxima-nova", "Source Sans Pro", sans-serif;
float: left;
border: none;
width: calc(100% - 90px);
padding: 11px 32px 10px 8px;
font-size: 0.8em;
color: #32465a;
}
@media screen and (max-width: 735px) {
.message-input .wrap input {
padding: 15px 32px 16px 8px;
}
}
.message-input .wrap input:focus {
outline: none;
}
.message-input .wrap .attachment {
position: absolute;
right: 60px;
z-index: 4;
margin-top: 10px;
font-size: 1.1em;
color: #435f7a;
opacity: 0.5;
cursor: pointer;
}
@media screen and (max-width: 735px) {
.message-input .wrap .attachment {
margin-top: 17px;
right: 65px;
}
}
.message-input .wrap .attachment:hover {
opacity: 1;
}
.message-input .wrap button {
float: right;
border: none;
width: 50px;
padding: 12px 0;
cursor: pointer;
background: #32465a;
color: #f5f5f5;
}
@media screen and (max-width: 735px) {
.message-input .wrap button {
padding: 16px 0;
}
}
.message-input .wrap button:hover {
background: #435f7a;
}
.message-input .wrap button:focus {
outline: none;
}

View File

@@ -0,0 +1,23 @@
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { openConversation } from "../../redux/messaging/messaging.actions";
import { Icon } from "antd";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = dispatch => ({
openConversation: phone => dispatch(openConversation(phone))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(function ChatOpenButton({ openConversation, phone }) {
return (
<Icon
style={{ margin: 4 }}
type="message"
onClick={() => openConversation(phone)}
/>
);
});

View File

@@ -0,0 +1,36 @@
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>
);
}

View File

@@ -0,0 +1,48 @@
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>
);
});

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,39 @@
import React from "react";
import { Input } from "antd";
import CKEditor from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
export default function SendEmailButtonComponent({
messageOptions,
handleConfigChange,
handleHtmlChange
}) {
return (
<div>
<Input
defaultValue={messageOptions.to}
onChange={handleConfigChange}
name='to'
/>
CC
<Input
defaultValue={messageOptions.cc}
onChange={handleConfigChange}
name='cc'
/>
Subject
<Input
defaultValue={messageOptions.subject}
onChange={handleConfigChange}
name='subject'
/>
<CKEditor
editor={ClassicEditor}
data={messageOptions.html}
onChange={(event, editor) => {
handleHtmlChange(editor.getData());
}}
/>
</div>
);
}

View File

@@ -0,0 +1,104 @@
import { Button, Modal, notification } from "antd";
import axios from "axios";
import React, { useEffect, useState } from "react";
import { useLazyQuery } from "react-apollo";
import ReactDOMServer from "react-dom/server";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { toggleEmailOverlayVisible } from "../../redux/email/email.actions";
import { selectEmailConfig, selectEmailVisible } from "../../redux/email/email.selectors.js";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import EmailOverlayComponent from "./email-overlay.component";
const mapStateToProps = createStructuredSelector({
modalVisible: selectEmailVisible,
emailConfig: selectEmailConfig
});
const mapDispatchToProps = dispatch => ({
toggleEmailOverlayVisible: () => dispatch(toggleEmailOverlayVisible())
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(function SendEmail({ emailConfig, modalVisible, toggleEmailOverlayVisible }) {
const { t } = useTranslation();
const [messageOptions, setMessageOptions] = useState(
emailConfig.messageOptions
);
useEffect(() => {
setMessageOptions(emailConfig.messageOptions);
}, [setMessageOptions, emailConfig.messageOptions]);
const [executeQuery, { called, loading, data }] = useLazyQuery(
emailConfig.queryConfig[0],
{
...emailConfig.queryConfig[1],
fetchPolicy: "network-only"
}
);
if (
emailConfig.queryConfig[0] &&
emailConfig.queryConfig[1] &&
modalVisible &&
!called
) {
executeQuery();
}
if (data && !messageOptions.html && emailConfig.template) {
setMessageOptions({
...messageOptions,
html: ReactDOMServer.renderToStaticMarkup(
<emailConfig.template data={data} />
)
//html: renderEmail(<emailConfig.template data={data} />)
});
}
const handleOk = () => {
//sendEmail(messageOptions);
axios
.post("/sendemail", messageOptions)
.then(response => {
console.log(JSON.stringify(response));
notification["success"]({ message: t("emails.successes.sent") });
toggleEmailOverlayVisible();
})
.catch(error => {
console.log(JSON.stringify(error));
notification["error"]({
message: t("emails.errors.notsent", { message: error.message })
});
});
};
const handleConfigChange = event => {
const { name, value } = event.target;
setMessageOptions({ ...messageOptions, [name]: value });
};
const handleHtmlChange = text => {
setMessageOptions({ ...messageOptions, html: text });
};
return (
<div>
<Modal
destroyOnClose={true}
visible={modalVisible}
width={"80%"}
onOk={handleOk}
onCancel={() => toggleEmailOverlayVisible()}>
<LoadingSpinner loading={loading}>
<EmailOverlayComponent
handleConfigChange={handleConfigChange}
messageOptions={messageOptions}
handleHtmlChange={handleHtmlChange}
/>
</LoadingSpinner>
</Modal>
<Button onClick={() => toggleEmailOverlayVisible()}>Show</Button>
</div>
);
});

View File

@@ -4,7 +4,7 @@ import React from "react";
export default function FooterComponent() {
return (
<Row>
<Col span={8} offset={9}>
<Col span={8} offset={8}>
Copyright Snapt Software 2019. All rights reserved.
</Col>
</Row>

View File

@@ -5,9 +5,13 @@ function FormItemEmail(props, ref) {
<Input
{...props}
addonAfter={
<a href={`mailto:${props.email}`}>
props.email ? (
<a href={`mailto:${props.email}`}>
<Icon type="mail" />
</a>
) : (
<Icon type="mail" />
</a>
)
}
/>
);

View File

@@ -0,0 +1,21 @@
import { Button } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import AlertComponent from "../alert/alert.component";
export default function ResetForm({ resetFields }) {
const { t } = useTranslation();
return (
<AlertComponent
message={
<div>
{t("general.messages.unsavedchanges")}
<Button style={{ marginLeft: "20px" }} onClick={() => resetFields()}>
{t("general.actions.reset")}
</Button>
</div>
}
closable
/>
);
}

View File

@@ -1,79 +0,0 @@
import React from "react";
// import { Icon, Button, Input, AutoComplete } from "antd";
// const { Option } = AutoComplete;
// function onSelect(value) {
// console.log("onSelect", value);
// }
// function getRandomInt(max, min = 0) {
// return Math.floor(Math.random() * (max - min + 1)) + min; // eslint-disable-line no-mixed-operators
// }
// function searchResult(query) {
// return new Array(getRandomInt(5))
// .join(".")
// .split(".")
// .map((item, idx) => ({
// query,
// category: `${query}${idx}`,
// count: getRandomInt(200, 100)
// }));
// }
// function renderOption(item) {
// return (
// <Option key={item.category} text={item.category}>
// <div className='global-search-item'>
// <span className='global-search-item-desc'>
// Found {item.query} on
// <a
// href={`https://s.taobao.com/search?q=${item.query}`}
// target='_blank'
// rel='noopener noreferrer'>
// {item.category}
// </a>
// </span>
// <span className='global-search-item-count'>{item.count} results</span>
// </div>
// </Option>
// );
// }
export default class GlobalSearch extends React.Component {
state = {
dataSource: []
};
// handleSearch = value => {
// this.setState({
// dataSource: value ? searchResult(value) : []
// });
// };
render() {
return (
<div />
// <div style={{ width: 300 }}>
// <AutoComplete
// size="large"
// style={{ width: "100%" }}
// dataSource={dataSource.map(renderOption)}
// onSelect={onSelect}
// onSearch={this.handleSearch}
// placeholder="input here"
// optionLabelProp="text"
// >
// <Input
// suffix={
// <Button style={{ marginRight: -12 }} size="large" type="primary">
// <Icon type="search" />
// </Button>
// }
// />
// </AutoComplete>
// </div>
);
}
}

View File

@@ -1,73 +1,181 @@
import { useApolloClient } from "@apollo/react-hooks";
import { Col, Icon, Menu, Row } from "antd";
import { Avatar, Col, Icon, Menu, Row } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import CurrentUserDropdown from "../current-user-dropdown/current-user-dropdown.component";
import GlobalSearch from "../global-search/global-search.component";
import 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 "./header.styles.scss";
export default ({ landingHeader, navItems, selectedNavItem }) => {
const apolloClient = useApolloClient();
export default ({
landingHeader,
selectedNavItem,
logo,
handleMenuClick,
currentUser
}) => {
const { t } = useTranslation();
const handleClick = e => {
apolloClient.writeData({ data: { selectedNavItem: e.key } });
};
//TODO Add
return (
<Row type='flex' justify='space-around'>
<Col span={16}>
<Menu
theme='dark'
className='header'
onClick={handleClick}
selectedKeys={selectedNavItem}
mode='horizontal'>
<Menu.Item>
<GlobalSearch />
</Menu.Item>
<Row type="flex" justify="space-around" align="middle">
{logo ? (
<Col span={3}>
<img alt="Shop Logo" src={logo} style={{ height: "40px" }} />
</Col>
) : null}
<Col span={14}>
{landingHeader ? (
<Menu
theme="dark"
className="header"
selectedKeys={selectedNavItem}
mode="horizontal"
onClick={handleMenuClick}
>
<ManageSignInButton />
<Menu.Item key='home'>
<Link to='/manage'>
<Icon type='home' />
{t("menus.header.home")}
</Link>
</Menu.Item>
<Menu.SubMenu title={t("menus.header.jobs")}>
<Menu.Item key='jobs'>
<Link to='/manage/jobs'>
<Icon type='home' />
{t("menus.header.activejobs")}
<Menu.SubMenu
title={
<div>
<Avatar
size="medium"
alt="Avatar"
src={
currentUser.photoURL ? currentUser.photoURL : UserImage
}
style={{ margin: "10px" }}
/>
{currentUser.displayName || t("general.labels.unknown")}
</div>
}
>
<Menu.Item onClick={signOutStart()}>
{t("user.actions.signout")}
</Menu.Item>
<Menu.Item>
<Link to="/manage/profile">
{t("menus.currentuser.profile")}
</Link>
</Menu.Item>
<Menu.SubMenu
title={
<span>
<Icon type="global" />
<span>{t("menus.currentuser.languageselector")}</span>
</span>
}
>
<Menu.Item actiontype="lang-select" key="en_us">
{t("general.languages.english")}
</Menu.Item>
<Menu.Item actiontype="lang-select" key="fr">
{t("general.languages.french")}
</Menu.Item>
<Menu.Item actiontype="lang-select" key="es">
{t("general.languages.spanish")}
</Menu.Item>
</Menu.SubMenu>
</Menu.SubMenu>
</Menu>
) : (
<Menu
theme="dark"
className="header"
selectedKeys={selectedNavItem}
mode="horizontal"
onClick={handleMenuClick}
>
<Menu.Item key="home">
<Link to="/manage">
<Icon type="home" />
{t("menus.header.home")}
</Link>
</Menu.Item>
<Menu.Item key='availablejobs'>
<Link to='/manage/available'>
<Icon type='home' />
{t("menus.header.availablejobs")}
</Link>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu title={t("menus.header.jobs")}>
<Menu.Item key="schedule">
<Link to="/manage/schedule">
<Icon type="calendar" />
{t("menus.header.schedule")}
</Link>
</Menu.Item>
<Menu.Item key="activejobs">
<Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
</Menu.Item>
<Menu.Item key="availablejobs">
<Link to="/manage/available">
{t("menus.header.availablejobs")}
</Link>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu title={t("menus.header.customers")}>
<Menu.Item key="owners">
<Link to="/manage/owners">
<Icon type="team" />
{t("menus.header.owners")}
</Link>
</Menu.Item>
<Menu.Item key="vehicles">
<Link to="/manage/vehicles">
<Icon type="car" />
{t("menus.header.vehicles")}
</Link>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu title={t("menus.header.shop")}>
<Menu.Item key="shop">
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
</Menu.Item>
<Menu.Item key="shop-vendors">
<Link to="/manage/shop/vendors">
{t("menus.header.shop_vendors")}
</Link>
</Menu.Item>
</Menu.SubMenu>
{
// navItems.map(navItem => (
// <Menu.Item key={navItem.title}>
// <Link to={navItem.path}>
// {navItem.icontype ? <Icon type={navItem.icontype} /> : null}
// {navItem.title}
// </Link>
// </Menu.Item>
// ))
}
{!landingHeader ? null : (
<Menu.Item>
<ManageSignInButton />
</Menu.Item>
)}
</Menu>
</Col>
<Col span={6} offset={2}>
{!landingHeader ? <CurrentUserDropdown /> : null}
<Menu.SubMenu
title={
<div>
<Avatar
size="medium"
alt="Avatar"
src={
currentUser.photoURL ? currentUser.photoURL : UserImage
}
style={{ margin: "10px" }}
/>
{currentUser.displayName || t("general.labels.unknown")}
</div>
}
>
<Menu.Item onClick={signOutStart()}>
{t("user.actions.signout")}
</Menu.Item>
<Menu.Item>
<Link to="/manage/profile">
{t("menus.currentuser.profile")}
</Link>
</Menu.Item>
<Menu.SubMenu
title={
<span>
<Icon type="global" />
<span>{t("menus.currentuser.languageselector")}</span>
</span>
}
>
<Menu.Item actiontype="lang-select" key="en_us">
{t("general.languages.english")}
</Menu.Item>
<Menu.Item actiontype="lang-select" key="fr">
{t("general.languages.french")}
</Menu.Item>
<Menu.Item actiontype="lang-select" key="es">
{t("general.languages.spanish")}
</Menu.Item>
</Menu.SubMenu>
</Menu.SubMenu>
</Menu>
)}
</Col>
</Row>
);

View File

@@ -1,46 +1,52 @@
import React from "react";
import "./header.styles.scss";
import { useQuery } from "react-apollo";
// //import {
// GET_LANDING_NAV_ITEMS,
// GET_NAV_ITEMS
// } from "../../graphql/metadata.queries";
import { GET_CURRENT_SELECTED_NAV_ITEM } from "../../graphql/local.queries";
//import LoadingSpinner from "../loading-spinner/loading-spinner.component";
//import AlertComponent from "../alert/alert.component";
import HeaderComponent from "./header.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import i18next from "i18next";
import { setUserLanguage, signOutStart } from "../../redux/user/user.actions";
import {
selectCurrentUser,
selectBodyshop
} from "../../redux/user/user.selectors";
export default ({ landingHeader, signedIn }) => {
const hookSelectedNavItem = useQuery(GET_CURRENT_SELECTED_NAV_ITEM);
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
bodyshop: selectBodyshop
});
// let hookNavItems;
// if (landingHeader) {
// hookNavItems = useQuery(GET_LANDING_NAV_ITEMS, {
// fetchPolicy: "network-only"
// });
// } else {
// hookNavItems = useQuery(GET_NAV_ITEMS, {
// fetchPolicy: "network-only"
// });
// }
const mapDispatchToProps = dispatch => ({
signOutStart: () => dispatch(signOutStart()),
setUserLanguage: language => dispatch(setUserLanguage(language))
});
// if (hookNavItems.loading || hookSelectedNavItem.loading)
// return <LoadingSpinner />;
// if (hookNavItems.error)
// return <AlertComponent message={hookNavItems.error.message} />;
// if (hookSelectedNavItem.error)
// return console.log(
// "Unable to load Selected Navigation Item.",
// hookSelectedNavItem.error
// );
const { selectedNavItem } = hookSelectedNavItem.data;
// const navItems = JSON.parse(hookNavItems.data.masterdata_by_pk.value);
export default connect(
mapStateToProps,
mapDispatchToProps
)(function HeaderContainer({
landingHeader,
currentUser,
bodyshop,
signOutStart,
setUserLanguage
}) {
const handleMenuClick = e => {
if (e.item.props.actiontype === "lang-select") {
i18next.changeLanguage(e.key, (err, t) => {
if (err)
return console.log("Error encountered when changing languages.", err);
setUserLanguage(e.key);
});
}
};
return (
<HeaderComponent
handleMenuClick={handleMenuClick}
signOutStart={signOutStart}
landingHeader={landingHeader}
selectedNavItem={selectedNavItem}
selectedNavItem={null}
currentUser={currentUser}
logo={bodyshop ? bodyshop.logo_img_path : null}
/>
);
};
});

View File

@@ -1,4 +0,0 @@
.header{
text-align: center;
width: 100%;
}

View File

@@ -0,0 +1,71 @@
import { Modal, Form, Input, InputNumber } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import ResetForm from "../form-items-formatted/reset-form-item.component";
export default function InvoiceEnterModalComponent({
visible,
invoice,
handleCancel,
handleSubmit,
form
}) {
const { t } = useTranslation();
const { getFieldDecorator, isFieldsTouched, resetFields } = form;
return (
<Modal
title={
invoice && invoice.id
? t("invoice.labels.edit")
: t("invoice.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>
</Modal>
);
}

View File

@@ -0,0 +1,114 @@
import { Form, notification } from "antd";
import React from "react";
import { useMutation } from "react-apollo";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
INSERT_NEW_JOB_LINE,
UPDATE_JOB_LINE
} from "../../graphql/jobs-lines.queries";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectInvoiceEnterModal } from "../../redux/modals/modals.selectors";
import InvoiceEnterModalComponent from "./invoice-enter-modal.component";
const mapStateToProps = createStructuredSelector({
invoiceEnterModal: selectInvoiceEnterModal
});
const mapDispatchToProps = dispatch => ({
toggleModalVisible: () => dispatch(toggleModalVisible("invoiceEnter"))
});
function InvoiceEnterModalContainer({
invoiceEnterModal,
toggleModalVisible,
form
}) {
const { t } = useTranslation();
// const [insertJobLine] = useMutation(INSERT_NEW_JOB_LINE);
// const [updateJobLine] = useMutation(UPDATE_JOB_LINE);
const handleSubmit = e => {
e.preventDefault();
form.validateFieldsAndScroll((err, values) => {
if (err) {
notification["error"]({
message: t("invoices.errors.validation"),
description: err.message
});
}
if (!err) {
alert("Closing this modal.");
toggleModalVisible();
// 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)
// jobLineEditModal.actions.refetch();
// toggleModalVisible();
// }
}
});
};
const handleCancel = () => {
toggleModalVisible();
};
return (
<InvoiceEnterModalComponent
visible={invoiceEnterModal.visible}
invoice={invoiceEnterModal.context}
handleSubmit={handleSubmit}
handleCancel={handleCancel}
form={form}
/>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(
Form.create({ name: "InvoiceEnterModalContainer" })(
InvoiceEnterModalContainer
)
);

View File

@@ -0,0 +1,735 @@
import React from "react";
export default ({ dmg1, dmg2 }) => (
<svg
id='svg166'
version='1.1'
viewBox='0 0 1668 1160'
xmlns='http://www.w3.org/2000/svg'>
<g id='g158' transform='translate(254 -254)'>
<g id='g34' transform='translate(-13.78 3.524)' stroke='#000'>
<path
id='path10'
d='m494.57 1006.9c41.429-15.714 140-11.427 191.43-12.857 51.429-1.429 160.48 10.23 201.43 27.143 40.995 16.93 134.78 67.656 151.43 72.857 22.857 7.143 41.429 7.143 80 20 38.572 12.857 25.714 32.857 25.714 32.857l-30-4.286-5.756 52.92s37.185 1.366 41.47 15.652c4.286 14.286 5.715 31.428-2.856 41.428-8.572 10-14.286-1.428-18.572 12.858-4.286 14.285-2.857 28.571-27.143 27.142-24.285-1.428-98.571 0-98.571 0s-15.714-108.57-98.573-105.71c-82.857 2.857-95.714 105.71-95.714 105.71h-500s-5.714-104.28-97.143-105.71c-91.428-1.428-98.571 105.71-98.571 105.71h-65.713l-18.572-38.571c-0.515 0-26.243 0-21.428-17.143 4.651-16.561-4.286-41.428 17.142-41.428 21.429 0 47.143 1.428 47.143 1.428l15.715-44.286-41.429-2.857s34.286-24.285 118.57-32.857c84.286-8.571 157.14-8.571 192.86-31.428 35.714-22.858 137.14-78.572 137.14-78.572z'
fill='none'
strokeWidth='5'
/>
<path
id='path12'
d='m28.857 1254h92.857m180 0h517.14m172.86 0h151.43'
fill='none'
strokeWidth='2'
/>
<path
id='path14'
d='m364.57 1101.1c15.715-18.572 123.37-81.525 151.43-88.572 29.502-7.41 103-7.142 103-7.142l-7.143 95.714z'
fill='#f0ffeb'
strokeWidth='5'
/>
<path
id='path16'
d='m404.57 1071.1v28.571'
fill='none'
strokeWidth='2'
/>
<path
id='path18'
d='m644.7 1006.8-4.285 94.328h207.16c-11.307-20.753-46.612-74.906-72.857-88.572-14.285-10-82.878-4.327-130.02-5.756zm167.02 7.164s80 84.286 85.715 87.143c5.714 2.857 115.71 1.428 115.71 1.428s-77.143-47.141-102.86-58.571c-25.715-11.429-90-31.429-98.572-30z'
fill='#f0ffeb'
strokeWidth='5'
/>
<g fill='none'>
<path
id='path20'
d='m345.64 1091.7c-14.075 30.968-18.298 71.788-18.298 94.31 0 22.521 4.223 91.493 22.521 105.57m282.93-298.41c-1.408 5.63-4.047 81.903-6.51 106.93-3.67 18.715-4.989 51.219-6.159 87.315 0 22.522 7.038 80.233 12.669 105.57'
strokeWidth='5'
/>
<path
id='path22'
d='m103.53 1252.1s15.483-85.864 106.98-85.864c91.494 0 109.79 87.271 109.79 87.271m479.99 0s15.483-85.863 106.98-85.863c91.493 0 109.79 87.27 109.79 87.27m-947.31-57.711h63.342'
strokeWidth='2'
/>
<path
id='path24'
d='m779.18 1000.2c18.299 12.668 56.304 50.673 92.9 104.16 36.598 53.489 8.447 59.12-18.298 74.603-26.744 15.483-49.266 42.227-67.564 112.61'
strokeWidth='5'
/>
<path
id='path26'
d='m1112.8 1196h-129.5m-696.76-0.222h544.74m-22.522-150.61v54.896'
strokeWidth='2'
/>
</g>
<g strokeWidth='2'>
<rect
id='rect28'
x='558.65'
y='1144.9'
width='41.286'
height='14.541'
rx='2.933'
ry='7.379'
fill='#e6e6e6'
/>
<rect
id='rect30'
x='816.24'
y='1144.9'
width='41.286'
height='14.541'
rx='2.933'
ry='7.379'
fill='#e6e6e6'
/>
<rect
id='rect32'
x='259.53'
y='1146.3'
width='18.776'
height='11.738'
rx='1.63'
ry='7.692'
fill='#ffcb00'
/>
</g>
</g>
<g id='g54' transform='translate(-13.78 15.524)' stroke='#000'>
<path
id='path36'
d='m62.767 812.59-11.712 16.12-18.95-6.157v-19.925l18.95-6.158z'
fill='none'
strokeWidth='5'
/>
<path
id='path38'
d='m31.742 745.02 315.3-111.2m-316.71 254.77 315.3 115.42'
fill='none'
strokeWidth='2'
/>
<path
id='path40'
d='m341.41 632.78 140.76 38.005s-8.446 49.222-8.446 71.963v147.62c0 30.967 8.445 78.825 8.445 78.825l-140.76 33.782s-17.242-61.902-17.242-92.901l2.111-185.8c-1.408-30.967 15.132-91.493 15.132-91.493z'
fill='#f0ffeb'
strokeWidth='5'
/>
<g fill='none'>
<path
id='path42'
d='m482.17 670.79s94.309 5.63 152.02 5.63c106.98 0 201.29-5.63 201.29-5.63m-1.408 297s-77.418-5.63-199.88-5.63c-68.972 0-152.02 7.038-152.02 7.038'
strokeWidth='5'
/>
<rect
id='rect44'
x='528.62'
y='729.2'
width='106.98'
height='181.58'
rx='31.19'
ry='34.436'
strokeWidth='2'
/>
<path
id='path46'
d='m345.78 632.78h-294.19l-14.076 102.75-7.038 11.26v142.17l7.038 14.076 15.483 101.35h288.56m491.11-333.6s-12.668 47.858-12.668 73.195v146.39c0 25.336 12.668 77.417 12.668 77.417l205.51 38.005h108.38s14.076-81.64 14.076-115.42v-147.8c0-38.005-14.076-109.79-14.076-109.79h-108.38z'
strokeWidth='5'
/>
</g>
<path
id='path48'
d='m843.92 689.09s-8.445 40.82-8.445 56.303v144.98c0 19.706 7.038 57.711 7.038 57.711s94.308 26.744 123.87 26.744h42.228v-312.49h-47.859c-25.336 0-116.83 26.745-116.83 26.745z'
fill='#f0ffeb'
strokeWidth='5'
/>
<path
id='path50'
d='m1038.2 632.78s30.967 40.82 30.967 111.2v146.39c0 64.749-30.967 116.83-30.967 116.83m-713.65-263.22 32.374-32.375v-40.82 112.61m-33.078 81.641 32.374-32.375v-40.82 112.61m648.9-116.83-32.375-32.374v-40.82 112.61'
fill='none'
strokeWidth='2'
/>
<path
id='path52'
d='m341.41 632.78s-42.228 78.825-42.228 111.2v146.39c0 40.82 42.228 114.02 42.228 114.02'
fill='none'
strokeWidth='2'
/>
</g>
<g fill='none' stroke='#000'>
<path
id='path56'
d='m-106.79 740.92 1.407-91.493s5.63-23.93-25.337-25.337c-30.967-1.408-26.744 2.815-26.744 2.815l1.408 415.24h28.152c28.152 0 22.521-22.52 22.521-22.52v-95.717c-12.677 0-11.26-9.614-11.26-16.891v-149.2c0.45-18.89 9.853-16.892 9.853-16.892z'
strokeWidth='5'
/>
<path
id='path58'
d='m-155.99 646.16h49.265m-48.415 374.27h49.266m-50.82-315.37h49.266m-46.451 259.81h49.266'
strokeWidth='2.2'
/>
<path
id='path60'
d='m-147.1 703.82v260.4m9.853-258.84v260.4'
strokeWidth='2'
/>
</g>
<g id='g88' transform='translate(-13.78 15.524)' stroke='#000'>
<path
id='path62'
d='m494.57 641.61c41.429 15.714 140 11.428 191.43 12.856s160.48-10.23 201.43-27.143c40.995-16.93 134.78-67.656 151.43-72.857 22.857-7.143 41.429-7.143 80-20 38.572-12.857 25.714-32.857 25.714-32.857l-30 4.285-5.756-52.92s37.185-1.365 41.47-15.651c4.286-14.286 5.715-31.429-2.856-41.429-8.572-10-14.286 1.429-18.572-12.857-4.286-14.285-2.857-28.571-27.143-27.143-24.285 1.429-98.571 0-98.571 0s-15.714 108.57-98.572 105.72c-82.857-2.857-95.714-105.72-95.714-105.72h-500s-5.714 104.29-97.143 105.72c-91.428 1.428-98.571-105.72-98.571-105.72h-65.714l-18.572 38.572c-0.515 0-26.243 0-21.428 17.143 4.651 16.561-4.286 41.428 17.142 41.428 21.429 0 47.143-1.428 47.143-1.428l15.715 44.285-41.429 2.859s34.286 24.285 118.57 32.857c84.286 8.571 157.14 8.571 192.86 31.428 35.714 22.857 137.14 78.572 137.14 78.572z'
fill='none'
strokeWidth='5'
/>
<path
id='path64'
d='m28.857 394.47h92.857m180 0h517.14m172.86 0h151.43'
fill='none'
strokeWidth='2'
/>
<path
id='path66'
d='m364.57 547.33c15.715 18.571 123.37 81.524 151.43 88.571 29.502 7.41 103 7.143 103 7.143l-7.143-95.714z'
fill='#f0ffeb'
strokeWidth='5'
/>
<path
id='path68'
d='m404.57 577.33v-28.571'
fill='none'
strokeWidth='2'
/>
<path
id='path70'
d='m644.7 641.64-4.285-94.328h207.16c-11.307 20.753-46.612 74.906-72.857 88.571-14.285 10-82.878 4.328-130.02 5.757zm167.02-7.165s80-84.285 85.715-87.142c5.714-2.857 115.71-1.429 115.71-1.429s-77.143 47.143-102.86 58.572c-25.715 11.428-90 31.428-98.572 30z'
fill='#f0ffeb'
strokeWidth='5'
/>
<g fill='none'>
<path
id='path72'
d='m345.64 556.81c-14.075-30.967-18.298-71.787-18.298-94.309 0-22.521 4.223-91.493 22.521-105.57m282.93 298.41c-1.408-5.63-4.047-81.905-6.51-106.93-3.67-18.714-4.989-51.218-6.159-87.314 0-22.522 7.038-80.233 12.669-105.57'
strokeWidth='5'
/>
<path
id='path74'
d='m103.53 396.34s15.483 85.864 106.98 85.864c91.494 0 109.79-87.271 109.79-87.271m479.99 0s15.483 85.863 106.98 85.863c91.493 0 109.79-87.27 109.79-87.27m-947.31 57.71h63.342'
strokeWidth='2'
/>
<path
id='path76'
d='m779.18 648.3c18.299-12.669 56.304-50.674 92.9-104.16 36.598-53.489 8.447-59.12-18.298-74.603-26.744-15.483-49.266-42.228-67.564-112.61'
strokeWidth='5'
/>
<path
id='path78'
d='m1112.8 452.42h-129.5m-696.76 0.223h544.74m-22.522 150.61v-54.895'
strokeWidth='2'
/>
</g>
<g strokeWidth='2'>
<rect
id='rect80'
transform='scale(1 -1)'
x='558.65'
y='-503.55'
width='41.286'
height='14.541'
rx='2.933'
ry='7.379'
fill='#e6e6e6'
/>
<rect
id='rect82'
transform='scale(1 -1)'
x='816.24'
y='-503.55'
width='41.286'
height='14.541'
rx='2.933'
ry='7.379'
fill='#e6e6e6'
/>
<rect
id='rect84'
transform='scale(1 -1)'
x='259.53'
y='-502.15'
width='18.776'
height='11.738'
rx='1.63'
ry='7.692'
fill='#ffcb00'
/>
<circle
id='circle86'
transform='translate(941.34 284)'
cx='59.119'
cy='211.28'
r='16.891'
fill='#fff'
/>
</g>
</g>
<path
id='path90'
d='m-126.58 704.93v260.4'
fill='none'
stroke='#000'
strokeWidth='2'
/>
<path
id='path92'
d='m-153.88 992.49v-26.041h45.043v52.08h-45.043zm0-316.71v-27.448h45.043v54.898h-45.043z'
fill='#ffffc0'
/>
<g fill='none' stroke='#000'>
<path
id='path94'
d='m-157.4 624.38s-4.223-12.669-18.299-12.669-14.076 8.446-14.076 8.446v423.69s1.408 9.853 14.076 9.853c12.669 0 18.3-9.853 18.3-9.853'
strokeWidth='5'
/>
<path
id='path96'
d='m-191.18 624.38s-35.19-1.408-35.19 21.114v371.6c0 22.521 36.597 25.336 36.597 25.336'
strokeWidth='5'
/>
<g strokeWidth='2'>
<rect
id='rect98'
x='-216.51'
y='648.31'
width='16.891'
height='35.19'
rx='7.742'
ry='6.284'
/>
<rect
id='rect100'
x='-216.51'
y='985.58'
width='16.891'
height='35.19'
rx='7.742'
ry='6.284'
/>
<path id='path102' d='m-62.939 645.49h30.967v377.24h-30.967z' />
</g>
<path
id='path104'
d='m1200.9 633.86v73.195h67.565v-83.048l-25.337-14.076zm0 329.38h67.565v78.826l-23.93 14.076-43.635-18.3zm0-270.26v284.33m67.565-283.74v284.33'
strokeWidth='5'
/>
<path
id='path106'
d='m1216.6 759.51h14.076v147.8h-14.076zm7.631-1.408v-53.488m0 257.37v-53.49m30.375-201.06v256.18'
strokeWidth='2'
/>
<path
id='path108'
d='m1268.8 624.97s4.223-12.668 18.299-12.668 14.076 8.445 14.076 8.445v423.69s-1.408 9.853-14.076 9.853c-12.669 0-18.299-9.853-18.299-9.853m33.783-419.46s35.19-1.408 35.19 21.114v371.6c0 22.521-36.598 25.337-36.598 25.337'
strokeWidth='5'
/>
<g strokeWidth='2'>
<rect
id='rect110'
transform='scale(-1 1)'
x='-1329.9'
y='648.9'
width='16.891'
height='35.19'
rx='7.742'
ry='6.284'
/>
<rect
id='rect112'
transform='scale(-1 1)'
x='-1329.9'
y='986.17'
width='16.891'
height='35.19'
rx='7.742'
ry='6.284'
/>
<path
id='path114'
d='m1199.7 632.82 67.565 74.603m-67.565 0.592 67.565-73.195m-67.342 328.16 67.565 74.602m-67.565 0.593 67.565-73.195'
/>
</g>
</g>
<g stroke='#000'>
<g strokeWidth='2'>
<circle
id='circle116'
transform='translate(78.489 1074.7)'
cx='119.65'
cy='202.84'
r='76.01'
fill='#c8c8c8'
/>
<circle
id='circle118'
transform='translate(94.676 1204.9)'
cx='103.46'
cy='72.633'
r='44.339'
fill='#fff'
/>
<circle
id='circle120'
transform='translate(78.489 187.66)'
cx='119.65'
cy='202.84'
r='76.01'
fill='#c8c8c8'
/>
<circle
id='circle122'
transform='translate(94.676 317.86)'
cx='103.46'
cy='72.633'
r='44.339'
fill='#fff'
/>
<circle
id='circle124'
transform='translate(773.02 187.66)'
cx='119.65'
cy='202.84'
r='76.01'
fill='#c8c8c8'
/>
<circle
id='circle126'
transform='translate(789.21 317.86)'
cx='103.46'
cy='72.633'
r='44.339'
fill='#fff'
/>
<circle
id='circle128'
transform='translate(773.02 1074.7)'
cx='119.65'
cy='202.84'
r='76.01'
fill='#c8c8c8'
/>
<circle
id='circle130'
transform='translate(789.21 1204.9)'
cx='103.46'
cy='72.633'
r='44.339'
fill='#fff'
/>
</g>
<path
id='path132'
d='m1338.5 829.07h40.82'
fill='none'
strokeWidth='5'
/>
<circle
id='circle134'
transform='translate(1441.3 600.31)'
cx='-59.119'
cy='229.58'
r='4.223'
fill='#3c3c3c'
strokeWidth='5'
/>
<g strokeWidth='2'>
<path id='path136' d='m778.06 845.37-38.709-38.709' fill='none' />
<g id='g146' transform='translate(-13.78 15.524)' fill='#3c3c3c'>
<circle
id='circle138'
transform='translate(-79.458 449.8)'
cx='-81.641'
cy='188.76'
r='4.223'
/>
<circle
id='circle140'
transform='translate(-79.458 569.92)'
cx='-81.641'
cy='188.76'
r='4.223'
/>
<circle
id='circle142'
transform='translate(-79.458 690.03)'
cx='-81.641'
cy='188.76'
r='4.223'
/>
<circle
id='circle144'
transform='translate(-79.458 810.15)'
cx='-81.641'
cy='188.76'
r='4.223'
/>
</g>
<g id='g156' transform='translate(-13.78 17.524)' fill='#3c3c3c'>
<circle
id='circle148'
transform='translate(1381.6 448.25)'
cx='-81.641'
cy='188.76'
r='4.223'
/>
<circle
id='circle150'
transform='translate(1381.6 568.36)'
cx='-81.641'
cy='188.76'
r='4.223'
/>
<circle
id='circle152'
transform='translate(1381.6 688.48)'
cx='-81.641'
cy='188.76'
r='4.223'
/>
<circle
id='circle154'
transform='translate(1381.6 808.59)'
cx='-81.641'
cy='188.76'
r='4.223'
/>
</g>
</g>
</g>
</g>
<g id='layer2' fill='#d00000'>
<circle
id='p02'
cx='503.65'
cy='248.75'
r='61.935'
opacity={dmg1 === "02" ? 100 : 0}
/>
<circle
id='p03'
cx='863.41'
cy='248.75'
r='61.935'
opacity={dmg1 === "03" ? 100 : 0}
/>
<circle
id='p04'
cx='1181.5'
cy='248.75'
r='61.935'
opacity={dmg1 === "04" ? 100 : 0}
/>
<circle
id='p05'
cx='1378.4'
cy='151.16'
r='61.935'
opacity={dmg1 === "05" ? 100 : 0}
/>
<circle
id='p06'
cx='1535.1'
cy='581.37'
r='61.935'
opacity={dmg1 === "06" ? 100 : 0}
/>
<circle
id='p07'
cx='1378.4'
cy='997.9'
r='61.935'
opacity={dmg1 === "07" ? 100 : 0}
/>
<circle
id='p08'
cx='1181.5'
cy='914.24'
r='61.935'
opacity={dmg1 === "08" ? 100 : 0}
/>
<circle
id='p09'
transform='scale(1,-1)'
cx='863.41'
cy='-914.24'
r='61.935'
opacity={dmg1 === "09" ? 100 : 0}
/>
<circle
id='p10'
cx='503.65'
cy='914.24'
r='61.935'
opacity={dmg1 === "10" ? 100 : 0}
/>
<circle
id='p11'
cx='297.77'
cy='997.9'
r='61.935'
opacity={dmg1 === "11" ? 100 : 0}
/>
<circle
id='p12'
cx='93.269'
cy='581.37'
r='61.935'
opacity={dmg1 === "12" ? 100 : 0}
/>
<circle
id='p25'
cx='424.31'
cy='581.37'
r='61.935'
opacity={dmg1 === "25" ? 100 : 0}
/>
<circle
id='p27'
cx='972.84'
cy='581.37'
r='61.935'
opacity={dmg1 === "27" ? 100 : 0}
/>
<circle
id='p01'
cx='297.77'
cy='151.16'
r='61.935'
opacity={dmg1 === "01" ? 100 : 0}
/>
<circle
id='p26'
cx='1339.4'
cy='581.37'
r='61.935'
opacity={dmg1 === "26" ? 100 : 0}
/>
</g>
<g id='g4994' fill='#ffef00'>
<circle
id='s02'
cx='503.65'
cy='248.75'
r='61.935'
opacity={dmg2 === "02" ? 100 : 0}
/>
<circle
id='s03'
cx='863.41'
cy='248.75'
r='61.935'
opacity={dmg2 === "03" ? 100 : 0}
/>
<circle
id='s04'
cx='1181.5'
cy='248.75'
r='61.935'
opacity={dmg2 === "04" ? 100 : 0}
/>
<circle
id='s05'
cx='1378.4'
cy='151.16'
r='61.935'
opacity={dmg2 === "05" ? 100 : 0}
/>
<circle
id='s06'
cx='1535.1'
cy='581.37'
r='61.935'
opacity={dmg2 === "06" ? 100 : 0}
/>
<circle
id='s07'
cx='1378.4'
cy='997.9'
r='61.935'
opacity={dmg2 === "07" ? 100 : 0}
/>
<circle
id='s08'
cx='1181.5'
cy='914.24'
r='61.935'
opacity={dmg2 === "08" ? 100 : 0}
/>
<circle
id='s09'
transform='scale(1,-1)'
cx='863.41'
cy='-914.24'
r='61.935'
opacity={dmg2 === "09" ? 100 : 0}
/>
<circle
id='s10'
cx='503.65'
cy='914.24'
r='61.935'
opacity={dmg2 === "10" ? 100 : 0}
/>
<circle
id='s11'
cx='297.77'
cy='997.9'
r='61.935'
opacity={dmg2 === "11" ? 100 : 0}
/>
<circle
id='s12'
cx='93.269'
cy='581.37'
r='61.935'
opacity={dmg2 === "12" ? 100 : 0}
/>
<circle
id='s25'
cx='424.31'
cy='581.37'
r='61.935'
opacity={dmg2 === "25" ? 100 : 0}
/>
<circle
id='s27'
cx='972.84'
cy='581.37'
r='61.935'
opacity={dmg2 === "27" ? 100 : 0}
/>
<circle
id='s01'
cx='297.77'
cy='151.16'
r='61.935'
opacity={dmg2 === "01" ? 100 : 0}
/>
<circle
id='s26'
cx='1339.4'
cy='581.37'
r='61.935'
opacity={dmg2 === "26" ? 100 : 0}
/>
{
// <text
// id='p15'
// opacity='0'
// x='382.62802'
// y='1034.3463'
// fill='#fd0000'
// fontFamily='sans-serif'
// fontSize='1696.9px'
// letterSpacing='0px'
// strokeWidth='17.676'
// wordSpacing='0px'
// style='line-height:5.25'>
// x
// </text>
}
</g>
</svg>
);

View File

@@ -17,8 +17,7 @@ import JobDetailCardsNotesComponent from "./job-detail-cards.notes.component";
import JobDetailCardsPartsComponent from "./job-detail-cards.parts.component";
import "./job-detail-cards.styles.scss";
import JobDetailCardsTotalsComponent from "./job-detail-cards.totals.component";
import ScheduleJobModalContainer from "../schedule-job-modal/schedule-job-modal.container";
export default function JobDetailCards({ selectedJob }) {
const { loading, error, data, refetch } = useQuery(QUERY_JOB_CARD_DETAILS, {
@@ -27,6 +26,7 @@ export default function JobDetailCards({ selectedJob }) {
skip: !selectedJob
});
const [noteModalVisible, setNoteModalVisible] = useState(false);
const scheduleModalState = useState(false);
const { t } = useTranslation();
if (!selectedJob) {
@@ -43,13 +43,18 @@ export default function JobDetailCards({ selectedJob }) {
changeVisibility={setNoteModalVisible}
refetch={refetch}
/>
<ScheduleJobModalContainer
scheduleModalState={scheduleModalState}
jobId={data.jobs_by_pk.id}
refetch={refetch}
/>
<PageHeader
ghost={false}
onBack={() => window.history.back()}
tags={
<span key='job-status'>
{data.jobs_by_pk.job_status ? (
<Tag color='blue'>{data.jobs_by_pk.job_status.name}</Tag>
{data.jobs_by_pk.status ? (
<Tag color='blue'>{data.jobs_by_pk.status}</Tag>
) : null}
</span>
}
@@ -67,6 +72,14 @@ export default function JobDetailCards({ selectedJob }) {
)
}
extra={[
<Button
key='schedule'
//TODO Enabled logic based on status.
onClick={() => {
scheduleModalState[1](true);
}}>
{t("jobs.actions.schedule")}
</Button>,
<Link
key='documents'
to={`/manage/jobs/${data.jobs_by_pk.id}#documents`}>

View File

@@ -14,7 +14,10 @@ export default function JobDetailCardsCustomerComponent({ loading, data }) {
extraLink={data && data.owner ? `/manage/owners/${data.owner.id}` : null}>
{data ? (
<span>
<div>{`${data.ownr_fn || ""} ${data.ownr_ln || ""}`}</div>
<div>
<Link to={`/manage/owners/${data.owner.id}`}>{`${data.ownr_fn ||
""} ${data.ownr_ln || ""}`}</Link>
</div>
<div>
{t("jobs.fields.phoneshort")}:
<PhoneFormatter>{`${data.ownr_ph1 ||

View File

@@ -1,18 +1,14 @@
import React from "react";
import { useTranslation } from "react-i18next";
import CardTemplate from "./job-detail-cards.template.component";
import UnfoldedCar from "../../assets/unfolded_car.svg";
import Car from "../job-damage-visual/job-damage-visual.component";
export default function JobDetailCardsDamageComponent({ loading, data }) {
const { t } = useTranslation();
const { area_of_damage } = data;
return (
<CardTemplate loading={loading} title={t("jobs.labels.cards.damage")}>
{data ? (
<span>
<img src={UnfoldedCar} alt='Damaged Area' width={200} height={200} />
</span>
) : null}
<Car dmg1={area_of_damage.impact1} dmg2={area_of_damage.impact2} />
</CardTemplate>
);
}

View File

@@ -1,8 +1,8 @@
import { Timeline } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { DateFormatter } from "../../utils/DateFormatter";
import CardTemplate from "./job-detail-cards.template.component";
import Moment from "react-moment";
import { Timeline } from "antd";
export default function JobDetailCardsDatesComponent({ loading, data }) {
const { t } = useTranslation();
@@ -31,90 +31,84 @@ export default function JobDetailCardsDatesComponent({ loading, data }) {
{data.actual_in ? (
<Timeline.Item>
{t("jobs.fields.actual_in")}
<Moment format='MM/DD/YYYY'>{data.actual_in || ""}</Moment>
<DateFormatter>{data.actual_in}</DateFormatter>
</Timeline.Item>
) : null}
{data.scheduled_completion ? (
<Timeline.Item>
{t("jobs.fields.scheduled_completion")}
<Moment format='MM/DD/YYYY'>
{data.scheduled_completion || ""}
</Moment>
{t("jobs.fields.scheduled_completion")}
<DateFormatter>{data.scheduled_completion}</DateFormatter>
</Timeline.Item>
) : null}
{data.scheduled_in ? (
<Timeline.Item>
{t("jobs.fields.scheduled_in")}
<Moment format='MM/DD/YYYY'>{data.scheduled_in || ""}</Moment>
<DateFormatter>{data.scheduled_in}</DateFormatter>
</Timeline.Item>
) : null}
{data.actual_completion ? (
<Timeline.Item>
{t("jobs.fields.actual_completion")}
<Moment format='MM/DD/YYYY'>
{data.actual_completion || ""}
</Moment>
<DateFormatter>{data.actual_completion}</DateFormatter>
</Timeline.Item>
) : null}
{data.scheduled_delivery ? (
<Timeline.Item>
{t("jobs.fields.scheduled_delivery")}
<Moment format='MM/DD/YYYY'>
{data.scheduled_delivery || ""}
</Moment>
<DateFormatter>{data.scheduled_delivery}</DateFormatter>
</Timeline.Item>
) : null}
{data.actual_delivery ? (
<Timeline.Item>
{t("jobs.fields.actual_delivery")}
<Moment format='MM/DD/YYYY'>{data.actual_delivery || ""}</Moment>
<DateFormatter>{data.actual_delivery}</DateFormatter>
</Timeline.Item>
) : null}
{data.date_estimated ? (
<Timeline.Item>
{t("jobs.fields.date_estimated")}
<Moment format='MM/DD/YYYY'>{data.date_estimated || ""}</Moment>
<DateFormatter>{data.date_estimated}</DateFormatter>
</Timeline.Item>
) : null}
{data.date_open ? (
<Timeline.Item>
{t("jobs.fields.date_open")}
<Moment format='MM/DD/YYYY'>{data.date_open || ""}</Moment>
<DateFormatter>{data.date_open}</DateFormatter>
</Timeline.Item>
) : null}
{data.date_scheduled ? (
<Timeline.Item>
{t("jobs.fields.date_scheduled")}
<Moment format='MM/DD/YYYY'>{data.date_scheduled || ""}</Moment>
<DateFormatter>{data.date_scheduled}</DateFormatter>
</Timeline.Item>
) : null}
{data.date_invoiced ? (
<Timeline.Item>
{t("jobs.fields.date_invoiced")}
<Moment format='MM/DD/YYYY'>{data.date_invoiced || ""}</Moment>
<DateFormatter>{data.date_invoiced}</DateFormatter>
</Timeline.Item>
) : null}
{data.date_closed ? (
<Timeline.Item>
{t("jobs.fields.date_closed")}
<Moment format='MM/DD/YYYY'>{data.date_closed || ""}</Moment>
<DateFormatter>{data.date_closed}</DateFormatter>
</Timeline.Item>
) : null}
{data.date_exported ? (
<Timeline.Item>
{t("jobs.fields.date_exported")}
<Moment format='MM/DD/YYYY'>{data.date_exported || ""}</Moment>
<DateFormatter>{data.date_exported}</DateFormatter>
</Timeline.Item>
) : null}
</Timeline>

View File

@@ -18,13 +18,12 @@ export default function JobDetailCardsDocumentsComponent({ loading, data }) {
<CardTemplate
loading={loading}
title={t("jobs.labels.cards.documents")}
extraLink={`/manage/jobs/${data.id}#documents`}>
{data.documents.count > 0 ? (
extraLink={`/manage/jobs/${data.id}#documents`}
>
{data.documents.length > 0 ? (
<Carousel autoplay>
{data.documents.map(item => (
<div key={item.id}>
<img src={item.thumb_url} alt={item.name} />
</div>
<img key={item.id} src={item.thumb_url} alt={item.name} />
))}
</Carousel>
) : (

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ export default function JobDetailCardsNotesComponent({ loading, data }) {
const { t } = useTranslation();
return (
<CardTemplate
<CardTemplate
loading={loading}
title={t("jobs.labels.cards.notes")}
extraLink={`/manage/jobs/${data.id}#notes`}>
@@ -22,7 +22,7 @@ export default function JobDetailCardsNotesComponent({ loading, data }) {
<List
size='small'
bordered
dataSource={data?.notes}
dataSource={data.notes}
renderItem={item => (
<List.Item>
{item.critical ? (

View File

@@ -9,7 +9,7 @@ export default function JobDetailCardsVehicleComponent({ loading, data }) {
<CardTemplate
loading={loading}
title={t("jobs.labels.cards.vehicle")}
extraLink={data?.vehicle ? `/manage/vehicles/${data?.vehicle?.id}` : null}
extraLink={data.vehicle ? `/manage/vehicles/${data.vehicle.id}` : null}
>
{data ? (
<span>

View File

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

View File

@@ -1,62 +1,103 @@
import { Table, Button } from "antd";
import React, { useContext, useState } from "react";
import { Button, Input, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import JobDetailFormContext from "../../pages/jobs-detail/jobs-detail.page.context";
import { Link } from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters";
import EditableCell from "./job-lines-cell.component";
import AllocationsAssignmentContainer from "../allocations-assignment/allocations-assignment.container";
import AllocationsBulkAssignmentContainer from "../allocations-bulk-assignment/allocations-bulk-assignment.container";
import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container";
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
export default function JobLinesComponent({ job }) {
//const form = useContext(JobDetailFormContext);
//const { getFieldDecorator } = form;
export default function JobLinesComponent({
loading,
refetch,
jobLines,
setSearchText,
selectedLines,
setSelectedLines,
partsOrderModalVisible,
jobId,
setJobLineEditContext
}) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" }
sortedInfo: {}
});
const [editingKey, setEditingKey] = useState("");
const { t } = useTranslation();
const setPartsModalVisible = partsOrderModalVisible[1];
const columns = [
{
title: t("joblines.fields.unq_seq"),
dataIndex: "joblines.unq_seq",
key: "joblines.unq_seq",
dataIndex: "unq_seq",
key: "unq_seq",
// onFilter: (value, record) => record.ro_number.includes(value),
// filteredValue: state.filteredInfo.text || null,
sorter: (a, b) => alphaSort(a, b),
sorter: (a, b) => a.unq_seq - b.unq_seq,
sortOrder:
state.sortedInfo.columnKey === "unq_seq" && state.sortedInfo.order,
//ellipsis: true,
editable: true
editable: true,
width: 75
},
{
title: t("joblines.fields.line_desc"),
dataIndex: "line_desc",
key: "joblines.line_desc",
key: "line_desc",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
sortOrder:
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
ellipsis: true,
editable: true
editable: true,
width: "20%"
},
{
title: t("joblines.fields.oem_partno"),
dataIndex: "oem_partno",
key: "oem_partno",
sorter: (a, b) =>
alphaSort(
a.oem_partno ? a.oem_partno : a.op_code_desc,
b.oem_partno ? b.oem_partno : b.op_code_desc
),
sortOrder:
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
ellipsis: true,
editable: true,
width: "10%",
render: (text, record) => (
<span>
{record.oem_partno ? record.oem_partno : record.op_code_desc}
</span>
)
},
{
title: t("joblines.fields.part_type"),
dataIndex: "part_type",
key: "joblines.part_type",
key: "part_type",
sorter: (a, b) => alphaSort(a.part_type, b.part_type),
sortOrder:
state.sortedInfo.columnKey === "part_type" && state.sortedInfo.order,
ellipsis: true,
editable: true
editable: true,
width: "7%"
},
{
title: t("joblines.fields.line_ind"),
dataIndex: "line_ind",
key: "line_ind",
sorter: (a, b) => alphaSort(a.line_ind, b.line_ind),
sortOrder:
state.sortedInfo.columnKey === "line_ind" && state.sortedInfo.order
},
{
title: t("joblines.fields.db_price"),
dataIndex: "db_price",
key: "joblines.db_price",
key: "db_price",
sorter: (a, b) => a.db_price - b.db_price,
sortOrder:
state.sortedInfo.columnKey === "db_price" && state.sortedInfo.order,
ellipsis: true,
width: "8%",
render: (text, record) => (
<CurrencyFormatter>{record.db_price}</CurrencyFormatter>
)
@@ -64,22 +105,85 @@ export default function JobLinesComponent({ job }) {
{
title: t("joblines.fields.act_price"),
dataIndex: "act_price",
key: "joblines.act_price",
key: "act_price",
sorter: (a, b) => a.act_price - b.act_price,
sortOrder:
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
ellipsis: true,
width: "8%",
render: (text, record) => (
<div>
{" "}
<CurrencyFormatter>{record.act_price}</CurrencyFormatter>{" "}
<CurrencyFormatter>{record.act_price}</CurrencyFormatter>
)
},
{
title: t("joblines.fields.mod_lb_hrs"),
dataIndex: "mod_lb_hrs",
key: "mod_lb_hrs",
sorter: (a, b) => a.mod_lb_hrs - b.mod_lb_hrs,
sortOrder:
state.sortedInfo.columnKey === "mod_lb_hrs" && state.sortedInfo.order
},
{
title: t("joblines.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order
},
{
title: t("allocations.fields.employee"),
dataIndex: "employee",
key: "employee",
width: "10%",
sorter: (a, b) =>
alphaSort(
a.allocations[0] &&
a.allocations[0].employee.first_name +
a.allocations[0].employee.last_name,
b.allocations[0] &&
b.allocations[0].employee.first_name +
b.allocations[0].employee.last_name
),
sortOrder:
state.sortedInfo.columnKey === "employee" && state.sortedInfo.order,
render: (text, record) => (
<span>
{record.allocations && record.allocations.length > 0
? record.allocations.map(item => (
<AllocationsEmployeeLabelContainer
key={item.id}
refetch={refetch}
allocation={item}
/>
))
: null}
<AllocationsAssignmentContainer
key={record.id}
refetch={refetch}
jobLineId={record.id}
hours={record.mod_lb_hrs}
/>
</span>
)
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<span>
<Button
onClick={() => {
setEditingKey(record.id);
}}>
EDIT
setJobLineEditContext({
actions: { refetch: refetch },
context: record
});
}}
>
{t("general.actions.edit")}
</Button>
</div>
</span>
)
}
];
@@ -88,37 +192,91 @@ export default function JobLinesComponent({ job }) {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
// const handleChange = event => {
// const { value } = event.target;
// setState({ ...state, filterinfo: { text: [value] } });
// };
const formItemLayout = {
labelCol: {
xs: { span: 12 },
sm: { span: 5 }
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 }
}
};
return (
<Table
size='small'
pagination={{ position: "bottom" }}
columns={columns.map(col => {
if (!col.editable) {
return col;
}
return {
...col,
onCell: record => ({
record,
inputType: col.dataIndex === "age" ? "number" : "text",
dataIndex: col.dataIndex,
title: col.title,
editing: editingKey === record.id
})
};
})}
components={{
body: {
cell: EditableCell
}
}}
rowKey='id'
dataSource={job.joblines}
onChange={handleTableChange}
/>
<div>
<PartsOrderModalContainer
partsOrderModalVisible={partsOrderModalVisible}
linesToOrder={selectedLines}
refetch={refetch}
jobId={jobId}
/>
<Table
title={() => {
return (
<div>
<Input.Search
placeholder={t("general.labels.search")}
onChange={e => {
e.preventDefault();
setSearchText(e.target.value);
}}
/>
<Button
disabled={selectedLines.length > 0 ? false : true}
onClick={() => setPartsModalVisible(true)}
>
{t("parts.actions.order")}
</Button>
<AllocationsBulkAssignmentContainer
jobLines={selectedLines}
refetch={refetch}
/>
<Button
onClick={() => {
setJobLineEditContext({
actions: { refetch: refetch },
context: { jobid: jobId }
});
}}
>
{t("joblines.actions.new")}
</Button>
</div>
);
}}
{...formItemLayout}
loading={loading}
size="small"
expandedRowRender={record => (
<div style={{ margin: 0 }}>
<strong>{t("parts_orders.labels.orderhistory")}</strong>
{record.parts_order_lines.map(item => (
<div key={item.id}>
{`${item.parts_order.order_number || ""} from `}
<Link to={`/manage/shop/vendors/${item.parts_order.vendor.id}`}>
{item.parts_order.vendor.name || ""}
</Link>
{` on ${item.parts_order.order_date || ""}`}
</div>
))}
</div>
)}
pagination={{ position: "top", defaultPageSize: 25 }}
rowSelection={{
// selectedRowKeys: selectedLines,
onSelectAll: (selected, selectedRows, changeRows) => {
setSelectedLines(selectedRows);
},
onSelect: (record, selected, selectedRows, nativeEvent) =>
setSelectedLines(selectedRows)
}}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={jobLines}
onChange={handleTableChange}
/>
</div>
);
}

View File

@@ -1,19 +1,72 @@
import React from "react";
import JobLinesComponent from "./job-lines.component";
import { useQuery } from "@apollo/react-hooks";
import AlertComponent from "../alert/alert.component";
import React, { useState } from "react";
import { GET_JOB_LINES_BY_PK } from "../../graphql/jobs-lines.queries";
import AlertComponent from "../alert/alert.component";
import JobLinesComponent from "./job-lines.component";
export default function JobLinesContainer({ jobId }) {
const { loading, error, data } = useQuery(GET_JOB_LINES_BY_PK, {
import { connect } from "react-redux";
import { setModalContext } from "../../redux/modals/modals.actions";
const mapDispatchToProps = dispatch => ({
setJobLineEditContext: context =>
dispatch(setModalContext({ context: context, modal: "jobLineEdit" }))
});
export default connect(
null,
mapDispatchToProps
)(function JobLinesContainer({ jobId, setJobLineEditContext }) {
const { loading, error, data, refetch } = useQuery(GET_JOB_LINES_BY_PK, {
variables: { id: jobId },
fetchPolicy: "network-only"
});
const [searchText, setSearchText] = useState("");
const [selectedLines, setSelectedLines] = useState([]);
const partsOrderModalVisible = useState(false);
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<JobLinesComponent loading={loading} joblines={data ? data.joblines : null} />
<JobLinesComponent
loading={loading}
refetch={refetch}
jobLines={
data && data.joblines
? searchText
? data.joblines.filter(
jl =>
(jl.unq_seq || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(jl.line_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(jl.part_type || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(jl.oem_partno || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(jl.op_code_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(jl.db_price || "")
.toString()
.includes(searchText.toLowerCase()) ||
(jl.act_price || "")
.toString()
.includes(searchText.toLowerCase())
)
: data.joblines
: null
}
setSearchText={setSearchText}
selectedLines={selectedLines}
setSelectedLines={setSelectedLines}
partsOrderModalVisible={partsOrderModalVisible}
jobId={jobId}
setJobLineEditContext={setJobLineEditContext}
/>
);
}
});

View File

@@ -0,0 +1,69 @@
import { Modal, Form, Input, InputNumber } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import ResetForm from "../form-items-formatted/reset-form-item.component";
export default function JobLinesUpsertModalComponent({
visible,
jobLine,
handleOk,
handleCancel,
handleSubmit,
form
}) {
const { t } = useTranslation();
const { getFieldDecorator, isFieldsTouched, resetFields } = form;
return (
<Modal
title={
jobLine && jobLine.id
? t("joblines.labels.edit")
: t("joblines.labels.new")
}
visible={visible}
okText={t("general.labels.save")}
onOk={handleSubmit}
onCancel={handleCancel}
>
{isFieldsTouched() ? <ResetForm resetFields={resetFields} /> : null}
<Form onSubmit={handleSubmit} autoComplete={"off"}>
<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>
</Modal>
);
}

View File

@@ -0,0 +1,110 @@
import { Form, notification } from "antd";
import React from "react";
import { useMutation } from "react-apollo";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
INSERT_NEW_JOB_LINE,
UPDATE_JOB_LINE
} from "../../graphql/jobs-lines.queries";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectJobLineEditModal } from "../../redux/modals/modals.selectors";
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
const mapStateToProps = createStructuredSelector({
jobLineEditModal: selectJobLineEditModal
});
const mapDispatchToProps = dispatch => ({
toggleModalVisible: () => dispatch(toggleModalVisible("jobLineEdit"))
});
function JobLinesUpsertModalContainer({
jobLineEditModal,
toggleModalVisible,
form
}) {
const { t } = useTranslation();
const [insertJobLine] = useMutation(INSERT_NEW_JOB_LINE);
const [updateJobLine] = useMutation(UPDATE_JOB_LINE);
const handleSubmit = e => {
e.preventDefault();
form.validateFieldsAndScroll((err, values) => {
if (err) {
notification["error"]({
message: t("joblines.errors.validation"),
description: err.message
});
}
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)
jobLineEditModal.actions.refetch();
toggleModalVisible();
}
}
});
};
const handleCancel = () => {
toggleModalVisible();
};
return (
<JobLinesUpdsertModal
visible={jobLineEditModal.visible}
jobLine={jobLineEditModal.context}
handleSubmit={handleSubmit}
handleCancel={handleCancel}
form={form}
/>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(
Form.create({ name: "JobsDetailPageContainer" })(JobLinesUpsertModalContainer)
);

View File

@@ -4,6 +4,8 @@ import { useTranslation } from "react-i18next";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import OwnerFindModalContainer from "../owner-find-modal/owner-find-modal.container";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
export default function JobsAvailableComponent({
loading,
data,
@@ -77,7 +79,10 @@ export default function JobsAvailableComponent({
key: "clm_amt",
sorter: (a, b) => a.clm_amt - b.clm_amt,
sortOrder:
state.sortedInfo.columnKey === "clm_amt" && state.sortedInfo.order
state.sortedInfo.columnKey === "clm_amt" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.clm_amt}</CurrencyFormatter>
)
//width: "12%",
//ellipsis: true
},
@@ -141,7 +146,8 @@ export default function JobsAvailableComponent({
estData.data.available_jobs_by_pk &&
estData.data.available_jobs_by_pk.est_data &&
estData.data.available_jobs_by_pk.est_data.owner &&
estData.data.available_jobs_by_pk.est_data.owner.data
estData.data.available_jobs_by_pk.est_data.owner.data &&
!estData.data.available_jobs_by_pk.issupplement
? estData.data.available_jobs_by_pk.est_data.owner.data
: null;
@@ -164,7 +170,7 @@ export default function JobsAvailableComponent({
return (
<div>
<Input.Search
placeholder="Search..."
placeholder="Search...//TODO Implement Search"
onSearch={value => {
console.log(value);
}}

View File

@@ -29,14 +29,7 @@ export default withRouter(function JobsAvailableContainer({
const onModalOk = () => {
setModalVisible(false);
console.log("selectedOwner", selectedOwner);
setInsertLoading(true);
console.log(
"logitest",
estData.data &&
estData.data.available_jobs_by_pk &&
estData.data.available_jobs_by_pk.est_data
);
if (
!(

View File

@@ -3,13 +3,25 @@ import React, { useState } from "react";
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";
export default function JobsAvailableSupplementComponent({
loading,
data,
refetch,
deleteJob,
updateJob,
onModalOk,
onModalCancel,
modalVisible,
setModalVisible,
selectedJob,
setSelectedJob,
deleteAllNewJobs,
estDataLazyLoad
loadEstData,
estData,
importOptionsState
}) {
const { t } = useTranslation();
@@ -81,7 +93,10 @@ export default function JobsAvailableSupplementComponent({
key: "clm_amt",
sorter: (a, b) => a.clm_amt - b.clm_amt,
sortOrder:
state.sortedInfo.columnKey === "clm_amt" && state.sortedInfo.order
state.sortedInfo.columnKey === "clm_amt" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.clm_amt}</CurrencyFormatter>
)
//width: "12%",
//ellipsis: true
},
@@ -127,7 +142,8 @@ export default function JobsAvailableSupplementComponent({
</Button>
<Button
onClick={() => {
alert("Add");
loadEstData({ variables: { id: record.id } });
setModalVisible(true);
}}
>
<Icon type="plus" />
@@ -140,54 +156,66 @@ export default function JobsAvailableSupplementComponent({
];
return (
<Table
loading={loading}
title={() => {
return (
<div>
<Input.Search
placeholder="Search..."
onSearch={value => {
console.log(value);
}}
enterButton
/>
<Button
onClick={() => {
refetch();
}}
>
<Icon type="sync" />
</Button>
<Button
onClick={() => {
deleteAllNewJobs()
.then(r => {
notification["success"]({
message: t("jobs.successes.all_deleted", {
count: r.data.delete_available_jobs.affected_rows
})
<div>
<JobsFindModalContainer
loading={estData.loading}
error={estData.error}
selectedJob={selectedJob}
setSelectedJob={setSelectedJob}
importOptionsState={importOptionsState}
visible={modalVisible}
onOk={onModalOk}
onCancel={onModalCancel}
/>
<Table
loading={loading}
title={() => {
return (
<div>
<Input.Search
placeholder="Search..."
onSearch={value => {
console.log(value);
}}
enterButton
/>
<Button
onClick={() => {
refetch();
}}
>
<Icon type="sync" />
</Button>
<Button
onClick={() => {
deleteAllNewJobs()
.then(r => {
notification["success"]({
message: t("jobs.successes.all_deleted", {
count: r.data.delete_available_jobs.affected_rows
})
});
refetch();
})
.catch(r => {
notification["error"]({
message: t("jobs.errors.deleted") + " " + r.message
});
});
refetch();
})
.catch(r => {
notification["error"]({
message: t("jobs.errors.deleted") + " " + r.message
});
});
}}
>
Delete All
</Button>
</div>
);
}}
size="small"
pagination={{ position: "top" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={data && data.available_jobs}
onChange={handleTableChange}
/>
}}
>
Delete All
</Button>
</div>
);
}}
size="small"
pagination={{ position: "top" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={data && data.available_jobs}
onChange={handleTableChange}
/>
</div>
);
}

View File

@@ -1,12 +1,21 @@
import React from "react";
import { notification } from "antd";
import React, { useState } from "react";
import { useMutation, useQuery } from "react-apollo";
import { DELETE_ALL_AVAILABLE_SUPPLEMENT_JOBS, QUERY_AVAILABLE_SUPPLEMENT_JOBS } from "../../graphql/available-jobs.queries";
import { useTranslation } from "react-i18next";
import { withRouter } from "react-router-dom";
import {
DELETE_ALL_AVAILABLE_SUPPLEMENT_JOBS,
QUERY_AVAILABLE_SUPPLEMENT_JOBS
} from "../../graphql/available-jobs.queries";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import AlertComponent from "../alert/alert.component";
import JobsAvailableSupplementComponent from "./jobs-available-supplement.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
export default function JobsAvailableSupplementContainer({
export default withRouter(function JobsAvailableSupplementContainer({
deleteJob,
estDataLazyLoad
estDataLazyLoad,
history
}) {
const { loading, error, data, refetch } = useQuery(
QUERY_AVAILABLE_SUPPLEMENT_JOBS,
@@ -14,17 +23,107 @@ export default function JobsAvailableSupplementContainer({
fetchPolicy: "network-only"
}
);
const { t } = useTranslation();
const [deleteAllNewJobs] = useMutation(DELETE_ALL_AVAILABLE_SUPPLEMENT_JOBS);
if (error) return <AlertComponent type="error" message={error.message} />;
const [modalVisible, setModalVisible] = useState(false);
const [selectedJob, setSelectedJob] = useState(null);
const [insertLoading, setInsertLoading] = useState(false);
const [updateJob] = useMutation(UPDATE_JOB);
const [loadEstData, estData] = estDataLazyLoad;
const importOptionsState = useState({ overrideHeaders: false });
const importOptions = importOptionsState[0];
const onModalOk = () => {
setModalVisible(false);
setInsertLoading(true);
if (
!(
estData.data &&
estData.data.available_jobs_by_pk &&
estData.data.available_jobs_by_pk.est_data
)
) {
//We don't have the right data. Error!
setInsertLoading(false);
notification["error"]({
message: t("jobs.errors.creating", { error: "No job data present." })
});
} else {
//create upsert job
let supp = estData.data.available_jobs_by_pk.est_data;
delete supp.joblines;
//TODO How to update the estimate lines.
delete supp.owner;
delete supp.vehicle;
if (!importOptions.overrideHeaders) {
delete supp["ins_ea"];
//TODO Remove all required fields.
}
updateJob({
variables: {
jobId: selectedJob,
job: supp
}
})
.then(r => {
notification["success"]({
message: t("jobs.successes.supplemented"),
onClick: () => {
history.push(
`/manage/jobs/${r.data.update_jobs.returning[0].id}`
);
}
});
//Job has been inserted. Clean up the available jobs record.
deleteJob({
variables: { id: estData.data.available_jobs_by_pk.id }
}).then(r => {
refetch();
setInsertLoading(false);
});
})
.catch(r => {
//error while inserting
notification["error"]({
message: t("jobs.errors.creating", { error: r.message })
});
refetch();
setInsertLoading(false);
});
}
};
const onModalCancel = () => {
setModalVisible(false);
setSelectedJob(null);
};
if (error) return <AlertComponent type='error' message={error.message} />;
return (
<JobsAvailableSupplementComponent
loading={loading}
data={data}
refetch={refetch}
deleteJob={deleteJob}
deleteAllNewJobs={deleteAllNewJobs}
estDataLazyLoad={estDataLazyLoad}
/>
<LoadingSpinner
loading={insertLoading}
message={t("jobs.labels.creating_new_job")}>
<JobsAvailableSupplementComponent
loading={loading}
data={data}
refetch={refetch}
deleteJob={deleteJob}
updateJob={updateJob}
onModalOk={onModalOk}
onModalCancel={onModalCancel}
modalVisible={modalVisible}
setModalVisible={setModalVisible}
selectedJob={selectedJob}
setSelectedJob={setSelectedJob}
deleteAllNewJobs={deleteAllNewJobs}
loadEstData={loadEstData}
estData={estData}
importOptionsState={importOptionsState}
/>
</LoadingSpinner>
);
}
});

View File

@@ -20,7 +20,7 @@ export default function JobsDetailClaims({ job }) {
initialValue: job.loss_desc
})(<Input name='loss_desc' />)}
</Form.Item>
TODO: How to handle different taxes and marking them as exempt?
TODO How to handle different taxes and marking them as exempt?
{
// <Form.Item label={t("jobs.fields.exempt")}>
// {getFieldDecorator("exempt", {

View File

@@ -0,0 +1,90 @@
import { DatePicker, Form } from "antd";
import moment from "moment";
import React, { useContext } from "react";
import { useTranslation } from "react-i18next";
import JobDetailFormContext from "../../pages/jobs-detail/jobs-detail.page.context";
export default function JobsDetailDatesComponent({ job }) {
const form = useContext(JobDetailFormContext);
const { getFieldDecorator } = form;
const { t } = useTranslation();
return (
<div>
<Form.Item label={t("jobs.fields.loss_date")}>
{getFieldDecorator("loss_date", {
initialValue: job.loss_date ? moment(job.loss_date) : null
})(<DatePicker name="loss_date" />)}
</Form.Item>
DAMAGE {JSON.stringify(job.area_of_damage)}
CAA # seems not correct based on field mapping Class seems not correct
based on field mapping
<Form.Item label={t("jobs.fields.date_estimated")}>
{getFieldDecorator("date_estimated", {
initialValue: job.date_estimated ? moment(job.date_estimated) : null
})(<DatePicker name="date_estimated" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.date_open")}>
{getFieldDecorator("date_open", {
initialValue: job.date_open ? moment(job.date_open) : null
})(<DatePicker name="date_open" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.date_scheduled")}>
{getFieldDecorator("date_scheduled", {
initialValue: job.date_scheduled ? moment(job.date_scheduled) : null
})(<DatePicker name="date_scheduled" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.scheduled_in")}>
{getFieldDecorator("scheduled_in", {
initialValue: job.scheduled_in ? moment(job.scheduled_in) : null
})(<DatePicker name="scheduled_in" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.actual_in")}>
{getFieldDecorator("actual_in", {
initialValue: job.actual_in ? moment(job.actual_in) : null
})(<DatePicker name="actual_in" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.scheduled_completion")}>
{getFieldDecorator("scheduled_completion", {
initialValue: job.scheduled_completion
? moment(job.scheduled_completion)
: null
})(<DatePicker name="scheduled_completion" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.actual_completion")}>
{getFieldDecorator("actual_completion", {
initialValue: job.actual_completion
? moment(job.actual_completion)
: null
})(<DatePicker name="actual_completion" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.scheduled_delivery")}>
{getFieldDecorator("scheduled_delivery", {
initialValue: job.scheduled_delivery
? moment(job.scheduled_delivery)
: null
})(<DatePicker name="scheduled_delivery" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.actual_delivery")}>
{getFieldDecorator("actual_delivery", {
initialValue: job.actual_delivery ? moment(job.actual_delivery) : null
})(<DatePicker name="actual_delivery" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.date_invoiced")}>
{getFieldDecorator("date_invoiced", {
initialValue: job.date_invoiced ? moment(job.date_invoiced) : null
})(<DatePicker name="date_invoiced" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.date_closed")}>
{getFieldDecorator("date_closed", {
initialValue: job.date_closed ? moment(job.date_closed) : null
})(<DatePicker name="date_closed" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.date_exported")}>
{getFieldDecorator("date_exported", {
initialValue: job.date_exported ? moment(job.date_exported) : null
})(<DatePicker name="date_exported" />)}
</Form.Item>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { Form, Input, InputNumber } from "antd";
import { Form, Input, InputNumber, Divider } from "antd";
import React, { useContext } from "react";
import { useTranslation } from "react-i18next";
import JobDetailFormContext from "../../pages/jobs-detail/jobs-detail.page.context";
@@ -25,13 +25,13 @@ export default function JobsDetailFinancials({ job }) {
initialValue: job.depreciation_taxes
})(<InputNumber name="depreciation_taxes" />)}
</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")}>
{getFieldDecorator("federal_tax_payable", {
initialValue: job.federal_tax_payable
})(<InputNumber name="federal_tax_payable" />)}
</Form.Item>
TODO: equivalent of other customer amount
TODO equivalent of other customer amount
<Form.Item label={t("jobs.fields.other_amount_payable")}>
{getFieldDecorator("other_amount_payable", {
initialValue: job.other_amount_payable
@@ -52,6 +52,130 @@ export default function JobsDetailFinancials({ job }) {
initialValue: job.adjustment_bottom_line
})(<InputNumber name="adjustment_bottom_line" />)}
</Form.Item>
<Divider />
Totals Table
<Form.Item label={t("jobs.fields.labor_rate_desc")}>
{getFieldDecorator("labor_rate_desc", {
initialValue: job.labor_rate_desc
})(<Input name="labor_rate_desc" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lab")}>
{getFieldDecorator("rate_lab", {
initialValue: job.rate_lab
})(<InputNumber name="rate_lab" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lad")}>
{getFieldDecorator("rate_lad", {
initialValue: job.rate_lad
})(<InputNumber name="rate_lad" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lae")}>
{getFieldDecorator("rate_lae", {
initialValue: job.rate_lae
})(<InputNumber name="rate_lae" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lar")}>
{getFieldDecorator("rate_lar", {
initialValue: job.rate_lar
})(<InputNumber name="rate_lar" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_las")}>
{getFieldDecorator("rate_las", {
initialValue: job.rate_las
})(<InputNumber name="rate_las" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_laf")}>
{getFieldDecorator("rate_laf", {
initialValue: job.rate_laf
})(<InputNumber name="rate_laf" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lam")}>
{getFieldDecorator("rate_lam", {
initialValue: job.rate_lam
})(<InputNumber name="rate_lam" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lag")}>
{getFieldDecorator("rate_lag", {
initialValue: job.rate_lag
})(<InputNumber name="rate_lag" />)}
</Form.Item>
Note //TODO Remove ATP rate?
<Form.Item label={t("jobs.fields.rate_atp")}>
{getFieldDecorator("rate_atp", {
initialValue: job.rate_atp
})(<InputNumber name="rate_atp" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lau")}>
{getFieldDecorator("rate_lau", {
initialValue: job.rate_lau
})(<InputNumber name="rate_lau" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_la1")}>
{getFieldDecorator("rate_la1", {
initialValue: job.rate_la1
})(<InputNumber name="rate_la1" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_la2")}>
{getFieldDecorator("rate_la2", {
initialValue: job.rate_la2
})(<InputNumber name="rate_la2" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_la3")}>
{getFieldDecorator("rate_la3", {
initialValue: job.rate_la3
})(<InputNumber name="rate_la3" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_la4")}>
{getFieldDecorator("rate_la4", {
initialValue: job.rate_la4
})(<InputNumber name="rate_la4" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_mapa")}>
{getFieldDecorator("rate_mapa", {
initialValue: job.rate_mapa
})(<InputNumber name="rate_mapa" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_mash")}>
{getFieldDecorator("rate_mash", {
initialValue: job.rate_mash
})(<InputNumber name="rate_mash" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_mahw")}>
{getFieldDecorator("rate_mahw", {
initialValue: job.rate_mahw
})(<InputNumber name="rate_mahw" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_ma2s")}>
{getFieldDecorator("rate_ma2s", {
initialValue: job.rate_ma2s
})(<InputNumber name="rate_ma2s" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_ma3s")}>
{getFieldDecorator("rate_ma3s", {
initialValue: job.rate_ma3s
})(<InputNumber name="rate_ma3s" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_mabl")}>
{getFieldDecorator("rate_mabl", {
initialValue: job.rate_mabl
})(<InputNumber name="rate_mabl" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_macs")}>
{getFieldDecorator("rate_macs", {
initialValue: job.rate_macs
})(<InputNumber name="rate_macs" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_matd")}>
{getFieldDecorator("rate_matd", {
initialValue: job.rate_matd
})(<InputNumber name="rate_matd" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_laa")}>
{getFieldDecorator("rate_laa", {
initialValue: job.rate_laa
})(<InputNumber name="rate_laa" />)}
</Form.Item>
</div>
);
}

View File

@@ -1,8 +1,12 @@
import {
Avatar,
Badge,
Button,
Checkbox,
Descriptions,
Dropdown,
Icon,
Menu,
notification,
PageHeader,
Tag
@@ -10,57 +14,71 @@ import {
import React from "react";
import { useTranslation } from "react-i18next";
import Moment from "react-moment";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import CarImage from "../../assets/car.svg";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import BarcodePopup from "../barcode-popup/barcode-popup.component";
export default function JobsDetailHeader({
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
export default connect(
mapStateToProps,
null
)(function JobsDetailHeader({
job,
mutationConvertJob,
refetch,
getFieldDecorator
handleSubmit,
scheduleModalState,
bodyshop,
updateJobStatus
}) {
const { t } = useTranslation();
const setscheduleModalVisible = scheduleModalState[1];
const tombstoneTitle = (
<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 : t("general.labels.na")
}`}
</div>
);
const tombstoneSubtitle = (
<div>
<Tag color="red">
{job.owner ? (
<Link to={`/manage/owners/${job.owner.id}`}>
{`${job.ownr_co_nm || ""}${job.ownr_fn || ""} ${job.ownr_ln || ""}`}
</Link>
) : (
t("jobs.errors.noowner")
)}
</Tag>
<Tag color="green">
{job.vehicle ? (
<Link to={`/manage/vehicles/${job.vehicle.id}`}>
{job.vehicle.v_model_yr || t("general.labels.na")}{" "}
{job.vehicle.v_make_desc || t("general.labels.na")}{" "}
{job.vehicle.v_model_desc || t("general.labels.na")} |{" "}
{job.vehicle.plate_no || t("general.labels.na")} |{" "}
{job.vehicle.v_vin || t("general.labels.na")}
</Link>
) : null}
</Tag>
</div>
const statusmenu = (
<Menu
onClick={e => {
updateJobStatus(e.key);
}}>
{bodyshop.md_ro_statuses.statuses.map(item => (
<Menu.Item key={item}>{item}</Menu.Item>
))}
</Menu>
);
const menuExtra = [
<Dropdown overlay={statusmenu} key='changestatus'>
<Button>
{t("jobs.actions.changestatus")} <Icon type='down' />
</Button>
</Dropdown>,
<Badge key='schedule' count={job.appointments_aggregate.aggregate.count}>
<Button
//TODO Enabled logic based on status.
onClick={() => {
setscheduleModalVisible(true);
}}>
{t("jobs.actions.schedule")}
</Button>
</Badge>,
<Button
key="convert"
type="dashed"
key='convert'
type='dashed'
disabled={job.converted}
onClick={() => {
mutationConvertJob({
@@ -72,11 +90,14 @@ export default function JobsDetailHeader({
message: t("jobs.successes.converted")
});
});
}}
>
}}>
{t("jobs.actions.convert")}
</Button>,
<Button type="primary" key="submit" htmlType="submit">
<Button
type='primary'
key='submit'
htmlType='button'
onClick={handleSubmit}>
{t("general.labels.save")}
</Button>
];
@@ -87,19 +108,38 @@ export default function JobsDetailHeader({
border: "1px solid rgb(235, 237, 240)"
}}
title={tombstoneTitle}
subTitle={tombstoneSubtitle}
//subTitle={tombstoneSubtitle}
tags={
<span key="job-status">
{job.job_status ? (
<Tag color="blue">{job.job_status.name}</Tag>
) : null}
<span key='job-status'>
{job.status ? <Tag color='blue'>{job.status}</Tag> : null}
<Tag color='red'>
{job.owner ? (
<Link to={`/manage/owners/${job.owner.id}`}>
{`${job.ownr_co_nm || ""}${job.ownr_fn || ""} ${job.ownr_ln ||
""}`}
</Link>
) : (
t("jobs.errors.noowner")
)}
</Tag>
<Tag color='green'>
{job.vehicle ? (
<Link to={`/manage/vehicles/${job.vehicle.id}`}>
{job.vehicle.v_model_yr || t("general.labels.na")}{" "}
{job.vehicle.v_make_desc || t("general.labels.na")}{" "}
{job.vehicle.v_model_desc || t("general.labels.na")} |{" "}
{job.vehicle.plate_no || t("general.labels.na")} |{" "}
{job.vehicle.v_vin || t("general.labels.na")}
</Link>
) : null}
</Tag>
<BarcodePopup value={job.id} />
</span>
}
extra={menuExtra}
>
<Descriptions size="small" column={5}>
extra={menuExtra}>
<Descriptions size='small' column={5}>
<Descriptions.Item label={t("jobs.fields.repairtotal")}>
<CurrencyFormatter>{job.claim_total}</CurrencyFormatter>
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
</Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.customerowing")}>
@@ -112,7 +152,7 @@ export default function JobsDetailHeader({
<Descriptions.Item label={t("jobs.fields.scheduled_completion")}>
{job.scheduled_completion ? (
<Moment format="MM/DD/YYYY">{job.scheduled_completion}</Moment>
<Moment format='MM/DD/YYYY'>{job.scheduled_completion}</Moment>
) : null}
</Descriptions.Item>
@@ -122,4 +162,4 @@ export default function JobsDetailHeader({
</Descriptions>
</PageHeader>
);
}
});

View File

@@ -11,8 +11,6 @@ export default function JobsDetailInsurance({ job }) {
const { getFieldDecorator, getFieldValue } = form;
const { t } = useTranslation();
console.log("job.loss_date", job.loss_date);
return (
<div>
<Form.Item label={t("jobs.fields.ins_co_id")}>

View File

@@ -0,0 +1,41 @@
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
toggleModalVisible,
setModalContext
} from "../../redux/modals/modals.actions";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = dispatch => ({
toggleModalVisible: () => dispatch(toggleModalVisible("invoiceEnter")),
setInvoiceEnterContext: context =>
dispatch(setModalContext({ context: context, modal: "invoiceEnter" }))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(function JobsDetailPliComponent({
toggleModalVisible,
setInvoiceEnterContext,
job
}) {
return (
<div>
<div
onClick={() => {
setInvoiceEnterContext({
actions: { refetch: null },
context: {
job
}
});
}}
>
Enter Invoice
</div>
</div>
);
});

View File

@@ -0,0 +1,7 @@
import React from "react";
import JobsDetailPliComponent from "./jobs-detail-pli.component";
export default function JobsDetailPliContainer({ job }) {
console.log("job", job);
return <JobsDetailPliComponent job={job} />;
}

View File

@@ -1,13 +1,22 @@
import React from "react";
import { useQuery } from "react-apollo";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_SHOP_ID } from "../../graphql/bodyshop.queries";
import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
import { selectCurrentUser } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import JobDocuments from "./jobs-documents.component";
import { GET_CURRENT_USER } from "../../graphql/local.queries";
export default function JobsDocumentsContainer({ jobId }) {
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser
});
export default connect(
mapStateToProps,
null
)(function JobsDocumentsContainer({ jobId, currentUser }) {
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
variables: { jobId: jobId },
fetchPolicy: "network-only"
@@ -17,14 +26,12 @@ export default function JobsDocumentsContainer({ jobId }) {
fetchPolicy: "network-only"
});
const user = useQuery(GET_CURRENT_USER);
if (loading || shopData.loading || user.loading) return <LoadingSpinner />;
if (error || shopData.error || user.error)
if (loading || shopData.loading) return <LoadingSpinner />;
if (error || shopData.error)
return (
<AlertComponent
type='error'
message={error.message || shopData.error.message || user.error.message}
type="error"
message={error.message || shopData.error.message}
/>
);
@@ -32,12 +39,10 @@ export default function JobsDocumentsContainer({ jobId }) {
<JobDocuments
data={data.documents}
jobId={jobId}
currentUser={user.data.currentUser}
currentUser={currentUser}
shopId={
shopData.data?.bodyshops[0]?.id
? shopData.data?.bodyshops[0]?.id
: "error"
shopData.data.bodyshops[0].id ? shopData.data.bodyshops[0].id : "error"
}
/>
);
}
});

View File

@@ -0,0 +1,172 @@
import { Checkbox, Divider, Table } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import PhoneFormatter from "../../utils/PhoneFormatter";
export default function JobsFindModalComponent({
selectedJob,
setSelectedJob,
jobsList,
jobsListLoading,
importOptionsState
}) {
const { t } = useTranslation();
const [importOptions, setImportOptions] = importOptionsState;
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
width: "8%",
render: (text, record) => (
<span>
<Link to={"/manage/jobs/" + record.id}>
{record.ro_number ? record.ro_number : "EST-" + record.est_number}
</Link>
</span>
)
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
width: "25%",
render: (text, record) => {
return record.owner ? (
<Link to={"/manage/owners/" + record.owner.id}>
{record.ownr_fn} {record.ownr_ln}
</Link>
) : (
// t("jobs.errors.noowner")
<span>{`${record.ownr_fn} ${record.ownr_ln}`}</span>
);
}
},
{
title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
width: "12%",
ellipsis: true,
render: (text, record) => {
return record.ownr_ph1 ? (
<PhoneFormatter>{record.ownr_ph1}</PhoneFormatter>
) : (
t("general.labels.unknown")
);
}
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
width: "10%",
ellipsis: true,
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.vehicle ? (
<Link to={"/manage/vehicles/" + record.vehicle.id}>
{record.vehicle.v_model_yr} {record.vehicle.v_make_desc}{" "}
{record.vehicle.v_model_desc}
</Link>
) : (
t("jobs.errors.novehicle")
);
}
},
{
title: t("vehicles.fields.plate_no"),
dataIndex: "plate_no",
key: "plate_no",
width: "8%",
ellipsis: true,
render: (text, record) => {
return record.vehicle.plate_no ? (
<span>{record.vehicle.plate_no}</span>
) : (
t("general.labels.unknown")
);
}
},
{
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
width: "12%",
ellipsis: true,
render: (text, record) => {
return record.clm_no ? (
<span>{record.clm_no}</span>
) : (
t("general.labels.unknown")
);
}
}
];
const handleOnRowClick = record => {
if (record) {
if (record.id) {
setSelectedJob(record.id);
return;
}
}
setSelectedJob(null);
};
return (
<div>
<Table
title={() => t("jobs.labels.existing_jobs")}
size="small"
pagination={{ position: "bottom" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
loading={jobsListLoading}
dataSource={jobsList}
rowSelection={{
onSelect: props => {
setSelectedJob(props.id);
},
type: "radio",
selectedRowKeys: [selectedJob]
}}
onRow={(record, rowIndex) => {
return {
onClick: event => {
handleOnRowClick(record);
}
};
}}
/>
<Divider />
<Checkbox
defaultChecked={importOptions.overrideHeader}
onChange={e =>
setImportOptions({
...importOptions,
overrideHeader: e.target.checked
})
}
>
{t("jobs.labels.override_header")}
</Checkbox>
</div>
);
}

View File

@@ -0,0 +1,58 @@
import { Modal } from "antd";
import React from "react";
import { useQuery } from "react-apollo";
import { useTranslation } from "react-i18next";
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import JobsFindModalComponent from "./jobs-find-modal.component";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
export default connect(
mapStateToProps,
null
)(function JobsFindModalContainer({
bodyshop,
loading,
error,
selectedJob,
setSelectedJob,
importOptionsState,
...modalProps
}) {
const { t } = useTranslation();
const jobsList = useQuery(QUERY_ALL_ACTIVE_JOBS, {
fetchPolicy: "network-only",
variables: {
statuses: bodyshop.md_ro_statuses.open_statuses || ["Open"]
}
});
return (
<Modal
title={t("jobs.labels.existing_jobs")}
width={"80%"}
okButtonProps={{ disabled: selectedJob ? false : true }}
{...modalProps}>
{loading ? <LoadingSpinner /> : null}
{error ? <AlertComponent message={error.message} type='error' /> : null}
{true ? (
<JobsFindModalComponent
selectedJob={selectedJob}
setSelectedJob={setSelectedJob}
importOptionsState={importOptionsState}
jobsListLoading={jobsList.loading}
jobsList={
jobsList.data && jobsList.data.jobs ? jobsList.data.jobs : null
}
/>
) : null}
</Modal>
);
});

View File

@@ -1,12 +1,15 @@
import { Input, Table, Icon } from "antd";
import { Button, Icon, Input, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { Link, withRouter } from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import PhoneFormatter from "../../utils/PhoneFormatter";
import { alphaSort } from "../../utils/sorters";
import { withRouter } from "react-router-dom";
import StartChatButton from "../chat-open-button/chat-open-button.component";
export default withRouter(function JobsList({
searchTextState,
refetch,
loading,
jobs,
selectedJob,
@@ -20,6 +23,7 @@ export default withRouter(function JobsList({
const { t } = useTranslation();
const setSearchText = searchTextState[1];
const columns = [
{
title: t("jobs.fields.ro_number"),
@@ -28,7 +32,11 @@ export default withRouter(function JobsList({
width: "8%",
// onFilter: (value, record) => record.ro_number.includes(value),
// filteredValue: state.filteredInfo.text || null,
sorter: (a, b) => alphaSort(a, b),
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,
@@ -56,14 +64,12 @@ export default withRouter(function JobsList({
</Link>
) : (
// t("jobs.errors.noowner")
<span>
{record.ownr_fn} {record.ownr_ln}
</span>
<span>{`${record.ownr_fn} ${record.ownr_ln}`}</span>
);
}
},
{
title: t("jobs.fields.phone1"),
title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
width: "12%",
@@ -72,13 +78,7 @@ export default withRouter(function JobsList({
return record.ownr_ph1 ? (
<span>
<PhoneFormatter>{record.ownr_ph1}</PhoneFormatter>
<Icon
style={{ margin: 4 }}
type='message'
onClick={() => {
alert("SMSing will happen here.");
}}
/>
<StartChatButton phone={record.ownr_ph1} />
</span>
) : (
t("general.labels.unknown")
@@ -91,11 +91,11 @@ export default withRouter(function JobsList({
key: "status",
width: "10%",
ellipsis: true,
sorter: (a, b) => alphaSort(a, b),
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => {
return record.job_status?.name || t("general.labels.na");
return record.status || t("general.labels.na");
}
},
@@ -107,7 +107,7 @@ export default withRouter(function JobsList({
ellipsis: true,
render: (text, record) => {
return record.vehicle ? (
<Link to={"manage/vehicles/" + record.vehicle.id}>
<Link to={"/manage/vehicles/" + record.vehicle.id}>
{record.vehicle.v_model_yr} {record.vehicle.v_make_desc}{" "}
{record.vehicle.v_model_desc}
</Link>
@@ -122,11 +122,11 @@ export default withRouter(function JobsList({
key: "plate_no",
width: "8%",
ellipsis: true,
sorter: (a, b) => alphaSort(a, b),
sorter: (a, b) => alphaSort(a.vehicle.plate_no, b.vehicle.plate_no),
sortOrder:
state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
render: (text, record) => {
return record.vehicle?.plate_no ? (
return record.vehicle.plate_no ? (
<span>{record.vehicle.plate_no}</span>
) : (
t("general.labels.unknown")
@@ -139,7 +139,7 @@ export default withRouter(function JobsList({
key: "clm_no",
width: "12%",
ellipsis: true,
sorter: (a, b) => alphaSort(a, b),
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
render: (text, record) => {
@@ -154,15 +154,13 @@ export default withRouter(function JobsList({
title: t("jobs.fields.clm_total"),
dataIndex: "clm_total",
key: "clm_total",
width: "8%",
// sorter: (a, b) => {
// return a > b;
// },
// sortOrder:
// state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
width: "10%",
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
render: (text, record) => {
return record.clm_total ? (
<span>{record.clm_total}</span>
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
) : (
t("general.labels.unknown")
);
@@ -196,7 +194,6 @@ export default withRouter(function JobsList({
if (record) {
if (record.id) {
setSelectedJob(record.id);
history.push(`#${record.id}`);
return;
}
}
@@ -209,19 +206,24 @@ export default withRouter(function JobsList({
loading={loading}
title={() => {
return (
<Input.Search
placeholder='Search...'
onSearch={value => {
console.log(value);
}}
enterButton
/>
<div style={{ display: "flex" }}>
<Button onClick={() => refetch()}>
<Icon type="sync" />
</Button>
<Input.Search
placeholder="Search..."
onChange={e => {
setSearchText(e.target.value);
}}
enterButton
/>
</div>
);
}}
size='small'
size="small"
pagination={{ position: "top" }}
columns={columns.map(item => ({ ...item }))}
rowKey='id'
rowKey="id"
dataSource={jobs}
rowSelection={{ selectedRowKeys: [selectedJob] }}
onChange={handleTableChange}

View File

@@ -4,5 +4,5 @@ import "./loading-skeleton.styles.scss";
import { Skeleton } from "antd";
export default function LoadingSkeleton(props) {
return <Skeleton {...props} className='loading-skeleton' active />;
return <Skeleton {...props} className="loading-skeleton" active />;
}

View File

@@ -8,7 +8,11 @@ export default function LoadingSpinner({ loading = true, message, ...props }) {
spinning={loading}
className="loading-spinner"
size="large"
//delay="500"
style={{
position: "relative",
alignContent: "center"
}}
delay={200}
tip={message ? message : null}
>
{props.children}

View File

@@ -1,3 +1,2 @@
.loading-spinner {
text-align: center;
}

View File

@@ -1,34 +1,27 @@
import React from "react";
import { useQuery } from "react-apollo";
import { Link } from "react-router-dom";
import { GET_CURRENT_USER } from "../../graphql/local.queries";
import { Icon } from "antd";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import React from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { selectCurrentUser } from "../../redux/user/user.selectors";
export default function ManageSignInButton() {
const {
loading,
error,
data: { currentUser }
} = useQuery(GET_CURRENT_USER);
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser
});
if (loading) return <LoadingSpinner />;
if (error) return error.message;
return currentUser ? (
<div>
{" "}
<Link to="/manage">
<Icon type="build" />
Manage
</Link>
</div>
export default connect(
mapStateToProps,
null
)(function ManageSignInButton({ currentUser }) {
return currentUser.authorized ? (
<Link to="/manage">
<Icon type="build" />
Manage
</Link>
) : (
<div>
<Link to="/signin">
<Icon type="login" />
Sign In
</Link>
</div>
<Link to="/signin">
<Icon type="login" />
Sign In
</Link>
);
}
});

View File

@@ -14,7 +14,7 @@ export default function NoteUpsertModalComponent({
return (
<Modal
title={noteState?.id ? t("notes.actions.edit") : t("notes.actions.new")}
title={noteState.id ? t("notes.actions.edit") : t("notes.actions.new")}
visible={visible}
okText={t("general.labels.save")}
onOk={() => {
@@ -22,7 +22,8 @@ export default function NoteUpsertModalComponent({
}}
onCancel={() => {
changeVisibility(false);
}}>
}}
>
<div>
{t("notes.fields.critical")}
<Switch

View File

@@ -5,12 +5,20 @@ import { useTranslation } from "react-i18next";
import { INSERT_NEW_NOTE, UPDATE_NOTE } from "../../graphql/notes.queries";
import NoteUpsertModalComponent from "./note-upsert-modal.component";
export default function NoteUpsertModalContainer({
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectCurrentUser } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser
});
export default connect (mapStateToProps,null)( function NoteUpsertModalContainer({
jobId,
visible,
changeVisibility,
refetch,
existingNote
existingNote,currentUser
}) {
const { t } = useTranslation();
const [insertNote] = useMutation(INSERT_NEW_NOTE);
@@ -33,7 +41,7 @@ export default function NoteUpsertModalContainer({
insertNote({
variables: {
noteInput: [
{ ...noteState, jobid: jobId, created_by: "patrick@bodyshop.app" } //TODO: Fix the created by.
{ ...noteState, jobid: jobId, created_by: currentUser.email }
]
}
}).then(r => {
@@ -73,3 +81,4 @@ export default function NoteUpsertModalContainer({
/>
);
}
);

View File

@@ -0,0 +1,106 @@
import { Button, Col, Form, Input, Row, Switch } 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";
import ResetForm from "../form-items-formatted/reset-form-item.component";
export default function OwnerDetailFormComponent({ form, owner }) {
const { t } = useTranslation();
const {
isFieldsTouched,
resetFields,
getFieldDecorator,
getFieldValue
} = form;
return (
<div>
{isFieldsTouched() ? <ResetForm resetFields={resetFields} /> : null}
<Button type="primary" key="submit" htmlType="submit">
{t("general.labels.save")}
</Button>
<Row>
<Col span={8}>
<Form.Item label={t("owners.fields.ownr_ln")}>
{getFieldDecorator("ownr_ln", {
initialValue: owner.ownr_ln
})(<Input name="ownr_ln" />)}
</Form.Item>
<Form.Item label={t("owners.fields.ownr_fn")}>
{getFieldDecorator("ownr_fn", {
initialValue: owner.ownr_fn
})(<Input name="ownr_fn" />)}
</Form.Item>
<Form.Item label={t("owners.fields.allow_text_message")}>
{getFieldDecorator("allow_text_message", {
initialValue: owner.allow_text_message,
valuePropName: "checked"
})(<Switch name="allow_text_message" />)}
</Form.Item>
<Form.Item label={t("owners.fields.ownr_addr1")}>
{getFieldDecorator("ownr_addr1", {
initialValue: owner.ownr_addr1
})(<Input name="ownr_addr1" />)}
</Form.Item>
<Form.Item label={t("owners.fields.ownr_addr2")}>
{getFieldDecorator("ownr_addr2", {
initialValue: owner.ownr_addr2
})(<Input name="ownr_addr2" />)}
</Form.Item>
<Form.Item label={t("owners.fields.ownr_city")}>
{getFieldDecorator("ownr_city", {
initialValue: owner.ownr_city
})(<Input name="ownr_city" />)}
</Form.Item>
<Form.Item label={t("owners.fields.ownr_ctry")}>
{getFieldDecorator("ownr_ctry", {
initialValue: owner.ownr_ctry
})(<Input name="ownr_ctry" />)}
</Form.Item>
</Col>
<Col span={8}>
{" "}
<Form.Item label={t("owners.fields.ownr_ea")}>
{getFieldDecorator("ownr_ea", {
initialValue: owner.ownr_ea,
rules: [
{
type: "email",
message: "This is not a valid email address."
}
]
})(
<FormItemEmail name="ownr_ea" email={getFieldValue("ownr_ea")} />
)}
</Form.Item>
<Form.Item label={t("owners.fields.ownr_ph1")}>
{getFieldDecorator("ownr_ph1", {
initialValue: owner.ownr_ph1
})(<FormItemPhone customInput={Input} name="ownr_ph1" />)}
</Form.Item>
<Form.Item label={t("owners.fields.ownr_st")}>
{getFieldDecorator("ownr_st", {
initialValue: owner.ownr_st
})(<Input name="ownr_st" />)}
</Form.Item>
<Form.Item label={t("owners.fields.ownr_zip")}>
{getFieldDecorator("ownr_zip", {
initialValue: owner.ownr_zip
})(<Input name="ownr_zip" />)}
</Form.Item>
<Form.Item label={t("owners.fields.preferred_contact")}>
{getFieldDecorator("preferred_contact", {
initialValue: owner.preferred_contact
})(<Input name="preferred_contact" />)}
</Form.Item>
<Form.Item label={t("owners.fields.ownr_title")}>
{getFieldDecorator("ownr_title", {
initialValue: owner.ownr_title
})(<Input name="ownr_title" />)}
</Form.Item>
</Col>
</Row>
</div>
);
}

View File

@@ -0,0 +1,46 @@
import { Form, notification } from "antd";
import React from "react";
import { useMutation } from "react-apollo";
import { useTranslation } from "react-i18next";
import { UPDATE_OWNER } from "../../graphql/owners.queries";
import OwnerDetailFormComponent from "./owner-detail-form.component";
function OwnerDetailFormContainer({ form, owner, refetch }) {
const { t } = useTranslation();
const [updateOwner] = useMutation(UPDATE_OWNER);
const handleSubmit = e => {
e.preventDefault();
form.validateFieldsAndScroll((err, values) => {
if (err) {
notification["error"]({
message: t("owners.errors.validationtitle"),
description: t("owners.errors.validation")
});
}
if (!err) {
updateOwner({
variables: { ownerId: owner.id, owner: values }
}).then(r => {
notification["success"]({
message: t("owners.successes.save")
});
//TODO Better way to reset the field decorators?
if (refetch) refetch().then();
form.resetFields();
});
}
});
};
return (
<Form onSubmit={handleSubmit} autoComplete="off">
<OwnerDetailFormComponent form={form} owner={owner} />
</Form>
);
}
export default Form.create({ name: "OwnerDetailFormContainer" })(
OwnerDetailFormContainer
);

View File

@@ -0,0 +1,59 @@
import React from "react";
import { Table } from "antd";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
export default function OwnerDetailJobsComponent({ owner }) {
const { t } = useTranslation();
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
ellipsis: true,
render: (text, record) => (
<Link to={`/manage/jobs/${record.id}`}>
{record.ro_number ? record.ro_number : `EST ${record.est_number}`}
</Link>
)
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "owner",
key: "owner",
render: (text, record) => (
<Link to={`/manage/vehicles/${record.vehicle.id}`}>
{`${record.vehicle.v_model_yr} ${record.vehicle.v_make_desc} ${record.vehicle.v_model_desc}`}
</Link>
)
},
{
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no"
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status"
},
{
title: t("jobs.fields.clm_total"),
dataIndex: "clm_total",
key: "clm_total",
render: (text, record) => (
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
)
}
];
return (
<Table
pagination={{ position: "bottom" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={owner.jobs}
/>
);
}

View File

@@ -118,7 +118,8 @@ export default function OwnerFindModalComponent({
checked={selectedOwner ? false : true}
onClick={() => setSelectedOwner(null)}
>
Create a new Owner record for this job.
{t("owners.labels.create_new")}
</Checkbox>
</div>
);

View File

@@ -20,9 +20,7 @@ export default function OwnerFindModalContainer({
const ownersList = useQuery(QUERY_SEARCH_OWNER_BY_IDX, {
variables: {
search: owner
? `${owner.ownr_fn} ${owner.ownr_ln} ${owner.ownr_addr1} ${owner.ownr_city} ${owner.ownr_zip} ${owner.ownr_ea} ${owner.ownr_ph1} ${owner.ownr_ph2}`
: null
search: owner ? `${owner.ownr_fn || ""} ${owner.ownr_ln || ""}` : null
},
skip: !owner,
fetchPolicy: "network-only"
@@ -32,18 +30,17 @@ export default function OwnerFindModalContainer({
<Modal
title={t("owners.labels.existing_owners")}
width={"80%"}
{...modalProps}
>
{...modalProps}>
{loading ? <LoadingSpinner /> : null}
{error ? <AlertComponent message={error.message} type="error" /> : null}
{error ? <AlertComponent message={error.message} type='error' /> : null}
{owner ? (
<OwnerFindModalComponent
selectedOwner={selectedOwner}
setSelectedOwner={setSelectedOwner}
ownersListLoading={ownersList.loading}
ownersList={
ownersList.data && ownersList.data.search_owners
? ownersList.data.search_owners
ownersList.data && ownersList.data.search_owner
? ownersList.data.search_owner
: null
}
/>

View File

@@ -0,0 +1,89 @@
import { Input, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import PhoneFormatter from "../../utils/PhoneFormatter";
import { alphaSort } from "../../utils/sorters";
export default function OwnersListComponent({ loading, owners, refetch }) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" }
});
const { t } = useTranslation();
const columns = [
{
title: t("owners.fields.name"),
dataIndex: "name",
key: "name",
// onFilter: (value, record) => record.ro_number.includes(value),
// filteredValue: state.filteredInfo.text || null,
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sortOrder:
state.sortedInfo.columnKey === "name" && state.sortedInfo.order,
render: (text, record) => (
<Link to={"/manage/owners/" + record.id}>
{`${record.ownr_fn} ${record.ownr_ln}`}
</Link>
)
},
{
title: t("owners.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
sorter: (a, b) => alphaSort(a.ownr_ph1, b.ownr_ph1),
sortOrder:
state.sortedInfo.columnKey === "ownr_ph1" && state.sortedInfo.order,
render: (text, record) => {
return <PhoneFormatter>{record.ownr_ph1}</PhoneFormatter>;
}
},
{
title: t("owners.fields.ownr_ea"),
dataIndex: "ownr_ea",
key: "ownr_ea"
},
{
title: t("owners.fields.address"),
dataIndex: "address",
key: "address",
render: (text, record) => {
return (
<div>{`${record.ownr_addr1 || ""} ${record.ownr_addr2 ||
""} ${record.ownr_city || ""} ${record.ownr_st ||
""} ${record.ownr_zip || ""}`}</div>
);
}
}
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
//TODO Implement searching & pagination
return (
<Table
loading={loading}
title={() => {
return (
<Input.Search
placeholder="Search..."
onSearch={value => {
console.log(value);
}}
enterButton
/>
);
}}
size="small"
pagination={{ position: "top" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={owners}
onChange={handleTableChange}
/>
);
}

View File

@@ -0,0 +1,20 @@
import React from "react";
import { useQuery } from "react-apollo";
import { QUERY_ALL_OWNERS } from "../../graphql/owners.queries";
import AlertComponent from "../alert/alert.component";
import OwnersListComponent from "./owners-list.component";
export default function OwnersListContainer() {
const { loading, error, data, refetch } = useQuery(QUERY_ALL_OWNERS, {
fetchPolicy: "network-only"
});
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<OwnersListComponent
loading={loading}
owners={data ? data.owners : null}
refetch={refetch}
/>
);
}

View File

@@ -0,0 +1,90 @@
import { AutoComplete, DatePicker, Icon, Input, List, Radio } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
export default function PartsOrderModalComponent({
vendorList,
state,
sendTypeState,
orderLinesState
}) {
const [partsOrder, setPartsOrder] = state;
const [sendType, setSendType] = sendTypeState;
const orderLines = orderLinesState[0];
const [vendorComplete, setVendorComplete] = useState(vendorList);
const { t } = useTranslation();
const handleSearch = value => {
if (value === "") setVendorComplete(vendorList);
else
setVendorComplete(
vendorList.filter(v =>
v.name.toLowerCase().includes(value.toLowerCase())
)
);
};
const handleSelect = (value, option) => {
setPartsOrder({ ...partsOrder, vendorid: option.key });
};
return (
<div>
<AutoComplete
onSearch={handleSearch}
onSelect={handleSelect}
defaultOpen
backfill
optionLabelProp='value'
dataSource={vendorComplete}
placeholder={t("vendors.labels.search")}>
{vendorComplete.map(v => (
<AutoComplete.Option value={v.name} key={v.id}>
<div>{v.name}</div>
<div> {v.favorite ? <Icon type='heart' /> : null}</div>
</AutoComplete.Option>
))}
</AutoComplete>
{t("parts_orders.fields.deliver_by")}
<DatePicker
defaultValue={partsOrder.deliver_by}
onChange={e => {
setPartsOrder({ ...partsOrder, deliver_by: e });
}}
/>
{t("parts_orders.labels.inthisorder")}
<List
itemLayout='horizontal'
dataSource={orderLines}
renderItem={item => (
<List.Item
actions={[
<Input placeholder={t("parts_orders.fields.lineremarks")} />
//TODO Editable table/adding line remarks to the order.
]}>
{
// <List.Item.Meta
// avatar={
// <Avatar src='https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png' />
// }
// title={<a href='https://ant.design'>{item.name.last}</a>}
// description='Ant Design, a design language for background applications, is refined by Ant UED Team'
// />
}
<div>{`${item.line_desc}${
item.oem_partno ? " | " + item.oem_partno : ""
}`}</div>
</List.Item>
)}
/>
<Radio.Group
defaultValue={sendType}
onChange={e => setSendType(e.target.value)}>
<Radio value={"e"}>{t("parts_orders.labels.email")}</Radio>
<Radio value={"p"}>{t("parts_orders.labels.print")}</Radio>
</Radio.Group>
</div>
);
}

View File

@@ -0,0 +1,188 @@
import { Modal, notification } from "antd";
import React, { useState, useEffect } from "react";
import { useMutation, useQuery } from "react-apollo";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_NEW_PARTS_ORDERS } from "../../graphql/parts-orders.queries";
import { UPDATE_JOB_LINE_STATUS } from "../../graphql/jobs-lines.queries";
import { QUERY_ALL_VENDORS_FOR_ORDER } from "../../graphql/vendors.queries";
import {
selectBodyshop,
selectCurrentUser
} from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import PartsOrderModalComponent from "./parts-order-modal.component";
import {
setEmailOptions,
toggleEmailOverlayVisible
} from "../../redux/email/email.actions";
import PartsOrderEmailTemplate from "../../emails/parts-order/parts-order.email";
import { REPORT_QUERY_PARTS_ORDER_BY_PK } from "../../emails/parts-order/parts-order.query";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
bodyshop: selectBodyshop
});
const mapDispatchToProps = dispatch => ({
setEmailOptions: e => dispatch(setEmailOptions(e)),
toggleEmailOverlayVisible: () => dispatch(toggleEmailOverlayVisible())
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(function PartsOrderModalContainer({
partsOrderModalVisible,
linesToOrder,
jobId,
currentUser,
bodyshop,
refetch,
setEmailOptions,
toggleEmailOverlayVisible
}) {
const { t } = useTranslation();
const [modalVisible, setModalVisible] = partsOrderModalVisible;
//set order lines to be a version of the incoming lines.
const orderLinesState = useState(
linesToOrder.reduce((acc, value) => {
acc.push({
line_desc: value.line_desc,
oem_partno: value.oem_partno,
db_price: value.db_price,
act_price: value.act_price,
line_remarks: "Alalala",
job_line_id: value.id,
status: bodyshop.md_order_statuses.default_ordered || "Ordered*"
});
return acc;
}, [])
);
const [orderLines, setOrderLinesState] = orderLinesState;
useEffect(() => {
if (modalVisible)
setOrderLinesState(
linesToOrder.reduce((acc, value) => {
acc.push({
line_desc: value.line_desc,
oem_partno: value.oem_partno,
db_price: value.db_price,
act_price: value.act_price,
line_remarks: "Alalala",
job_line_id: value.id,
status: bodyshop.md_order_statuses.default_ordered || "Ordered*"
});
return acc;
}, [])
);
}, [
modalVisible,
setOrderLinesState,
linesToOrder,
bodyshop.md_order_statuses.default_ordered
]);
const sendTypeState = useState("e");
const sendType = sendTypeState[0];
const partsOrderState = useState({
vendorid: null,
jobid: jobId,
user_email: currentUser.email
});
const partsOrder = partsOrderState[0];
const { loading, error, data } = useQuery(QUERY_ALL_VENDORS_FOR_ORDER, {
fetchPolicy: "network-only",
skip: !modalVisible
});
const [insertPartOrder] = useMutation(INSERT_NEW_PARTS_ORDERS);
const [updateJobLines] = useMutation(UPDATE_JOB_LINE_STATUS);
const handleOk = () => {
insertPartOrder({
variables: {
po: [
{
...partsOrder,
status: bodyshop.md_order_statuses.default_ordered || "Ordered*",
parts_order_lines: {
data: orderLines
}
}
]
}
})
.then(r => {
updateJobLines({
variables: {
ids: linesToOrder.map(item => item.id),
status: bodyshop.md_order_statuses.default_ordered || "Ordered*"
}
})
.then(response => {
notification["success"]({
message: t("parts_orders.successes.created")
});
if (refetch) refetch();
setModalVisible(false);
if (sendType === "e") {
//Show the email modal and set the data.
//TODO Remove some of the options below.
setEmailOptions({
messageOptions: {
from: {
name: "Kavia Autobdoy",
address: "noreply@bodyshop.app"
},
to: "patrickwf@gmail.com",
replyTo: "snaptsoft@gmail.com"
},
template: PartsOrderEmailTemplate,
queryConfig: [
REPORT_QUERY_PARTS_ORDER_BY_PK,
{
variables: {
id: r.data.insert_parts_orders.returning[0].id
}
}
]
});
toggleEmailOverlayVisible();
}
})
.catch(error => {
notification["error"]({
message: t("parts_orders.errors.creating"),
description: error.message
});
});
})
.catch(error => {
notification["error"]({
message: t("parts_orders.errors.creating"),
description: error.message
});
});
};
return (
<Modal
visible={modalVisible}
onCancel={() => setModalVisible(false)}
onOk={handleOk}>
{error ? <AlertComponent message={error.message} type='error' /> : null}
<LoadingSpinner loading={loading}>
<PartsOrderModalComponent
vendorList={(data && data.vendors) || []}
state={partsOrderState}
sendTypeState={sendTypeState}
orderLinesState={orderLinesState}
/>
</LoadingSpinner>
</Modal>
);
});

View File

@@ -1,15 +1,17 @@
import React from "react";
import { useTranslation } from "react-i18next";
import AlertComponent from "../alert/alert.component";
import ProfileMyComponent from "../profile-my/profile-my.component";
import ProfileShopsContainer from "../profile-shops/profile-shops.container";
export default function ProfileContent({ sidebarSelection }) {
const { t } = useTranslation();
switch (sidebarSelection.key) {
case "profile":
return <div>Profile stuff</div>;
case "shop":
return <div>Shop stuff</div>;
return <ProfileMyComponent />;
case "shops":
return <ProfileShopsContainer />;
default:
return (
<AlertComponent message={t("profile.errors.state")} type="error" />

View File

@@ -0,0 +1,76 @@
import { Button, Form, Input, notification } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { updateUserDetails } from "../../redux/user/user.actions";
import { selectCurrentUser } from "../../redux/user/user.selectors";
import ResetForm from "../form-items-formatted/reset-form-item.component";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser
});
const mapDispatchToProps = dispatch => ({
updateUserDetails: userDetails => dispatch(updateUserDetails(userDetails))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(
Form.create({ name: "ProfileMyComponentForm" })(function ProfileMyComponent({
currentUser,
form,
updateUserDetails
}) {
const { isFieldsTouched, resetFields, getFieldDecorator } = form;
const { t } = useTranslation();
const handleSubmit = e => {
e.preventDefault();
form.validateFieldsAndScroll((err, values) => {
if (err) {
notification["error"]({
message: t("jobs.errors.validationtitle"),
description: t("jobs.errors.validation")
});
}
if (!err) {
console.log("values", values);
updateUserDetails({
displayName: values.displayname,
photoURL: values.photoURL
});
}
});
};
return (
<div>
{isFieldsTouched() ? <ResetForm resetFields={resetFields} /> : null}
<Form onSubmit={handleSubmit} autoComplete={"no"}>
<Form.Item label={t("user.fields.displayname")}>
{getFieldDecorator("displayname", {
initialValue: currentUser.displayName,
rules: [{ required: true }]
})(<Input name='displayname' />)}
</Form.Item>
<Form.Item label={t("user.fields.photourl")}>
{getFieldDecorator("photoURL", {
initialValue: currentUser.photoURL
})(<Input name='photoURL' />)}
</Form.Item>
<Button
type='primary'
key='submit'
htmlType='submit'
onClick={handleSubmit}>
{t("user.actions.updateprofile")}
</Button>
</Form>
</div>
);
})
);

View File

@@ -0,0 +1,52 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Table, Button } from "antd";
export default function ProfileShopsComponent({
loading,
data,
updateActiveShop
}) {
const { t } = useTranslation();
const columns = [
{
title: t("associations.fields.shopname"),
dataIndex: "shopname",
key: "shopname",
width: "25%",
render: (text, record) => <span>{record.bodyshop.shopname}</span>
},
{
title: t("associations.fields.active"),
dataIndex: "active",
key: "active",
width: "25%",
render: (text, record) => <span>{record.active ? "Yes" : "No"}</span>
},
{
title: t("associations.labels.actions"),
dataIndex: "actions",
key: "actions",
width: "25%",
render: (text, record) => (
<span>
{record.active ? null : (
<Button onClick={() => updateActiveShop(record.id)}>
Activate
</Button>
)}
</span>
)
}
];
return (
<Table
loading={loading}
size="small"
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={data}
/>
);
}

View File

@@ -0,0 +1,34 @@
import React from "react";
import { useQuery, useMutation } from "react-apollo";
import {
QUERY_ALL_ASSOCIATIONS,
UPDATE_ASSOCIATION
} from "../../graphql/associations.queries";
import AlertComponent from "../alert/alert.component";
import ProfileShopsComponent from "./profile-shops.component";
export default function ProfileShopsContainer() {
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ASSOCIATIONS);
const [updateAssocation] = useMutation(UPDATE_ASSOCIATION);
const updateActiveShop = activeShopId => {
data.associations.forEach(record => {
updateAssocation({
variables: {
assocId: record.id,
assocActive: record.id === activeShopId ? true : false
}
});
});
refetch();
};
if (error) return <AlertComponent type="error" message={error.message} />;
return (
<ProfileShopsComponent
loading={loading}
data={data ? data.associations : null}
updateActiveShop={updateActiveShop}
/>
);
}

View File

@@ -0,0 +1,67 @@
import { Checkbox, Col, DatePicker, Modal, Row, TimePicker, Input } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import ScheduleDayViewContainer from "../schedule-day-view/schedule-day-view.container";
export default function ScheduleJobModalComponent({
appData,
setAppData,
formData,
setFormData,
...props
}) {
const { t } = useTranslation();
//TODO Existing appointments list only refreshes sometimes after modal close. May have to do with the container class.
return (
<Modal
{...props}
width={"80%"}
maskClosable={false}
destroyOnClose={true}
okButtonProps={{ disabled: appData.start ? false : true }}
>
<Row>
<Col span={14}>
<Row>
Manual Job Selection Scheduled Time
<Input
placeholder={t("appointments.fields.title")}
onChange={e => {
setAppData({ ...appData, title: e.target.value });
}}
/>
<DatePicker
value={appData.start}
onChange={e => {
setAppData({ ...appData, start: e });
}}
/>
<TimePicker
value={appData.start}
format={"HH:mm"}
minuteStep={15}
onChange={e => {
setAppData({ ...appData, start: e });
}}
/>
</Row>
{
//TODO Build out notifications.
}
<Checkbox
defaultChecked={formData.notifyCustomer}
onChange={e =>
setFormData({ ...formData, notifyCustomer: e.target.checked })
}
>
{t("jobs.labels.appointmentconfirmation")}
</Checkbox>
</Col>
<Col span={10}>
<ScheduleDayViewContainer day={appData.start} />
</Col>
</Row>
</Modal>
);
}

View File

@@ -0,0 +1,74 @@
import { notification } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useMutation } from "react-apollo";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_APPOINTMENT } from "../../graphql/appointments.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ScheduleAppointmentModalComponent from "./schedule-appointment-modal.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
export default connect(
mapStateToProps,
null
)(function ScheduleAppointmentModalContainer({
scheduleModalState,
jobId,
bodyshop,
refetch
}) {
const [scheduleModalVisible, setscheduleModalVisible] = scheduleModalState;
const [appData, setAppData] = useState({
jobid: jobId,
bodyshopid: bodyshop.id,
isintake: false,
start: null
});
const [insertAppointment] = useMutation(INSERT_APPOINTMENT);
const [formData, setFormData] = useState({ notifyCustomer: false });
const { t } = useTranslation();
return (
<ScheduleAppointmentModalComponent
appData={appData}
setAppData={setAppData}
formData={formData}
setFormData={setFormData}
//Spreadable Modal Props
visible={scheduleModalVisible}
onCancel={() => setscheduleModalVisible(false)}
onOk={() => {
//TODO Customize the amount of minutes it will add.
insertAppointment({
variables: {
app: { ...appData, end: moment(appData.start).add(60, "minutes") }
}
})
.then(r => {
notification["success"]({
message: t("appointments.successes.created")
});
if (formData.notifyCustomer) {
//TODO Implement customer reminder on scheduling.
alert("Chosed to notify the customer somehow!");
}
setscheduleModalVisible(false);
if (refetch) refetch();
})
.catch(error => {
notification["error"]({
message: t("appointments.errors.saving", {
message: error.message
})
});
});
}}
/>
);
});

View File

@@ -0,0 +1 @@
@import 'react-big-calendar/lib/sass/styles';

View File

@@ -0,0 +1,34 @@
import moment from "moment";
import React from "react";
import { Calendar, momentLocalizer } from "react-big-calendar";
//import "react-big-calendar/lib/css/react-big-calendar.css";
import "./schedule-calendar.styles.scss";
import DateCellWrapper from "../schedule-datecellwrapper/schedule-datecellwrapper.component";
import Event from "../schedule-event/schedule-event.container";
const localizer = momentLocalizer(moment);
export default function ScheduleCalendarWrapperComponent({
data,
refetch,
defaultView,
...otherProps
}) {
return (
<Calendar
events={data}
defaultView={defaultView}
step={30}
showMultiDayTimes
localizer={localizer}
min={new Date("2020-01-01T06:00:00")} //TODO Read from business settings.
max={new Date("2020-01-01T20:00:00")}
components={{
event: e => {
return Event({ event: e.event, refetch: refetch });
},
dateCellWrapper: DateCellWrapper
}}
{...otherProps}
/>
);
}

View File

@@ -0,0 +1,46 @@
import React from "react";
//import "react-big-calendar/lib/css/react-big-calendar.css";
import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/scheduler-calendar-wrapper.component";
import { Button, Icon } from "antd";
import { useTranslation } from "react-i18next";
import ScheduleAppointmentModalContainer from "../schedule-appointment-modal/schedule-appointment-modal.container";
export default function ScheduleCalendarComponent({
data,
refetch,
scheduleModalState
}) {
const { t } = useTranslation();
return (
<div>
<Button
onClick={() => {
refetch();
}}
>
<Icon type="sync" />
</Button>
<Button
onClick={() => {
scheduleModalState[1](true);
}}
>
{t("appointments.actions.new")}
</Button>
<ScheduleAppointmentModalContainer
scheduleModalState={scheduleModalState}
jobId={null}
refetch={refetch}
/>
<ScheduleCalendarWrapperComponent
data={data}
defaultView="week"
refetch={refetch}
/>
</div>
);
}

View File

@@ -0,0 +1,36 @@
import React, { useState } from "react";
import { useQuery } from "react-apollo";
import ScheduleCalendarComponent from "./schedule-calendar.component";
import { QUERY_ALL_ACTIVE_APPOINTMENTS } from "../../graphql/appointments.queries";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import AlertComponent from "../alert/alert.component";
export default function ScheduleCalendarContainer() {
const { loading, error, data, refetch } = useQuery(
QUERY_ALL_ACTIVE_APPOINTMENTS,
{
fetchPolicy: "network-only"
}
);
const scheduleModalState = useState(false);
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type="error" />;
let normalizedData = data.appointments.map(e => {
//Required becuase Hasura returns a string instead of a date object.
return Object.assign(
{},
e,
{ start: new Date(e.start) },
{ end: new Date(e.end) }
);
});
return (
<ScheduleCalendarComponent
scheduleModalState={scheduleModalState}
refetch={refetch}
data={data ? normalizedData : null}
/>
);
}

View File

@@ -0,0 +1,18 @@
import React from "react";
export default function ScheduleDateCellWrapper(dateCellWrapperProps) {
// Show 'click me' text in arbitrary places by using the range prop
const style = {
display: "flex",
flex: 1,
borderLeft: "1px solid #DDD",
backgroundColor: "#fff"
};
return (
<div style={style}>
PLACEHOLDER:DATA
{dateCellWrapperProps.children}
</div>
);
}

View File

@@ -0,0 +1,21 @@
import React from "react";
import "react-big-calendar/lib/css/react-big-calendar.css";
import { useTranslation } from "react-i18next";
import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/scheduler-calendar-wrapper.component";
export default function ScheduleDayViewComponent({ data, day }) {
const { t } = useTranslation();
if (data)
//TODO Remove addtional calendar elements from day view.
return (
<ScheduleCalendarWrapperComponent
events={data}
defaultView="day"
views={["day"]}
style={{ height: "40vh" }}
defaultDate={new Date(day)}
//onNavigate={e => console.log("e", e)}
/>
);
else return <div>{t("appointments.labels.nodateselected")}</div>;
}

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