Merged in dev-patrick (pull request #6)

Dev patrick
This commit is contained in:
Snapt Software
2020-04-21 00:47:07 +00:00
916 changed files with 63041 additions and 9210 deletions

View File

@@ -0,0 +1,5 @@
commands:
10_cleanup:
command: |
sudo rm -f /opt/elasticbeanstalk/hooks/configdeploy/post/*
sudo rm -f /etc/nginx/conf.d/*

View File

@@ -0,0 +1,13 @@
Resources:
sslSecurityGroupIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: {"Fn::GetAtt" : ["AWSEBSecurityGroup", "GroupId"]}
IpProtocol: tcp
ToPort: 443
FromPort: 443
CidrIp: 0.0.0.0/0
packages:
yum:
epel-release: []

View File

@@ -0,0 +1,105 @@
files:
"/etc/nginx/nginx.pre":
mode: "000644"
owner: root
group: root
content: |
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
port_in_redirect off;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
log_format healthd '$msec"$uri"$status"$request_time"$upstream_response_time"$http_x_forwarded_for';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
include /etc/nginx/mime.types;
include /etc/nginx/conf.d/*.conf;
}
"/etc/nginx/conf.d/http_custom.conf":
mode: "000644"
owner: root
group: root
content: |
server {
listen 8080;
location ~ /.well-known/ {
root /var/www/letsencrypt/;
}
location / {
return 301 https://$host$request_uri;
}
}
"/etc/nginx/conf.d/https_custom.pre":
mode: "000644"
owner: root
group: root
content: |
upstream nodejs {
server 127.0.0.1:5000;
keepalive 256;
}
server {
listen 443 ssl default;
server_name localhost;
error_page 497 https://$host$request_uri;
if ($time_iso8601 ~ "^(\d{4})-(\d{2})-(\d{2})T(\d{2})") {
set $year $1;
set $month $2;
set $day $3;
set $hour $4;
}
access_log /var/log/nginx/healthd/application.log.$year-$month-$day-$hour healthd;
access_log /var/log/nginx/access.log main;
location / {
proxy_pass http://nodejs;
proxy_set_header Connection "";
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
gzip on;
gzip_comp_level 4;
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
ssl_certificate /etc/letsencrypt/live/ebcert/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ebcert/privkey.pem;
ssl_session_timeout 5m;
ssl_protocols TLSv1.1 TLSv1.2;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_prefer_server_ciphers on;
if ($host ~* ^www\.(.*)) {
set $host_without_www $1;
rewrite ^(.*) https://$host_without_www$1 permanent;
}
if ($ssl_protocol = "") {
rewrite ^ https://$host$request_uri? permanent;
}
}

View File

@@ -0,0 +1,45 @@
container_commands:
10_setup_nginx:
command: |
sudo rm -f /tmp/deployment/config/#etc#nginx#conf.d#00_elastic_beanstalk_proxy.conf
sudo rm -f /etc/nginx/conf.d/00_elastic_beanstalk_proxy.conf
sudo rm -f /tmp/deployment/config/#etc#nginx#nginx.conf
sudo rm -f /etc/nginx/nginx.conf
sudo mv /etc/nginx/nginx.pre /etc/nginx/nginx.conf
sudo service nginx stop
sudo service nginx start
20_install_certbot:
command: |
wget https://dl.eff.org/certbot-auto
mv certbot-auto /usr/local/bin/certbot-auto
chown root /usr/local/bin/certbot-auto
chmod 0755 /usr/local/bin/certbot-auto
30_create_webroot_path:
command: |
sudo rm -rf /var/www/letsencrypt/
sudo mkdir /var/www/letsencrypt/
40_configure_cert:
command: |
certbot_command="/usr/local/bin/certbot-auto certonly --webroot --webroot-path /var/www/letsencrypt --debug --non-interactive --email ${LETSENCRYPT_EMAIL} --agree-tos --expand --keep-until-expiring"
for domain in $(echo ${LETSENCRYPT_DOMAIN} | sed "s/,/ /g")
do
certbot_command="$certbot_command --domains $domain"
done
eval $certbot_command
50_link_cert:
command: |
domain="$( cut -d ',' -f 1 <<< "${LETSENCRYPT_DOMAIN}" )";
if [ -d /etc/letsencrypt/live ]; then
domain_folder_name="$(ls /etc/letsencrypt/live | sort -n | grep $domain | head -1)";
if [ -d /etc/letsencrypt/live/${domain_folder_name} ]; then
ln -sfn /etc/letsencrypt/live/${domain_folder_name} /etc/letsencrypt/live/ebcert
fi
fi
60_enable_https_config:
command: |
sudo mv /etc/nginx/conf.d/https_custom.pre /etc/nginx/conf.d/https_custom.conf
sudo service nginx stop
sudo service nginx start

View File

@@ -0,0 +1,11 @@
files:
# Elastic Beanstalk recreates the default configuration during every configuration deployment
"/opt/elasticbeanstalk/hooks/configdeploy/post/99_kill_default_nginx.sh":
mode: "000755"
owner: root
group: root
content: |
#!/bin/bash -xe
rm -f /etc/nginx/conf.d/00_elastic_beanstalk_proxy.conf
service nginx stop
service nginx start

View File

@@ -0,0 +1,8 @@
files:
# Cron to renew cert
"/etc/cron.d/certbot_renew":
mode: "000644"
owner: root
group: root
content: |
@weekly root /usr/local/bin/certbot-auto renew

View File

@@ -0,0 +1,9 @@
branch-defaults:
master:
environment: Bodyshop-prod
global:
application_name: bodyshop
default_ec2_keyname: e-yqpq3yupbk
default_platform: Node.js running on 64bit Amazon Linux/4.14.1
default_region: ca-central-1
sc: git

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,92 @@
CREATE SCHEMA audit;
CREATE TABLE audit_trail (
id serial PRIMARY KEY,
tstamp timestamp DEFAULT now(),
schemaname text,
tabname text,
operation text,
recordid uuid,
-- who text DEFAULT current_user,
new_val json,
old_val json,
useremail text,
bodyshopid uuid
);
-- More as an example than anything else, I wanted a function that would take two JSONB objects in PostgreSQL, and return how the left-hand side differs from the right-hand side. This means any key that is in the left but not in the right would be returned, along with any key whose value on the left is different from the right.
-- Heres a quick example of how to do this in a single SELECT. In real life, you probably want more error checking, but it shows how nice the built-in primitives are:
CREATE OR REPLACE FUNCTION json_diff(l JSONB, r JSONB) RETURNS JSONB AS
$json_diff$
SELECT jsonb_object_agg(a.key, a.value) FROM
( SELECT key, value FROM jsonb_each(l) ) a LEFT OUTER JOIN
( SELECT key, value FROM jsonb_each(r) ) b ON a.key = b.key
WHERE a.value != b.value OR b.key IS NULL;
$json_diff$
LANGUAGE sql;
CREATE OR REPLACE FUNCTION audit_trigger() RETURNS trigger AS $$
DECLARE
shopid text ;
email text;
BEGIN
select b.id, u.email INTO shopid, email from users u join associations a on u.email = a.useremail join bodyshops b on b.id = a.shopid where u.authid = current_setting('hasura.user', 't')::jsonb->>'x-hasura-user-id' and a.active = true;
IF TG_OP = 'INSERT'
THEN
INSERT INTO public.audit_trail (tabname, schemaname, operation, new_val, recordid, bodyshopid, useremail)
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(NEW), NEW.id, shopid, email);
RETURN NEW;
ELSIF TG_OP = 'UPDATE'
THEN
INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, new_val, recordid, bodyshopid, useremail)
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP,
json_diff(to_jsonb(OLD), to_jsonb(NEW)) , json_diff(to_jsonb(NEW), to_jsonb(OLD)), OLD.id, shopid, email);
RETURN NEW;
ELSIF TG_OP = 'DELETE'
THEN
INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, recordid, bodyshopid, useremail)
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(OLD), OLD.ID, shopid, email);
RETURN OLD;
END IF;
END;
$$ LANGUAGE 'plpgsql' SECURITY DEFINER;
CREATE TRIGGER audit_trigger_users AFTER INSERT OR UPDATE OR DELETE ON users
FOR EACH ROW EXECUTE PROCEDURE audit_trigger();

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

View File

@@ -1,11 +0,0 @@
module.exports = {
client: {
service: {
name: "Dev",
url: "https://bodyshop-dev-db.herokuapp.com/v1/graphql",
headers: {
"x-hasura-admin-secret": "Dev-BodyShopAppBySnaptSoftware!"
}
}
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -4,35 +4,49 @@
"private": true,
"proxy": "https://localhost:5000",
"dependencies": {
"antd": "^3.26.0",
"@nivo/pie": "^0.61.1",
"@tinymce/tinymce-react": "^3.5.0",
"aamva": "^1.2.0",
"antd": "^4.1.0",
"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-retry": "^2.2.15",
"apollo-link-ws": "^1.0.19",
"axios": "^0.19.1",
"chart.js": "^2.9.3",
"axios": "^0.19.2",
"dotenv": "^8.2.0",
"firebase": "^7.5.0",
"graphql": "^14.5.8",
"i18next": "^19.0.2",
"node-sass": "^4.13.0",
"react": "^16.12.0",
"firebase": "^7.13.1",
"graphql": "^14.6.0",
"i18next": "^19.3.4",
"i18next-browser-languagedetector": "^4.1.1",
"node-sass": "^4.13.1",
"query-string": "^6.11.1",
"react": "^16.13.1",
"react-apollo": "^3.1.3",
"react-chartjs-2": "^2.8.0",
"react-dom": "^16.12.0",
"react-i18next": "^11.2.7",
"react-icons": "^3.8.0",
"react-barcode": "^1.4.0",
"react-big-calendar": "^0.24.1",
"react-dom": "^16.13.1",
"react-ga": "^2.7.0",
"react-grid-gallery": "^0.5.5",
"react-grid-layout": "^0.18.3",
"react-i18next": "^11.3.4",
"react-icons": "^3.9.0",
"react-image-file-resizer": "^0.2.1",
"react-moment": "^0.9.7",
"react-number-format": "^4.3.1",
"react-number-format": "^4.4.1",
"react-redux": "^7.2.0",
"react-router-dom": "^5.1.2",
"react-scripts": "3.2.0",
"react-trello": "^2.2.3",
"styled-components": "^4.4.1",
"react-scripts": "3.4.1",
"redux": "^4.0.5",
"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"
},
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
@@ -56,6 +70,8 @@
"devDependencies": {
"@apollo/react-testing": "^3.1.3",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2"
"enzyme-adapter-react-16": "^1.15.2",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.4.2"
}
}

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,30 +1,22 @@
import React, { Component } from "react";
import App from "./App";
import Spin from "../components/loading-spinner/loading-spinner.component";
import { ApolloProvider } from "@apollo/react-common";
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 { RetryLink } from "apollo-link-retry";
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 { ApolloProvider } from "react-apollo";
import { persistCache } from "apollo-cache-persist";
import initialState from "../graphql/initial-state";
//import { shouldRefreshToken, refreshToken } from "../graphql/middleware";
import React, { Component } from "react";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
import { auth } from "../firebase/firebase.utils";
import errorLink from "../graphql/apollo-error-handling";
class AppContainer extends Component {
state = {
client: null,
loaded: false
};
async componentDidMount() {
import App from "./App";
export default class AppContainer extends Component {
constructor() {
super();
const httpLink = new HttpLink({
uri: process.env.REACT_APP_GRAPHQL_ENDPOINT
});
@@ -34,8 +26,9 @@ class AppContainer extends Component {
options: {
lazy: true,
reconnect: true,
connectionParams: () => {
const token = localStorage.getItem("token");
connectionParams: async () => {
//const token = localStorage.getItem("token");
const token = await auth.currentUser.getIdToken(true);
if (token) {
return {
headers: {
@@ -46,6 +39,13 @@ class AppContainer extends Component {
}
}
});
const subscriptionMiddleware = {
applyMiddleware: async (options, next) => {
options.authToken = await auth.currentUser.getIdToken(true);
next();
}
};
wsLink.subscriptionClient.use([subscriptionMiddleware]);
const link = split(
// split based on operation type
@@ -69,22 +69,29 @@ 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 auth.currentUser.getIdToken().then(token => {
if (token) {
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : ""
}
};
} else {
return { headers };
}
});
});
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : ""
}
};
} else {
return { headers };
const retryLink = new RetryLink({
delay: {
initial: 300,
max: 5,
jitter: true
},
attempts: {
max: 5,
retryIf: (error, _operation) => !!error
}
});
@@ -92,55 +99,27 @@ class AppContainer extends Component {
if (process.env.NODE_ENV === "development") {
middlewares.push(apolloLogger);
}
middlewares.push(errorLink.concat(authLink.concat(link)));
middlewares.push(retryLink.concat(errorLink.concat(authLink.concat(link))));
const cache = new InMemoryCache();
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.
this.state = { client };
}
componentWillUnmount() {}
render() {
const { client, loaded } = this.state;
if (!loaded) {
return <Spin />;
}
const { client } = this.state;
return (
<ApolloProvider client={client}>
<GlobalLoadingBar />
<App />
</ApolloProvider>
);
}
}
export default AppContainer;

View File

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

View File

@@ -1,133 +1,64 @@
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");
checkUserSession();
return () => {};
}, [checkUserSession]);
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) => {
const { t } = useTranslation();
if (currentUser && currentUser.language)
i18next.changeLanguage(currentUser.language, err => {
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>
<ErrorBoundary>
<Suspense fallback={<LoadingSpinner />}>
<Suspense fallback={<LoadingSpinner message='App.Js Suspense' />}>
<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={currentUser.authorized}
path='/manage'
component={ManagePage}
/>
@@ -136,4 +67,4 @@ export default () => {
</Switch>
</div>
);
};
});

View File

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

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,150 @@
import { Editor } from "@tinymce/tinymce-react";
import axios from "axios";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { EmailSettings } from "../../emails/constants";
import {
endLoading,
startLoading,
} from "../../redux/application/application.actions";
import { setEmailOptions } from "../../redux/email/email.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
load: () => dispatch(startLoading()),
endload: () => dispatch(endLoading()),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(function Test({ setEmailOptions, load, endload, bodyshop }) {
const [state, setState] = useState(temp);
const handleEditorChange = (content, editor) => {
setState(content);
};
return (
<div>
<button
onClick={() => {
axios
.post("/render", {
view: state,
context: {
people: ["Yehuda Katz", "Alan Johnson", "Charles Jolley"],
},
})
.then((r) => {
var newWin = window.open(
"url",
"windowName",
"height=300,width=300"
);
newWin.document.write(r.data);
});
}}
>
TinyMCE
</button>
<Editor
value={state}
apiKey="f3s2mjsd77ya5qvqkee9vgh612cm6h41e85efqakn2d0kknk"
init={{
height: 500,
//menubar: false,
encoding: "raw",
extended_valid_elements: "span",
plugins: [
"advlist autolink lists link image charmap print preview anchor",
"searchreplace visualblocks code fullscreen",
"insertdatetime media table paste code help wordcount",
],
toolbar:
"undo redo | formatselect | bold italic backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | removeformat | help",
}}
onEditorChange={handleEditorChange}
/>
<button
onClick={() =>
setEmailOptions({
messageOptions: {
from: {
name: bodyshop.shopname || EmailSettings.fromNameDefault,
address: EmailSettings.fromAddress,
},
to: "patrickwf@gmail.com",
replyTo: bodyshop.email,
Subject: "TODO FIX ME",
},
template: {
name: "appointment_reminder",
variables: { id: "2b42336f-b8de-4f04-a053-d6bff034d384" },
},
})
}
>
Set email config.
</button>
<button
onClick={() =>
setEmailOptions({
messageOptions: {
from: {
name: bodyshop.shopname || EmailSettings.fromNameDefault,
address: EmailSettings.fromAddress,
},
to: "patrickwf@gmail.com",
replyTo: bodyshop.email,
Subject: "TODO FIX ME",
},
template: {
name: "parts_order_confirmation",
variables: { id: "6fea31e9-ea85-4c89-ac56-6f9cc84531fe" },
},
})
}
>
Parts Order
</button>
</div>
);
});
const temp = `<div style="font-family: Arial, Helvetica, sans-serif;">
<p style="text-align: center;"><span>&rarr;<span> This is a full-featured editor demo. </span>Please explore! &larr;</span></p>
<p style="text-align: center;">&nbsp;</p>
<h2 style="text-align: center;"><span>TinyMCE is the world's most customizable, and flexible, rich text editor.</span></h2>
<p style="text-align: center;"><span><strong> A featherweight download, TinyMCE can handle any challenge you throw at it. </strong></span></p>
<p style="text-align: center;">&nbsp;</p>
<p>&nbsp;</p>
<table style="border-collapse: collapse; width: 85%; height: 86px; border-color: initial; border-style: solid; margin-left: auto; margin-right: auto;">
<tbody>
<tr style="height: 22px;">
<td style="width: 25%; text-align: center; padding: 7px; height: 22px;"><span>🛠 50+ Plugins</span></td>
<td style="width: 25%; text-align: center; padding: 7px; height: 22px;"><span>💡 Premium Support</span></td>
<td style="width: 25%; text-align: center; padding: 7px; height: 22px;"><span>🖍 Custom Skins</span></td>
<td style="width: 25%; text-align: center; padding: 7px; height: 22px;"><span>⚙ Full API Access</span></td>
</tr>
<tr style="height: 21px; display: none;">
<td style="height: 21px; display: none;"><span>{{#each people}}</span></td>
</tr>
<tr style="height: 22px;">
<td style="width: 25%; text-align: center; padding: 7px; height: 22px; border-style: solid; border-width: 1px;"><span>{{this}}</span></td>
<td style="width: 25%; text-align: center; padding: 7px; height: 22px; border-style: solid; border-width: 1px;"><span>{{this}}</span></td>
<td style="width: 25%; text-align: center; padding: 7px; height: 22px; border-style: solid; border-width: 1px;"><span>{{this}}</span></td>
<td style="width: 25%; text-align: center; padding: 7px; height: 22px; border-style: solid; border-width: 1px;"><span>{{this}}</span></td>
</tr>
<tr style="height: 21px; display: none;">
<td style="height: 21px;"><span>{{/each}}</span></td>
</tr>
</tbody>
</table>
</div>`;

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Alert component should render Alert component 1`] = `ShallowWrapper {}`;

View File

@@ -2,10 +2,22 @@ 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" />);
describe("Alert component", () => {
let wrapper;
beforeEach(() => {
const mockProps = {
type: "error",
message: "Test error message."
};
wrapper = shallow(<Alert {...mockProps} />);
});
it("should render Alert component", () => {
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AllocationsAssignmentComponent component should create an allocation on save 1`] = `ReactWrapper {}`;
exports[`AllocationsAssignmentComponent component should render AllocationsAssignmentComponent component 1`] = `ReactWrapper {}`;

View File

@@ -0,0 +1,72 @@
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 function AllocationsAssignmentComponent({
bodyshop,
handleAssignment,
assignment,
setAssignment,
visibilityState,
maxHours
}) {
const { t } = useTranslation();
const onChange = e => {
setAssignment({ ...assignment, employeeid: e });
};
const [visibility, setVisibility] = visibilityState;
const popContent = (
<div>
<Select id="employeeSelector"
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>
);
}
export default connect(mapStateToProps, null)(AllocationsAssignmentComponent);

View File

@@ -0,0 +1,38 @@
import { mount, shallow } from "enzyme";
import React from "react";
import { AllocationsAssignmentComponent } from "./allocations-assignment.component";
import { MockBodyshop } from "../../utils/TestingHelpers";
import { Select } from "antd";
const div = document.createElement("div");
describe("AllocationsAssignmentComponent component", () => {
let wrapper;
beforeEach(() => {
const mockProps = {
bodyshop: MockBodyshop,
handleAssignment: jest.fn(),
assignment: {},
setAssignment: jest.fn(),
visibilityState: [false, jest.fn()],
maxHours: 4
};
wrapper = mount(<AllocationsAssignmentComponent {...mockProps} />);
});
it("should render AllocationsAssignmentComponent component", () => {
expect(wrapper).toMatchSnapshot();
});
it("should render a list of employees", () => {
const empList = wrapper.find("#employeeSelector");
console.log(empList.debug());
expect(empList.children()).to.have.lengthOf(2);
});
it("should create an allocation on save", () => {
wrapper.find("Button").simulate("click");
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,47 @@
import React, { useState } from "react";
import AllocationsAssignmentComponent from "./allocations-assignment.component";
import { useMutation } from "@apollo/react-hooks";
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,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 "@apollo/react-hooks";
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 "@ant-design/icons";
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 "@apollo/react-hooks";
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,85 @@
import React, { useState } from "react";
import { Table } from "antd";
import { alphaSort } from "../../utils/sorters";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { useTranslation } from "react-i18next";
import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component";
export default function AuditTrailListComponent({ loading, data }) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: {}
});
const { t } = useTranslation();
const columns = [
{
title: t("audit.fields.created"),
dataIndex: " created",
key: " created",
width: "10%",
render: (text, record) => (
<DateTimeFormatter>{record.created}</DateTimeFormatter>
),
sorter: (a, b) => a.created - b.created,
sortOrder:
state.sortedInfo.columnKey === "created" && state.sortedInfo.order
},
{
title: t("audit.fields.operation"),
dataIndex: "operation",
key: "operation",
width: "10%",
sorter: (a, b) => alphaSort(a.operation, b.operation),
sortOrder:
state.sortedInfo.columnKey === "operation" && state.sortedInfo.order
},
{
title: t("audit.fields.values"),
dataIndex: " old_val",
key: " old_val",
width: "10%",
render: (text, record) => (
<AuditTrailValuesComponent
oldV={record.old_val}
newV={record.new_val}
/>
)
},
{
title: t("audit.fields.useremail"),
dataIndex: "useremail",
key: "useremail",
width: "10%",
sorter: (a, b) => alphaSort(a.useremail, b.useremail),
sortOrder:
state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order
}
];
const formItemLayout = {
labelCol: {
xs: { span: 12 },
sm: { span: 5 }
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 }
}
};
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Table
{...formItemLayout}
loading={loading}
size="small"
pagination={{ position: "top", defaultPageSize: 25 }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={data}
onChange={handleTableChange}
/>
);
}

View File

@@ -0,0 +1,24 @@
import React from "react";
import AuditTrailListComponent from "./audit-trail-list.component";
import { useQuery } from "@apollo/react-hooks";
import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
import AlertComponent from "../alert/alert.component";
export default function AuditTrailListContainer({ recordId }) {
const { loading, error, data } = useQuery(QUERY_AUDIT_TRAIL, {
variables: { id: recordId },
fetchPolicy: "network-only"
});
return (
<div>
{error ? (
<AlertComponent type="error" message={error.message} />
) : (
<AuditTrailListComponent
loading={loading}
data={data ? data.audit_trail : null}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,29 @@
import React from "react";
import { List } from "antd";
import Icon from "@ant-design/icons";
import { FaArrowRight } from "react-icons/fa";
export default function AuditTrailValuesComponent({ oldV, newV }) {
if (!oldV && !newV) return <div></div>;
if (!oldV && newV)
return (
<List style={{ width: "800px" }} bordered size='small'>
{Object.keys(newV).map((key, idx) => (
<List.Item key={idx} value={key}>
{key}: {JSON.stringify(newV[key])}
</List.Item>
))}
</List>
);
return (
<List style={{ width: "800px" }} bordered size='small'>
{Object.keys(oldV).map((key, idx) => (
<List.Item key={idx}>
{key}: {oldV[key]} <Icon component={FaArrowRight} />
{JSON.stringify(newV[key])}
</List.Item>
))}
</List>
);
}

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,28 @@
import React from "react";
import { Breadcrumb } from "antd";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBreadcrumbs } from "../../redux/application/application.selectors";
import { Link } from "react-router-dom";
const mapStateToProps = createStructuredSelector({
breadcrumbs: selectBreadcrumbs,
});
export function BreadCrumbs({ breadcrumbs }) {
return (
<Breadcrumb>
<Breadcrumb.Item>Home</Breadcrumb.Item>
{breadcrumbs.map((item) =>
item.link ? (
<Breadcrumb.Item key={item.label}>
<Link to={item.link}>{item.label} </Link>
</Breadcrumb.Item>
) : (
<Breadcrumb.Item key={item.label}>{item.label}</Breadcrumb.Item>
)
)}
</Breadcrumb>
);
}
export default connect(mapStateToProps, null)(BreadCrumbs);

View File

@@ -0,0 +1,41 @@
import { ShrinkOutlined } from "@ant-design/icons";
import { Badge } from "antd";
import React from "react";
import { connect } from "react-redux";
import {
openConversation,
toggleChatVisible
} from "../../redux/messaging/messaging.actions";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
const mapDispatchToProps = dispatch => ({
toggleChatVisible: () => dispatch(toggleChatVisible()),
openConversation: number => dispatch(openConversation(number))
});
export function ChatConversationListComponent({
toggleChatVisible,
conversationList,
openConversation
}) {
return (
<div className='chat-overlay-open'>
<ShrinkOutlined onClick={() => toggleChatVisible()} />
{conversationList.map(item => (
<Badge count={item.messages_aggregate.aggregate.count || 0}>
<div
key={item.id}
style={{ cursor: "pointer", display: "block" }}
onClick={() =>
openConversation({ phone_num: item.phone_num, id: item.id })
}>
<div>
<PhoneNumberFormatter>{item.phone_num}</PhoneNumberFormatter>
</div>
</div>
</Badge>
))}
</div>
);
}
export default connect(null, mapDispatchToProps)(ChatConversationListComponent);

View File

@@ -0,0 +1,38 @@
import { CloseCircleFilled } from "@ant-design/icons";
import React from "react";
import { connect } from "react-redux";
import {
closeConversation,
sendMessage,
toggleConversationVisible
} from "../../redux/messaging/messaging.actions";
import PhoneFormatter from "../../utils/PhoneFormatter";
const mapDispatchToProps = dispatch => ({
toggleConversationVisible: conversationId =>
dispatch(toggleConversationVisible(conversationId)),
closeConversation: phone => dispatch(closeConversation(phone)),
sendMessage: message => dispatch(sendMessage(message))
});
function ChatConversationClosedComponent({
conversation,
toggleConversationVisible,
closeConversation
}) {
return (
<div
className='chat-conversation-closed'
onClick={() => toggleConversationVisible(conversation.id)}>
<PhoneFormatter>{conversation.phone_num}</PhoneFormatter>
<CloseCircleFilled
onClick={() => closeConversation(conversation.phone_num)}
/>
</div>
);
}
export default connect(
null,
mapDispatchToProps
)(ChatConversationClosedComponent);

View File

@@ -0,0 +1,29 @@
import { Badge, Card } from "antd";
import React from "react";
import ChatConversationClosedComponent from "./chat-conversation.closed.component";
import ChatConversationOpenComponent from "./chat-conversation.open.component";
export default function ChatConversationComponent({
conversation,
messages,
subState,
unreadCount
}) {
return (
<div className='chat-conversation'>
<Badge count={unreadCount}>
<Card size='small'>
{conversation.open ? (
<ChatConversationOpenComponent
messages={messages}
conversation={conversation}
subState={subState}
/>
) : (
<ChatConversationClosedComponent conversation={conversation} />
)}
</Card>
</Badge>
</div>
);
}

View File

@@ -0,0 +1,34 @@
import { useSubscription } from "@apollo/react-hooks";
import React from "react";
import { CONVERSATION_SUBSCRIPTION_BY_PK } from "../../graphql/conversations.queries";
import ChatConversationComponent from "./chat-conversation.component";
export default function ChatConversationContainer({ conversation }) {
const { loading, error, data } = useSubscription(
CONVERSATION_SUBSCRIPTION_BY_PK,
{
variables: { conversationId: conversation.id }
}
);
return (
<ChatConversationComponent
subState={[loading, error]}
conversation={conversation}
unreadCount={
(data &&
data.conversations_by_pk &&
data.conversations_by_pk.messages_aggregate &&
data.conversations_by_pk.messages_aggregate.aggregate &&
data.conversations_by_pk.messages_aggregate.aggregate.count) ||
0
}
messages={
(data &&
data.conversations_by_pk &&
data.conversations_by_pk.messages) ||
[]
}
/>
);
}

View File

@@ -0,0 +1,36 @@
import React from "react";
import { connect } from "react-redux";
import { toggleConversationVisible } from "../../redux/messaging/messaging.actions";
import AlertComponent from "../alert/alert.component";
import ChatMessageListComponent from "../chat-messages-list/chat-message-list.component";
import ChatSendMessage from "../chat-send-message/chat-send-message.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import { ShrinkOutlined } from "@ant-design/icons";
const mapDispatchToProps = dispatch => ({
toggleConversationVisible: conversation =>
dispatch(toggleConversationVisible(conversation))
});
export function ChatConversationOpenComponent({
conversation,
messages,
subState,
toggleConversationVisible
}) {
const [loading, error] = subState;
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type='error' />;
return (
<div className='chat-conversation-open'>
<ShrinkOutlined
onClick={() => toggleConversationVisible(conversation.id)}
/>
<ChatMessageListComponent messages={messages} />
<ChatSendMessage conversation={conversation} />
</div>
);
}
export default connect(null, mapDispatchToProps)(ChatConversationOpenComponent);

View File

@@ -0,0 +1,32 @@
import { Affix } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectConversations } from "../../redux/messaging/messaging.selectors";
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
import ChatMessagesButtonContainer from "../chat-messages-button/chat-messages-button.container";
import "./chat-dock.styles.scss";
const mapStateToProps = createStructuredSelector({
activeConversations: selectConversations
});
export function ChatOverlayContainer({ activeConversations }) {
return (
<Affix offsetBottom={0}>
<div className='chat-dock'>
<ChatMessagesButtonContainer />
{activeConversations
? activeConversations.map(conversation => (
<ChatConversationContainer
conversation={conversation}
key={conversation.id}
/>
))
: null}
</div>
</Affix>
);
}
export default connect(mapStateToProps, null)(ChatOverlayContainer);

View File

@@ -0,0 +1,188 @@
.chat-dock {
z-index: 5;
//overflow-x: scroll;
// overflow-y: hidden;
width: 100%;
display: flex;
align-items: baseline;
}
.chat-conversation {
margin: 2em 1em 0em 1em;
}
.chat-conversation-open {
height: 500px;
}
// .chat-messages {
// height: 80%;
// overflow-x: hidden;
// overflow-y: scroll;
// flex-grow: 1;
// ul {
// list-style: none;
// margin: 0;
// padding: 0;
// }
// ul li {
// display: inline-block;
// clear: both;
// padding: 3px 10px;
// border-radius: 30px;
// margin-bottom: 2px;
// }
// .inbound {
// background: #eee;
// float: left;
// }
// .outbound {
// float: right;
// background: #0084ff;
// color: #fff;
// }
// .inbound + .outbound {
// border-bottom-right-radius: 5px;
// }
// .outbound + .outbound {
// border-top-right-radius: 5px;
// border-bottom-right-radius: 5px;
// }
// .outbound:last-of-type {
// border-bottom-right-radius: 30px;
// }
// }
.messages {
height: auto;
min-height: calc(100% - 10px);
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;
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,47 @@
import { MessageFilled } from "@ant-design/icons";
import { Badge, Card } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
import ChatConversationListComponent from "../chat-conversation-list/chat-conversation-list.component";
const mapStateToProps = createStructuredSelector({
chatVisible: selectChatVisible
});
const mapDispatchToProps = dispatch => ({
toggleChatVisible: () => dispatch(toggleChatVisible())
});
export function ChatWindowComponent({
chatVisible,
toggleChatVisible,
conversationList,
unreadCount
}) {
const { t } = useTranslation();
return (
<div className='chat-conversation'>
<Badge count={unreadCount}>
<Card size='small'>
{chatVisible ? (
<ChatConversationListComponent
conversationList={conversationList}
/>
) : (
<div onClick={() => toggleChatVisible()}>
<MessageFilled />
<strong>{t("messaging.labels.messaging")}</strong>
</div>
)}
</Card>
</Badge>
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ChatWindowComponent);

View File

@@ -0,0 +1,27 @@
import { useSubscription } from "@apollo/react-hooks";
import React from "react";
import { CONVERSATION_LIST_SUBSCRIPTION } from "../../graphql/conversations.queries";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import ChatMessagesButtonComponent from "./chat-messages-button.component";
export default function ChatMessagesButtonContainer() {
const { loading, error, data } = useSubscription(
CONVERSATION_LIST_SUBSCRIPTION
);
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type='error' />;
return (
<ChatMessagesButtonComponent
conversationList={(data && data.conversations) || []}
unreadCount={
(data &&
data.conversations.reduce((acc, val) => {
return (acc = acc + val.messages_aggregate.aggregate.count);
}, 0)) ||
0
}
/>
);
}

View File

@@ -0,0 +1,44 @@
import { CheckCircleOutlined, CheckOutlined } from "@ant-design/icons";
import React, { useEffect, useRef } from "react";
export default function ChatMessageListComponent({ messages }) {
const messagesEndRef = useRef(null);
const scrollToBottom = () => {
console.log("use");
!!messagesEndRef.current &&
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
};
useEffect(scrollToBottom, [messages]);
const StatusRender = status => {
switch (status) {
case "sent":
return <CheckOutlined style={{ margin: "2px", float: "right" }} />;
case "delivered":
return (
<CheckCircleOutlined style={{ margin: "2px", float: "right" }} />
);
default:
return null;
}
};
return (
<div className='messages'>
<ul>
{messages.map(item => (
<li
key={item.id}
className={`${item.isoutbound ? "replies" : "sent"}`}>
<p>
{item.text}
{StatusRender(item.status)}
</p>
</li>
))}
<li ref={messagesEndRef} />
</ul>
</div>
);
}

View File

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

View File

@@ -0,0 +1,69 @@
import { Input, Spin } from "antd";
import { LoadingOutlined } from "@ant-design/icons";
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { sendMessage } from "../../redux/messaging/messaging.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = dispatch => ({
sendMessage: message => dispatch(sendMessage(message))
});
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage }) {
const [message, setMessage] = useState("");
useEffect(() => {
if (conversation.isSending === false) {
setMessage("");
}
}, [conversation, setMessage]);
const { t } = useTranslation();
const handleEnter = () => {
sendMessage({
to: conversation.phone_num,
body: message,
messagingServiceSid: bodyshop.messagingservicesid,
conversationid: conversation.id
});
};
return (
<div style={{ display: "flex " }}>
<Input.TextArea
allowClear
autoFocus
suffix={<span>a</span>}
autoSize={{ minRows: 1, maxRows: 4 }}
value={message}
disabled={conversation.isSending}
placeholder={t("messaging.labels.typeamessage")}
onChange={e => setMessage(e.target.value)}
onPressEnter={event => {
event.preventDefault();
if (!!!event.shiftKey) handleEnter();
}}
/>
<Spin
style={{ display: `${conversation.isSending ? "" : "none"}` }}
indicator={
<LoadingOutlined
style={{
fontSize: 24
}}
spin
/>
}
/>
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ChatSendMessageComponent);

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

@@ -0,0 +1,119 @@
import { Input, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
export default function ContractsCarsComponent({
loading,
data,
selectedCar,
handleSelect
}) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
search: ""
});
const { t } = useTranslation();
const columns = [
{
title: t("courtesycars.fields.fleetnumber"),
dataIndex: "fleetnumber",
key: "fleetnumber",
sorter: (a, b) => alphaSort(a.fleetnumber, b.fleetnumber),
sortOrder:
state.sortedInfo.columnKey === "fleetnumber" && state.sortedInfo.order
},
{
title: t("courtesycars.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order
},
{
title: t("courtesycars.fields.year"),
dataIndex: "year",
key: "year",
sorter: (a, b) => alphaSort(a.year, b.year),
sortOrder: state.sortedInfo.columnKey === "year" && state.sortedInfo.order
},
{
title: t("courtesycars.fields.make"),
dataIndex: "make",
key: "make",
sorter: (a, b) => alphaSort(a.make, b.make),
sortOrder: state.sortedInfo.columnKey === "make" && state.sortedInfo.order
},
{
title: t("courtesycars.fields.model"),
dataIndex: "model",
key: "model",
sorter: (a, b) => alphaSort(a.model, b.model),
sortOrder:
state.sortedInfo.columnKey === "model" && state.sortedInfo.order
},
{
title: t("courtesycars.fields.plate"),
dataIndex: "plate",
key: "plate",
sorter: (a, b) => alphaSort(a.plate, b.plate),
sortOrder:
state.sortedInfo.columnKey === "plate" && state.sortedInfo.order
}
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const filteredData =
state.search === ""
? data
: data.filter(
cc =>
(cc.fleetnumber || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(cc.status || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(cc.year || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(cc.make || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(cc.model || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(cc.plate || "").toLowerCase().includes(state.search.toLowerCase())
);
return (
<Table
loading={loading}
title={() => (
<Input.Search
placeholder={t("general.labels.search")}
value={state.search}
onChange={e => setState({ ...state, search: e.target.value })}
/>
)}
size="small"
pagination={{ position: "top" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={filteredData}
onChange={handleTableChange}
rowSelection={{
onSelect: handleSelect,
type: "radio",
selectedRowKeys: [selectedCar]
}}
/>
);
}

View File

@@ -0,0 +1,29 @@
import { useQuery } from "@apollo/react-hooks";
import React from "react";
import { QUERY_AVAILABLE_CC } from "../../graphql/courtesy-car.queries";
import AlertComponent from "../alert/alert.component";
import ContractCarsComponent from "./contract-cars.component";
export default function ContractCarsContainer({ selectedCarState, bodyshop }) {
const { loading, error, data } = useQuery(QUERY_AVAILABLE_CC);
const [selectedCar, setSelectedCar] = selectedCarState;
const handleSelect = record => {
setSelectedCar(record.id);
};
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<div>
<ContractCarsComponent
handleSelect={handleSelect}
selectedCar={selectedCar}
loading={loading}
data={data ? data.courtesycars : []}
/>
</div>
);
}

View File

@@ -0,0 +1,26 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Descriptions, Card } from "antd";
import { Link } from "react-router-dom";
export default function ContractCourtesyCarBlock({ courtesyCar }) {
const { t } = useTranslation();
return (
<Link to={`/manage/courtesycars/${courtesyCar && courtesyCar.id}`}>
<Card title={t("courtesycars.labels.courtesycar")}>
<Descriptions size="small" column={1}>
<Descriptions.Item label={t("courtesycars.fields.fleetnumber")}>
{(courtesyCar && courtesyCar.fleetnumber) || ""}
</Descriptions.Item>
<Descriptions.Item label={t("courtesycars.fields.plate")}>
{(courtesyCar && courtesyCar.plate) || ""}
</Descriptions.Item>
<Descriptions.Item label={t("courtesycars.labels.vehicle")}>
{`${(courtesyCar && courtesyCar.year) || ""} ${(courtesyCar &&
courtesyCar.make) ||
""} ${(courtesyCar && courtesyCar.model) || ""}`}
</Descriptions.Item>
</Descriptions>
</Card>
</Link>
);
}

View File

@@ -0,0 +1,264 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Form, Input, DatePicker, InputNumber, Button } from "antd";
import aamva from "aamva";
import InputPhone from "../form-items-formatted/phone-form-item.component";
import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
export default function ContractFormComponent() {
const [state, setState] = useState("");
const { t } = useTranslation();
return (
<div>
<div style={{ background: "#f00" }}>
TEST AREA
<Input value={state} onChange={e => setState(e.target.value)} />
<Button
onClick={() => {
console.log("state", state);
//let data = state;
var data =
"%FLDELRAY BEACH^DOE$JOHN$^4818 S FEDERAL BLVD^ ? ;6360100462172082009=2101198299090=? #! 33435 I 1600 ECCECC00000?";
data = data.replace(/\n/, "");
// replace spaces with regular space
data = data.replace(/\s/g, " ");
var track = data.match(/(.*?\?)(.*?\?)(.*?\?)/);
console.log("data", data);
console.log("track", track);
const a = aamva.stripe(data);
console.log(JSON.stringify(a));
}}
>
Decode
</Button>
</div>
<Form.Item
label={t("contracts.fields.status")}
name="status"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<ContractStatusSelector />
</Form.Item>
<Form.Item
label={t("contracts.fields.start")}
name="start"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<DatePicker />
</Form.Item>
<Form.Item
label={t("contracts.fields.scheduledreturn")}
name="scheduledreturn"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<DatePicker />
</Form.Item>
<Form.Item label={t("contracts.fields.actualreturn")} name="actualreturn">
<DatePicker />
</Form.Item>
<Form.Item
label={t("contracts.fields.kmstart")}
name="kmstart"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<InputNumber />
</Form.Item>
<Form.Item label={t("contracts.fields.kmend")} name="kmend">
<InputNumber />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_dlnumber")}
name="driver_dlnumber"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_dlexpiry")}
name="driver_dlexpiry"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<DatePicker />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_dlst")}
name="driver_dlst"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_fn")}
name="driver_fn"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_ln")}
name="driver_ln"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_addr1")}
name="driver_addr1"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item label={t("contracts.fields.driver_addr2")} name="driver_addr2">
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_city")}
name="driver_city"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_state")}
name="driver_state"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_zip")}
name="driver_zip"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_ph1")}
name="driver_ph1"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<InputPhone />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_dob")}
name="driver_dob"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<DatePicker />
</Form.Item>
<Form.Item
label={t("contracts.fields.cc_num")}
name="cc_num"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.cc_expiry")}
name="cc_expiry"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.cc_cardholder")}
name="cc_cardholder"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
</div>
);
}

View File

@@ -0,0 +1,27 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Descriptions, Card } from "antd";
import { Link } from "react-router-dom";
export default function ContractJobBlock({ job }) {
const { t } = useTranslation();
return (
<Link to={`/manage/jobs/${job && job.id}`}>
<Card title={t("jobs.labels.job")}>
<Descriptions size="small" column={1}>
<Descriptions.Item label={t("jobs.fields.ro_number")}>
{(job && job.ro_number) || ""}
</Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.vehicle")}>
{`${(job && job.v_model_yr) || ""} ${(job && job.v_make_desc) ||
""} ${(job && job.v_model_desc) || ""}`}
</Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.owner")}>
{`${(job && job.ownr_fn) || ""} ${(job && job.ownr_ln) ||
""} ${(job && job.ownr_co_nm) || ""}`}
</Descriptions.Item>
</Descriptions>
</Card>
</Link>
);
}

View File

@@ -0,0 +1,187 @@
import { Table, Input } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
export default function ContractsJobsComponent({
loading,
data,
selectedJob,
handleSelect
}) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
search: ""
});
const { t } = useTranslation();
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
width: "8%",
sorter: (a, b) =>
alphaSort(
a.ro_number ? a.ro_number : "EST-" + a.est_number,
b.ro_number ? b.ro_number : "EST-" + b.est_number
),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<span>
{record.ro_number ? record.ro_number : "EST-" + record.est_number}
</span>
)
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
width: "25%",
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.owner ? (
<span>
{record.ownr_fn} {record.ownr_ln}
</span>
) : (
<span>{`${record.ownr_fn} ${record.ownr_ln}`}</span>
);
}
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
width: "10%",
ellipsis: true,
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => {
return record.status || t("general.labels.na");
}
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
width: "15%",
ellipsis: true,
render: (text, record) => {
return record.vehicleid ? (
<span>
{`${record.v_model_yr || ""} ${record.v_make_desc ||
""} ${record.v_model_desc || ""}`}
</span>
) : (
t("jobs.errors.novehicle")
);
}
},
{
title: t("vehicles.fields.plate_no"),
dataIndex: "plate_no",
key: "plate_no",
width: "8%",
ellipsis: true,
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
sortOrder:
state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
render: (text, record) => {
return record.plate_no ? (
<span>{record.plate_no}</span>
) : (
t("general.labels.unknown")
);
}
},
{
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
width: "12%",
ellipsis: true,
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
render: (text, record) => {
return record.clm_no ? (
<span>{record.clm_no}</span>
) : (
t("general.labels.unknown")
);
}
}
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const filteredData =
state.search === ""
? data
: data.filter(
j =>
(j.est_number || "")
.toString()
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(j.ro_number || "")
.toString()
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(j.ownr_fn || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(j.ownr_ln || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(j.clm_no || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(j.v_make_desc || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(j.v_model_desc || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(j.plate_no || "")
.toLowerCase()
.includes(state.search.toLowerCase())
);
return (
<Table
loading={loading}
title={() => (
<Input.Search
placeholder={t("general.labels.search")}
value={state.search}
onChange={e => setState({ ...state, search: e.target.value })}
/>
)}
size="small"
pagination={{ position: "top" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={filteredData}
onChange={handleTableChange}
rowSelection={{
onSelect: handleSelect,
type: "radio",
selectedRowKeys: [selectedJob]
}}
/>
);
}

View File

@@ -0,0 +1,38 @@
import { useQuery } from "@apollo/react-hooks";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import ContractJobsComponent from "./contract-jobs.component";
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop
});
export function ContractJobsContainer({ selectedJobState, bodyshop }) {
const { loading, error, data } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
variables: {
statuses: bodyshop.md_ro_statuses.open_statuses || ["Open"]
}
});
const [selectedJob, setSelectedJob] = selectedJobState;
const handleSelect = record => {
setSelectedJob(record.id);
};
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<div>
<ContractJobsComponent
handleSelect={handleSelect}
selectedJob={selectedJob}
loading={loading}
data={data ? data.jobs : []}
/>
</div>
);
}
export default connect(mapStateToProps, null)(ContractJobsContainer);

View File

@@ -0,0 +1,35 @@
import React, { useState, useEffect } from "react";
import { Select } from "antd";
import { useTranslation } from "react-i18next";
const { Option } = Select;
const ContractStatusComponent = ({
value = "contracts.status.new",
onChange
}) => {
const [option, setOption] = useState(value);
const { t } = useTranslation();
useEffect(() => {
if (onChange) {
onChange(option);
}
}, [option, onChange]);
return (
<Select
value={option}
style={{
width: 100
}}
onChange={setOption}
>
<Option value="contracts.status.new">{t("contracts.status.new")}</Option>
<Option value="contracts.status.out">{t("contracts.status.out")}</Option>
<Option value="contracts.status.returned">
{t("contracts.status.returned")}
</Option>
</Select>
);
};
export default ContractStatusComponent;

View File

@@ -0,0 +1,102 @@
import { Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { alphaSort } from "../../utils/sorters";
import { DateFormatter } from "../../utils/DateFormatter";
export default function ContractsList({ loading, contracts }) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" }
});
const { t } = useTranslation();
const columns = [
{
title: t("contracts.fields.agreementnumber"),
dataIndex: "agreementnumber",
key: "agreementnumber",
sorter: (a, b) => a.agreementnumber - b.agreementnumber,
sortOrder:
state.sortedInfo.columnKey === "agreementnumber" &&
state.sortedInfo.order,
render: (text, record) => (
<Link to={`/manage/courtesycars/contracts/${record.id}`}>
{record.agreementnumber || ""}
</Link>
)
},
{
title: t("jobs.fields.ro_number"),
dataIndex: "job.ro_number",
key: "job.ro_number",
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
sortOrder:
state.sortedInfo.columnKey === "job.ro_number" &&
state.sortedInfo.order,
render: (text, record) => (
<Link to={`/manage/jobs/${record.job.id}`}>
{record.job.ro_number || ""}
</Link>
)
},
{
title: t("contracts.fields.driver"),
dataIndex: "driver_ln",
key: "driver_ln",
sorter: (a, b) => alphaSort(a.driver_ln, b.driver_ln),
sortOrder:
state.sortedInfo.columnKey === "driver_ln" && state.sortedInfo.order,
render: (text, record) =>
`${record.driver_fn || ""} ${record.driver_ln || ""}`
},
{
title: t("contracts.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => t(record.status)
},
{
title: t("contracts.fields.start"),
dataIndex: "start",
key: "start",
sorter: (a, b) => alphaSort(a.start, b.start),
sortOrder:
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
render: (text, record) => <DateFormatter>{record.start}</DateFormatter>
},
{
title: t("contracts.fields.scheduledreturn"),
dataIndex: "scheduledreturn",
key: "scheduledreturn",
sorter: (a, b) => alphaSort(a.scheduledreturn, b.scheduledreturn),
sortOrder:
state.sortedInfo.columnKey === "scheduledreturn" &&
state.sortedInfo.order,
render: (text, record) => (
<DateFormatter>{record.scheduledreturn}</DateFormatter>
)
}
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Table
loading={loading}
size="small"
pagination={{ position: "top" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={contracts}
onChange={handleTableChange}
/>
);
}

View File

@@ -0,0 +1,100 @@
import { Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { alphaSort } from "../../utils/sorters";
import { DateFormatter } from "../../utils/DateFormatter";
export default function CourtesyCarContractListComponent({ contracts }) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" }
});
const { t } = useTranslation();
const columns = [
{
title: t("contracts.fields.agreementnumber"),
dataIndex: "agreementnumber",
key: "agreementnumber",
sorter: (a, b) => a.agreementnumber - b.agreementnumber,
sortOrder:
state.sortedInfo.columnKey === "agreementnumber" &&
state.sortedInfo.order,
render: (text, record) => (
<Link to={`/manage/courtesycars/contracts/${record.id}`}>
{record.agreementnumber || ""}
</Link>
)
},
{
title: t("jobs.fields.ro_number"),
dataIndex: "job.ro_number",
key: "job.ro_number",
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
sortOrder:
state.sortedInfo.columnKey === "job.ro_number" &&
state.sortedInfo.order,
render: (text, record) => (
<Link to={`/manage/jobs/${record.job.id}`}>
{record.job.ro_number || ""}
</Link>
)
},
{
title: t("contracts.fields.driver"),
dataIndex: "driver_ln",
key: "driver_ln",
sorter: (a, b) => alphaSort(a.driver_ln, b.driver_ln),
sortOrder:
state.sortedInfo.columnKey === "driver_ln" && state.sortedInfo.order,
render: (text, record) =>
`${record.driver_fn || ""} ${record.driver_ln || ""}`
},
{
title: t("contracts.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => t(record.status)
},
{
title: t("contracts.fields.start"),
dataIndex: "start",
key: "start",
sorter: (a, b) => alphaSort(a.start, b.start),
sortOrder:
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
render: (text, record) => <DateFormatter>{record.start}</DateFormatter>
},
{
title: t("contracts.fields.scheduledreturn"),
dataIndex: "scheduledreturn",
key: "scheduledreturn",
sorter: (a, b) => a.scheduledreturn - b.scheduledreturn,
sortOrder:
state.sortedInfo.columnKey === "scheduledreturn" &&
state.sortedInfo.order,
render: (text, record) => (
<DateFormatter>{record.scheduledreturn}</DateFormatter>
)
}
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Table
size="small"
pagination={{ position: "top" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={contracts}
onChange={handleTableChange}
/>
);
}

View File

@@ -0,0 +1,200 @@
import React from "react";
import { Form, Input, InputNumber, DatePicker, Button } from "antd";
import { useTranslation } from "react-i18next";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
export default function CourtesyCarCreateFormComponent() {
const { t } = useTranslation();
return (
<div>
<Button type="primary" htmlType="submit">
{t("general.actions.save")}
</Button>
<Form.Item
label={t("courtesycars.fields.make")}
name="make"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.model")}
name="model"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.year")}
name="year"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.plate")}
name="plate"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.color")}
name="color"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.vin")}
name="vin"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.fleetnumber")}
name="fleetnumber"
>
<Input />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.purchasedate")}
name="purchasedate"
>
<DatePicker />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.servicestartdate")}
name="servicestartdate"
>
<Input />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.serviceenddate")}
name="serviceenddate"
>
<DatePicker />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.leaseenddate")}
name="leaseenddate"
>
<DatePicker />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.status")}
name="status"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<CourtesyCarStatus />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.nextservicekm")}
name="nextservicekm"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.nextservicedate")}
name="nextservicedate"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<DatePicker />
</Form.Item>
<Form.Item label={t("courtesycars.fields.damage")} name="damage">
<Input />
</Form.Item>
<Form.Item label={t("courtesycars.fields.notes")} name="notes">
<Input />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.fuel")}
name="fuel"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<CourtesyCarFuelSlider />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.registrationexpires")}
name="registrationexpires"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<DatePicker />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.insuranceexpires")}
name="insuranceexpires"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<DatePicker />
</Form.Item>
<Form.Item label={t("courtesycars.fields.dailycost")} name="dailycost">
<CurrencyInput />
</Form.Item>
</div>
);
}

View File

@@ -0,0 +1,46 @@
import { Slider } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
const CourtesyCarFuelComponent = ({ value = 100, onChange }) => {
const [option, setOption] = useState(value);
const { t } = useTranslation();
useEffect(() => {
if (onChange) {
onChange(option);
}
}, [option, onChange]);
const marks = {
0: {
style: {
color: "#f50"
},
label: t("courtesycars.labels.fuel.empty")
},
13: t("courtesycars.labels.fuel.18"),
25: t("courtesycars.labels.fuel.14"),
38: t("courtesycars.labels.fuel.38"),
50: t("courtesycars.labels.fuel.12"),
63: t("courtesycars.labels.fuel.58"),
75: t("courtesycars.labels.fuel.34"),
88: t("courtesycars.labels.fuel.78"),
100: {
style: {
color: "#008000"
},
label: <strong>{t("courtesycars.labels.fuel.full")}</strong>
}
};
return (
<Slider
marks={marks}
defaultValue={value}
onChange={setOption}
step={null}
/>
);
};
export default CourtesyCarFuelComponent;

View File

@@ -0,0 +1,49 @@
import { Form, DatePicker, InputNumber } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
export default function CourtesyCarReturnModalComponent() {
const { t } = useTranslation();
return (
<div>
<Form.Item
label={t("contracts.fields.actualreturn")}
name="actualreturn"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<DatePicker />
</Form.Item>
<Form.Item
label={t("contracts.fields.kmend")}
name="kmend"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.fuel")}
name={["courtesycar", "data", "fuel"]}
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<CourtesyCarFuelSlider />
</Form.Item>
</div>
);
}

View File

@@ -0,0 +1,84 @@
import { Form, Modal, notification } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectCourtesyCarReturn } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CourtesyCarReturnModalComponent from "./courtesy-car-return-modal.component";
import moment from "moment";
import { RETURN_CONTRACT } from "../../graphql/cccontracts.queries";
import { useMutation } from "@apollo/react-hooks";
const mapStateToProps = createStructuredSelector({
courtesyCarReturnModal: selectCourtesyCarReturn,
bodyshop: selectBodyshop
});
const mapDispatchToProps = dispatch => ({
toggleModalVisible: () => dispatch(toggleModalVisible("courtesyCarReturn"))
});
export function InvoiceEnterModalContainer({
courtesyCarReturnModal,
toggleModalVisible,
bodyshop
}) {
const { visible, context, actions } = courtesyCarReturnModal;
const { t } = useTranslation();
const [form] = Form.useForm();
const [updateContract] = useMutation(RETURN_CONTRACT);
const handleFinish = values => {
updateContract({
variables: {
contractId: context.contractId,
cccontract: {
kmend: values.kmend,
actualreturn: values.actualreturn,
status: "contracts.status.returned"
},
courtesycarid: context.courtesyCarId,
courtesycar: {
status: "courtesycars.status.in",
fuel: values.fuel,
mileage: values.kmend
}
}
})
.then(r => {
if (actions.refetch) actions.refetch();
toggleModalVisible();
})
.catch(error => {
notification["error"]({
message: t("contracts.errors.returning", { error: error })
});
});
};
return (
<Modal
title={t("courtesycars.labels.return")}
visible={visible}
onCancel={() => toggleModalVisible()}
width={"90%"}
okText={t("general.actions.save")}
onOk={() => form.submit()}
okButtonProps={{ htmlType: "submit" }}
>
<Form
form={form}
onFinish={handleFinish}
initialValues={{ fuel: 100, actualreturn: moment(new Date()) }}
>
<CourtesyCarReturnModalComponent />
</Form>
</Modal>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(InvoiceEnterModalContainer);

View File

@@ -0,0 +1,39 @@
import React, { useState, useEffect } from "react";
import { Select } from "antd";
import { useTranslation } from "react-i18next";
const { Option } = Select;
const CourtesyCarStatusComponent = ({
value = "courtesycars.status.in",
onChange
}) => {
const [option, setOption] = useState(value);
const { t } = useTranslation();
useEffect(() => {
if (onChange) {
onChange(option);
}
}, [option, onChange]);
return (
<Select
value={option}
style={{
width: 100
}}
onChange={setOption}
>
<Option value="courtesycars.status.in">
{t("courtesycars.status.in")}
</Option>
<Option value="courtesycars.status.inservice">
{t("courtesycars.status.inservice")}
</Option>
<Option value="courtesycars.status.out">
{t("courtesycars.status.out")}
</Option>
</Select>
);
};
export default CourtesyCarStatusComponent;

View File

@@ -0,0 +1,82 @@
import { Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { alphaSort } from "../../utils/sorters";
export default function CourtesyCarsList({ loading, courtesycars }) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" }
});
const { t } = useTranslation();
const columns = [
{
title: t("courtesycars.fields.fleetnumber"),
dataIndex: "fleetnumber",
key: "fleetnumber",
sorter: (a, b) => alphaSort(a.fleetnumber, b.fleetnumber),
sortOrder:
state.sortedInfo.columnKey === "fleetnumber" && state.sortedInfo.order
},
{
title: t("courtesycars.fields.vin"),
dataIndex: "vin",
key: "vin",
sorter: (a, b) => alphaSort(a.vin, b.vin),
sortOrder: state.sortedInfo.columnKey === "vin" && state.sortedInfo.order,
render: (text, record) => (
<Link to={`/manage/courtesycars/${record.id}`}>{record.vin}</Link>
)
},
{
title: t("courtesycars.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => t(record.status)
},
{
title: t("courtesycars.fields.year"),
dataIndex: "year",
key: "year",
sorter: (a, b) => alphaSort(a.year, b.year),
sortOrder: state.sortedInfo.columnKey === "year" && state.sortedInfo.order
},
{
title: t("courtesycars.fields.make"),
dataIndex: "make",
key: "make",
sorter: (a, b) => alphaSort(a.make, b.make),
sortOrder: state.sortedInfo.columnKey === "make" && state.sortedInfo.order
},
{
title: t("courtesycars.fields.model"),
dataIndex: "model",
key: "model",
sorter: (a, b) => alphaSort(a.model, b.model),
sortOrder:
state.sortedInfo.columnKey === "model" && state.sortedInfo.order
}
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Table
loading={loading}
size="small"
pagination={{ position: "top" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={courtesycars}
onChange={handleTableChange}
/>
);
}

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,64 @@
import { Card } from "antd";
import React, { useState } from "react";
import { Responsive, WidthProvider } from "react-grid-layout";
import styled from "styled-components";
//Combination of the following:
// /node_modules/react-grid-layout/css/styles.css
// /node_modules/react-resizable/css/styles.css
import "./dashboard-grid.styles.css";
const Sdiv = styled.div`
position: absolute;
height: 80%;
width: 80%;
top: 10%;
left: 10%;
// background-color: #ffcc00;
`;
const ResponsiveReactGridLayout = WidthProvider(Responsive);
export default function DashboardGridComponent() {
const [state, setState] = useState({
layout: [
{ i: "1", x: 0, y: 0, w: 2, h: 2 },
{ i: "2", x: 2, y: 0, w: 2, h: 2 },
{ i: "3", x: 4, y: 0, w: 2, h: 2 }
]
});
const defaultProps = {
className: "layout",
breakpoints: { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }
// cols: { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 },
// rowHeight: 100
};
// We're using the cols coming back from this to calculate where to add new items.
const onBreakpointChange = (breakpoint, cols) => {
console.log("breakpoint, cols", breakpoint, cols);
// setState({ ...state, breakpoint: breakpoint, cols: cols });
};
if (true) return null;
return (
<Sdiv>
The Grid.
<ResponsiveReactGridLayout
{...defaultProps}
onBreakpointChange={onBreakpointChange}
width='100%'
onLayoutChange={layout => {
console.log("layout", layout);
setState({ ...state, layout });
}}>
{state.layout.map((item, index) => {
return (
<Card style={{ width: "100px" }} key={item.i} data-grid={item}>
A Card {index}
</Card>
);
})}
</ResponsiveReactGridLayout>
</Sdiv>
);
}

View File

@@ -0,0 +1,126 @@
.react-resizable {
position: relative;
}
.react-resizable-handle {
position: absolute;
width: 20px;
height: 20px;
background-repeat: no-repeat;
background-origin: content-box;
box-sizing: border-box;
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iNnB4Ij48ZyBvcGFjaXR5PSIwLjMwMiI+PHBhdGggZD0iTSA2IDYgTCAwIDYgTCAwIDQuMiBMIDQgNC4yIEwgNC4yIDQuMiBMIDQuMiAwIEwgNiAwIEwgNiA2IEwgNiA2IFoiIGZpbGw9IiMwMDAwMDAiLz48L2c+PC9zdmc+");
background-position: bottom right;
padding: 0 3px 3px 0;
}
.react-resizable-handle-sw {
bottom: 0;
left: 0;
cursor: sw-resize;
transform: rotate(90deg);
}
.react-resizable-handle-se {
bottom: 0;
right: 0;
cursor: se-resize;
}
.react-resizable-handle-nw {
top: 0;
left: 0;
cursor: nw-resize;
transform: rotate(180deg);
}
.react-resizable-handle-ne {
top: 0;
right: 0;
cursor: ne-resize;
transform: rotate(270deg);
}
.react-resizable-handle-w,
.react-resizable-handle-e {
top: 50%;
margin-top: -10px;
cursor: ew-resize;
}
.react-resizable-handle-w {
left: 0;
transform: rotate(135deg);
}
.react-resizable-handle-e {
right: 0;
transform: rotate(315deg);
}
.react-resizable-handle-n,
.react-resizable-handle-s {
left: 50%;
margin-left: -10px;
cursor: ns-resize;
}
.react-resizable-handle-n {
top: 0;
transform: rotate(225deg);
}
.react-resizable-handle-s {
bottom: 0;
transform: rotate(45deg);
}
.react-grid-layout {
position: relative;
transition: height 200ms ease;
}
.react-grid-item {
transition: all 200ms ease;
transition-property: left, top;
}
.react-grid-item.cssTransforms {
transition-property: transform;
}
.react-grid-item.resizing {
z-index: 1;
will-change: width, height;
}
.react-grid-item.react-draggable-dragging {
transition: none;
z-index: 3;
will-change: transform;
}
.react-grid-item.dropping {
visibility: hidden;
}
.react-grid-item.react-grid-placeholder {
background: red;
opacity: 0.2;
transition-duration: 100ms;
z-index: 2;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
.react-grid-item > .react-resizable-handle {
position: absolute;
width: 20px;
height: 20px;
bottom: 0;
right: 0;
cursor: se-resize;
}
.react-grid-item > .react-resizable-handle::after {
content: "";
position: absolute;
right: 3px;
bottom: 3px;
width: 5px;
height: 5px;
border-right: 2px solid rgba(0, 0, 0, 0.4);
border-bottom: 2px solid rgba(0, 0, 0, 0.4);
}
.react-resizable-hide > .react-resizable-handle {
display: none;
}

View File

@@ -0,0 +1,20 @@
import { UploadOutlined } from "@ant-design/icons";
import { Button, Upload } from "antd";
import React from "react";
export default function DocumentsUploadComponent({ handleUpload, UploadRef }) {
return (
<div>
<Upload
multiple={true}
customRequest={handleUpload}
accept="audio/*,video/*,image/*"
ref={UploadRef}
>
<Button>
<UploadOutlined /> Click to Upload
</Button>
</Upload>
</div>
);
}

View File

@@ -0,0 +1,165 @@
import { useMutation } from "@apollo/react-hooks";
import { notification } from "antd";
import axios from "axios";
import React, { useRef } from "react";
import { useTranslation } from "react-i18next";
import Resizer from "react-image-file-resizer";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import DocumentsUploadComponent from "./documents-upload.component";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
bodyshop: selectBodyshop,
});
export function DocumentsUploadContainer({
jobId,
tagsArray,
invoiceId,
currentUser,
bodyshop,
callbackAfterUpload,
onChange,
}) {
const { t } = useTranslation();
const [insertNewDocument] = useMutation(INSERT_NEW_DOCUMENT);
const UploadRef = useRef(null);
const handleUpload = (ev) => {
const { onError, onSuccess, onProgress } = ev;
//If PDF, upload directly.
//If JPEG, resize and upload.
//TODO If this is just an invoice job? Where to put it?
let key = `${bodyshop.id}/${jobId}/${ev.file.name}`;
if (ev.file.type.includes("image")) {
Resizer.imageFileResizer(
ev.file,
2500,
2500,
"JPEG",
75,
0,
(uri) => {
let file = new File([uri], ev.file.name, {});
file.uid = ev.file.uid;
uploadToS3(key, file.type, file, onError, onSuccess, onProgress);
},
"blob"
);
} else {
uploadToS3(key, ev.file.type, ev.file, onError, onSuccess, onProgress);
}
};
const uploadToS3 = (
fileName,
fileType,
file,
onError,
onSuccess,
onProgress
) => {
let timestamp = Math.floor(Date.now() / 1000);
let public_id = fileName;
let tags = `${bodyshop.textid},${
tagsArray ? tagsArray.map((tag) => `${tag},`) : ""
}`;
let eager = "w_200,h_200,c_thumb";
axios
.post("/media/sign", {
eager: eager,
public_id: public_id,
tags: tags,
timestamp: timestamp,
})
.then((response) => {
var signature = response.data;
var options = {
headers: { "X-Requested-With": "XMLHttpRequest" },
onUploadProgress: (e) => {
onProgress({ percent: (e.loaded / e.total) * 100 });
},
};
const formData = new FormData();
formData.append("file", file);
formData.append("eager", eager);
formData.append("api_key", process.env.REACT_APP_CLOUDINARY_API_KEY);
formData.append("public_id", public_id);
formData.append("tags", tags);
formData.append("timestamp", timestamp);
formData.append("signature", signature);
axios
.post(
`${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/upload`,
formData,
options
)
.then((response) => {
console.log("response", response);
insertNewDocument({
variables: {
docInput: [
{
jobid: jobId,
uploaded_by: currentUser.email,
key: fileName,
invoiceid: invoiceId,
type: fileType,
},
],
},
}).then((r) => {
onSuccess({
uid: r.data.insert_documents.returning[0].id,
name: r.data.insert_documents.returning[0].name,
status: "done",
key: r.data.insert_documents.returning[0].key,
});
notification["success"]({
message: t("documents.successes.insert"),
});
if (callbackAfterUpload) {
callbackAfterUpload();
}
if (onChange) {
//Used in a form.
onChange(UploadRef.current.state.fileList);
}
});
})
.catch((error) => {
onError(error);
notification["error"]({
message: t("documents.errors.insert", {
message: JSON.stringify(error),
}),
});
});
})
.catch((error) => {
console.log("error", error);
notification["error"]({
message: t("documents.errors.getpresignurl", {
message: JSON.stringify(error),
}),
});
});
};
return (
<DocumentsUploadComponent
handleUpload={handleUpload}
UploadRef={UploadRef}
/>
);
}
export default connect(mapStateToProps, null)(DocumentsUploadContainer);

View File

@@ -0,0 +1,49 @@
import { Editor } from "@tinymce/tinymce-react";
import { Input } from "antd";
import React from "react";
export default function EmailOverlayComponent({
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"
/>
<Editor
value={messageOptions.html}
apiKey="f3s2mjsd77ya5qvqkee9vgh612cm6h41e85efqakn2d0kknk"
init={{
height: 500,
//menubar: false,
encoding: "raw",
extended_valid_elements: "span",
//entity_encoding: "raw",
plugins: [
"advlist autolink lists link image charmap print preview anchor",
"searchreplace visualblocks code fullscreen",
"insertdatetime media table paste code help wordcount",
],
toolbar:
"undo redo | formatselect | bold italic backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | removeformat | help",
}}
onEditorChange={handleHtmlChange}
/>
</div>
);
}

View File

@@ -0,0 +1,145 @@
import { useApolloClient } from "@apollo/react-hooks";
import { Modal, notification } from "antd";
import { gql } from "apollo-boost";
import axios from "axios";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_TEMPLATES_BY_NAME } from "../../graphql/templates.queries";
import { toggleEmailOverlayVisible } from "../../redux/email/email.actions";
import {
selectEmailConfig,
selectEmailVisible,
} from "../../redux/email/email.selectors.js";
import { selectBodyshop } from "../../redux/user/user.selectors";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import EmailOverlayComponent from "./email-overlay.component";
const mapStateToProps = createStructuredSelector({
modalVisible: selectEmailVisible,
emailConfig: selectEmailConfig,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
toggleEmailOverlayVisible: () => dispatch(toggleEmailOverlayVisible()),
});
export function EmailOverlayContainer({
emailConfig,
modalVisible,
toggleEmailOverlayVisible,
bodyshop,
}) {
const { t } = useTranslation();
const [messageOptions, setMessageOptions] = useState(
emailConfig.messageOptions
);
const client = useApolloClient();
const renderEmail = () => {
client
.query({
query: QUERY_TEMPLATES_BY_NAME,
variables: { name: emailConfig.template.name },
fetchPolicy: "network-only",
})
.then(({ data: templateRecords }) => {
let templateToUse;
if (templateRecords.templates.length === 1) {
console.log("Only 1 Template found.");
templateToUse = templateRecords.templates[0];
} else if (templateRecords.templates.length === 2) {
console.log("2 Templates found..");
templateToUse = templateRecords.templates.filter(
(t) => !!t.bodyshopid
);
} else {
//No template found.Uh oh.
alert("Templating Error!");
}
client
.query({
query: gql(templateToUse.query),
variables: { ...emailConfig.template.variables },
fetchPolicy: "network-only",
})
.then(({ data: contextData }) => {
handleRender(contextData, templateToUse.html);
});
});
};
const handleRender = (contextData, html) => {
axios
.post("/render", {
view: html,
context: { ...contextData, bodyshop: bodyshop },
})
.then((r) => {
setMessageOptions({ ...messageOptions, html: r.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 });
};
useEffect(() => {
if (modalVisible) renderEmail();
}, [modalVisible]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<Modal
destroyOnClose={true}
visible={modalVisible}
width={"80%"}
onOk={handleOk}
onCancel={() => {
toggleEmailOverlayVisible();
}}
>
<LoadingSpinner loading={false}>
<EmailOverlayComponent
handleConfigChange={handleConfigChange}
messageOptions={messageOptions}
handleHtmlChange={handleHtmlChange}
/>
<button
onClick={() => {
console.log(messageOptions.html);
navigator.clipboard.writeText(messageOptions.html);
}}
>
Get HTML
</button>
</LoadingSpinner>
</Modal>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(EmailOverlayContainer);

View File

@@ -0,0 +1,61 @@
import { Select, Tag } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
const { Option } = Select;
//To be used as a form element only.
const EmployeeSearchSelect = ({
value,
onChange,
options,
onSelect,
onBlur,
}) => {
const [option, setOption] = useState(value);
const { t } = useTranslation();
useEffect(() => {
if (onChange) {
onChange(option);
}
}, [option, onChange]);
return (
<Select
showSearch
value={option}
style={{
width: 400,
}}
onChange={setOption}
optionFilterProp="search"
onSelect={onSelect}
onBlur={onBlur}
>
{options
? options.map((o) => (
<Option
key={o.id}
value={o.id}
search={`${o.employee_number} ${o.first_name} ${o.last_name}`}
discount={o.discount}
>
<div style={{ display: "flex" }}>
{`${o.employee_number} ${o.first_name} ${o.last_name}`}
<Tag color="blue">{o.cost_center}</Tag>
<Tag color="red">
<CurrencyFormatter>{o.base_rate}</CurrencyFormatter>
</Tag>
<Tag color="green">
{o.flat_rate
? t("timetickets.labels.flat_rate")
: t("timetickets.labels.straight_time")}
</Tag>
</div>
</Option>
))
: null}
</Select>
);
};
export default EmployeeSearchSelect;

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

@@ -0,0 +1,14 @@
import { InputNumber } from "antd";
import React, { forwardRef } from "react";
function FormItemCurrency(props, ref) {
return (
<InputNumber
{...props}
//formatter={value => `$ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
// parser={value => value.replace(/\$\s?|(,*)/g, "")}
precision={2}
/>
);
}
export default forwardRef(FormItemCurrency);

View File

@@ -1,13 +1,18 @@
import { Icon, Input } from "antd";
import { Input } from "antd";
import { MailFilled } from "@ant-design/icons";
import React, { forwardRef } from "react";
function FormItemEmail(props, ref) {
return (
<Input
{...props}
addonAfter={
<a href={`mailto:${props.email}`}>
<Icon type="mail" />
</a>
props.email ? (
<a href={`mailto:${props.email}`}>
<MailFilled />
</a>
) : (
<MailFilled />
)
}
/>
);

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

@@ -0,0 +1,54 @@
//import { useNProgress } from "@tanem/react-nprogress";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectLoading } from "../../redux/application/application.selectors";
const mapStateToProps = createStructuredSelector({
loading: selectLoading,
});
export default connect(mapStateToProps, null)(GlobalLoadingHeader);
function GlobalLoadingHeader({ loading }) {
return <span></span>;
// const { animationDuration, isFinished, progress } = useNProgress({
// isAnimating: loading,
// });
// return (
// <div
// style={{
// opacity: isFinished ? 0 : 1,
// pointerEvents: "none",
// transition: `opacity ${animationDuration}ms linear`,
// }}
// >
// <div
// style={{
// background: "#29d",
// height: 4,
// left: 0,
// marginLeft: `${(-1 + progress) * 100}%`,
// position: "fixed",
// top: 0,
// transition: `margin-left ${animationDuration}ms linear`,
// width: "100%",
// zIndex: 1031,
// }}
// >
// <div
// style={{
// boxShadow: "0 0 10px #29d, 0 0 5px #29d",
// display: "block",
// height: "100%",
// opacity: 1,
// position: "absolute",
// right: 0,
// transform: "rotate(3deg) translate(0px, -4px)",
// width: 100,
// }}
// />
// </div>
// </div>
// );
}

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,74 +1,280 @@
import { useApolloClient } from "@apollo/react-hooks";
import { Col, Icon, Menu, Row } from "antd";
import Icon, {
CarFilled,
FileAddFilled,
FileFilled,
GlobalOutlined,
HomeFilled,
TeamOutlined,
DollarCircleFilled,
} from "@ant-design/icons";
import { Avatar, Col, Menu, Row } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { FaCalendarAlt, FaCarCrash } from "react-icons/fa";
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 ManageSignInButton from "../manage-sign-in-button/manage-sign-in-button.component";
import "./header.styles.scss";
export default ({ landingHeader, navItems, selectedNavItem }) => {
const apolloClient = useApolloClient();
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
setInvoiceEnterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "invoiceEnter" })),
setTimeTicketContext: (context) =>
dispatch(setModalContext({ context: context, modal: "timeTicket" })),
});
function Header({
landingHeader,
selectedNavItem,
logo,
handleMenuClick,
currentUser,
signOutStart,
setInvoiceEnterContext,
setTimeTicketContext,
}) {
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={21}>
{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>
<GlobalOutlined />
<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-CA">
{t("general.languages.french")}
</Menu.Item>
<Menu.Item actiontype="lang-select" key="es-MX">
{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">
<HomeFilled />
{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={
<span>
<Icon component={FaCarCrash} />
<span>{t("menus.header.jobs")}</span>
</span>
}
>
<Menu.Item key="schedule">
<Link to="/manage/schedule">
<Icon component={FaCalendarAlt} />
{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">
<TeamOutlined />
{t("menus.header.owners")}
</Link>
</Menu.Item>
<Menu.Item key="vehicles">
<Link to="/manage/vehicles">
<CarFilled />
{t("menus.header.vehicles")}
</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>
// ))
}
<Menu.SubMenu
title={
<span>
<CarFilled />
<span>{t("menus.header.courtesycars")}</span>
</span>
}
>
<Menu.Item key="courtesycarsall">
<Link to="/manage/courtesycars">
<CarFilled />
{t("menus.header.courtesycars-all")}
</Link>
</Menu.Item>
<Menu.Item key="contracts">
<Link to="/manage/courtesycars/contracts">
<FileFilled />
{t("menus.header.courtesycars-contracts")}
</Link>
</Menu.Item>
<Menu.Item key="newcontract">
<Link to="/manage/courtesycars/contracts/new">
<FileAddFilled />
{t("menus.header.courtesycars-newcontract")}
</Link>
</Menu.Item>
</Menu.SubMenu>
{!landingHeader ? null : (
<Menu.Item>
<ManageSignInButton />
</Menu.Item>
)}
</Menu>
</Col>
<Col span={6} offset={2}>
{!landingHeader ? <CurrentUserDropdown /> : null}
<Menu.SubMenu
title={
<span>
<DollarCircleFilled />
<span>{t("menus.header.accounting")}</span>
</span>
}
>
<Menu.Item
key="enterinvoices"
onClick={() => {
setInvoiceEnterContext({
actions: {},
context: {},
});
}}
>
{t("menus.header.enterinvoices")}
</Menu.Item>
<Menu.Item key="invoices">
<Link to="/manage/invoices">{t("menus.header.invoices")}</Link>
</Menu.Item>
<Menu.Item
key="entertimetickets"
onClick={() => {
setTimeTicketContext({
actions: {},
context: {},
});
}}
>
{t("menus.header.entertimeticket")}
</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>
<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>
<GlobalOutlined />
<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-CA">
{t("general.languages.french")}
</Menu.Item>
<Menu.Item actiontype="lang-select" key="es-MX">
{t("general.languages.spanish")}
</Menu.Item>
</Menu.SubMenu>
</Menu.SubMenu>
</Menu>
)}
</Col>
</Row>
);
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Header);

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,36 @@
import React, { useState } from "react";
import { Button, Popover, Input, InputNumber, Form } from "antd";
import { SelectOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
export default function InvoiceAddLineButton({ jobLine, discount, disabled }) {
const [visibility, setVisibility] = useState(false);
const { t } = useTranslation();
const popContent = (
<div style={{ display: "flex" }}>
<Form.Item name="line_desc" label={t("joblines.fields.line_desc")}>
<Input />
</Form.Item>
<Form.Item name="oem_partno" label={t("joblines.fields.oem_partno")}>
<Input />
</Form.Item>
<Form.Item name="retail" label={t("invoicelines.fields.retail")}>
<InputNumber precision={2} />
</Form.Item>
<Form.Item name="actual" label={t("invoicelines.fields.actual")}>
<InputNumber precision={2} />
</Form.Item>
DISC: {discount}
<Button onClick={() => setVisibility(false)}>X</Button>
</div>
);
return (
<Popover content={popContent} visible={visibility}>
<Button onClick={() => setVisibility(true)} disabled={!disabled}>
<SelectOutlined />
</Button>
</Popover>
);
}

View File

@@ -0,0 +1,131 @@
import { DatePicker, Form, Input, Switch } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import JobSearchSelect from "../job-search-select/job-search-select.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import InvoiceEnterModalLinesComponent from "./invoice-enter-modal.lines.component";
import DocumentsUploadContainer from "../documents-upload/documents-upload.container";
export default function InvoiceEnterModalComponent({
form,
roAutoCompleteOptions,
vendorAutoCompleteOptions,
lineData,
responsibilityCenters,
loadLines,
}) {
const { t } = useTranslation();
const [discount, setDiscount] = useState(0);
const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount);
};
return (
<div>
<div style={{ display: "flex" }}>
<Form.Item
name="jobid"
label={t("invoices.fields.ro_number")}
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<JobSearchSelect
options={roAutoCompleteOptions}
onBlur={() => {
if (form.getFieldValue("jobid") !== null) {
loadLines({ variables: { id: form.getFieldValue("jobid") } });
}
}}
/>
</Form.Item>
<Form.Item
label={t("invoices.fields.vendor")}
name="vendorid"
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<VendorSearchSelect
options={vendorAutoCompleteOptions}
onSelect={handleVendorSelect}
/>
</Form.Item>
</div>
<div style={{ display: "flex" }}>
<Form.Item
label={t("invoices.fields.invoice_number")}
name="invoice_number"
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("invoices.fields.date")}
name="date"
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<DatePicker />
</Form.Item>
<Form.Item
label={t("invoices.fields.is_credit_memo")}
name="is_credit_memo"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label={t("invoices.fields.total")}
name="total"
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<CurrencyInput />
</Form.Item>
</div>
<InvoiceEnterModalLinesComponent
lineData={lineData}
discount={discount}
form={form}
responsibilityCenters={responsibilityCenters}
/>
<Form.Item
name="upload"
label="Upload"
>
<DocumentsUploadContainer jobId={form.getFieldValue("jobid")} />
</Form.Item>
<button
onClick={() => {
console.log(form.getFieldsValue());
}}
>
a
</button>
</div>
);
}

View File

@@ -0,0 +1,167 @@
import { useLazyQuery, useMutation, useQuery } from "@apollo/react-hooks";
import { Form, Modal, notification, Button } from "antd";
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_NEW_INVOICE } from "../../graphql/invoices.queries";
import { GET_JOB_LINES_TO_ENTER_INVOICE } from "../../graphql/jobs-lines.queries";
import { ACTIVE_JOBS_FOR_AUTOCOMPLETE } from "../../graphql/jobs.queries";
import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectInvoiceEnterModal } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import InvoiceEnterModalComponent from "./invoice-enter-modal.component";
import { setModalContext } from "../../redux/modals/modals.actions";
const mapStateToProps = createStructuredSelector({
invoiceEnterModal: selectInvoiceEnterModal,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("invoiceEnter")),
setInvoiceEnterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "invoiceEnter" })),
});
function InvoiceEnterModalContainer({
invoiceEnterModal,
toggleModalVisible,
bodyshop,
setInvoiceEnterContext,
}) {
const [form] = Form.useForm();
const { t } = useTranslation();
const [enterAgain, setEnterAgain] = useState(false);
const [insertInvoice] = useMutation(INSERT_NEW_INVOICE);
const { data: RoAutoCompleteData } = useQuery(ACTIVE_JOBS_FOR_AUTOCOMPLETE, {
fetchPolicy: "network-only",
variables: { statuses: bodyshop.md_ro_statuses.open_statuses || ["Open"] },
skip: !invoiceEnterModal.visible,
});
const { data: VendorAutoCompleteData } = useQuery(
SEARCH_VENDOR_AUTOCOMPLETE,
{
fetchPolicy: "network-only",
skip: !invoiceEnterModal.visible,
}
);
const [loadLines, { data: lineData }] = useLazyQuery(
GET_JOB_LINES_TO_ENTER_INVOICE,
{
fetchPolicy: "network-only",
}
);
const handleFinish = (values) => {
insertInvoice({
variables: {
invoice: [
Object.assign({}, values, {
invoicelines: { data: values.invoicelines },
}),
],
},
})
.then((r) => {
notification["success"]({
message: t("invoices.successes.created"),
});
if (invoiceEnterModal.actions.refetch)
invoiceEnterModal.actions.refetch();
if (enterAgain) {
form.resetFields();
} else {
toggleModalVisible();
}
setEnterAgain(false);
})
.catch((error) => {
setEnterAgain(false);
notification["error"]({
message: t("invoices.errors.creating", {
message: JSON.stringify(error),
}),
});
});
};
const handleCancel = () => {
toggleModalVisible();
};
useEffect(() => {
if (enterAgain) form.submit();
}, [enterAgain, form]);
return (
<Modal
title={
invoiceEnterModal.context && invoiceEnterModal.context.id
? t("invoices.labels.edit")
: t("invoices.labels.new")
}
width={"90%"}
visible={invoiceEnterModal.visible}
okText={t("general.actions.save")}
onOk={() => form.submit()}
onCancel={handleCancel}
afterClose={() => form.resetFields()}
footer={
<span>
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
<Button onClick={() => form.submit()}>
{t("general.actions.save")}
</Button>
{invoiceEnterModal.context && invoiceEnterModal.context.id ? null : (
<Button
type="primary"
onClick={() => {
setEnterAgain(true);
}}
>
{t("general.actions.saveandnew")}
</Button>
)}
</span>
}
destroyOnClose
>
<Form
onFinish={handleFinish}
autoComplete={"off"}
form={form}
onFinishFailed={() => {
setEnterAgain(false);
console.log("Finish failed");
}}
initialValues={{
jobid:
(invoiceEnterModal.context.job &&
invoiceEnterModal.context.job.id) ||
null,
}}
>
<InvoiceEnterModalComponent
form={form}
roAutoCompleteOptions={RoAutoCompleteData && RoAutoCompleteData.jobs}
vendorAutoCompleteOptions={
VendorAutoCompleteData && VendorAutoCompleteData.vendors
}
loadLines={loadLines}
lineData={lineData ? lineData.joblines : null}
responsibilityCenters={bodyshop.md_responsibility_centers || null}
/>
</Form>
</Modal>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(InvoiceEnterModalContainer);

View File

@@ -0,0 +1,245 @@
import { DeleteFilled } from "@ant-design/icons";
import { Button, Col, Form, Input, Row, Select, Tag } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
export default function InvoiceEnterModalLinesComponent({
lineData,
discount,
form,
responsibilityCenters
}) {
const { t } = useTranslation();
const { setFieldsValue, getFieldsValue } = form;
const [amounts, setAmounts] = useState({ invoiceTotal: 0, enteredAmount: 0 });
const calculateTotals = () => {
setAmounts({
invoiceTotal: getFieldsValue().total,
enteredTotal: getFieldsValue("invoicelines").invoicelines
? getFieldsValue("invoicelines").invoicelines.reduce(
(acc, value) =>
acc + (value && value.actual_cost ? value.actual_cost : 0),
0
)
: 0
});
};
return (
<div>
<Form.List name="invoicelines" >
{(fields, { add, remove }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item required={false} key={field.key}>
<div style={{ display: "flex" }}>
<Form.Item
label={t("invoicelines.fields.line_desc")}
key={`${index}joblinename`}
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Select
autoFocus
name={`le${index}`}
style={{ width: "450px" }}
onSelect={(value, opt) => {
setFieldsValue({
invoicelines: getFieldsValue([
"invoicelines"
]).invoicelines.map((item, idx) => {
if (idx === index) {
return {
...item,
joblineid: opt.key.includes("noline")
? null
: opt.key,
line_desc: opt.key.includes("noline")
? ""
: opt.value,
actual_price: opt.cost ? opt.cost : 0,
cost_center: opt.part_type
? responsibilityCenters.defaults[
opt.part_type
] || null
: null
};
}
return item;
})
});
}}
showSearch
>
<Select.Option
key={`${index}noline`}
value={t("invoicelines.labels.other")}
cost={0}
>
{t("invoicelines.labels.other")}
</Select.Option>
{lineData
? lineData.map(item => (
<Select.Option
key={item.id}
value={item.line_desc}
cost={item.act_price ? item.act_price : 0}
part_type={item.part_type}
>
<Row justify="center" align="middle">
<Col span={12}> {item.line_desc}</Col>
<Col span={8}>
<Tag color="blue">{item.oem_partno}</Tag>
</Col>
<Col span={4}>
<Tag color="green">
<CurrencyFormatter>
{item.act_price}
</CurrencyFormatter>
</Tag>
</Col>
</Row>
</Select.Option>
))
: null}
</Select>
</Form.Item>
{getFieldsValue("invoicelines").invoicelines[index] &&
getFieldsValue("invoicelines").invoicelines[index]
.joblinename &&
!getFieldsValue("invoicelines").invoicelines[index]
.joblineid ? (
<Form.Item
label={t("invoicelines.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
rules={[
{
required: !getFieldsValue("invoicelines")
.invoicelines[index].joblineid,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
) : null}
<Form.Item
label={t("invoicelines.fields.actual")}
key={`${index}actual_price`}
name={[field.name, "actual_price"]}
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<CurrencyInput
onBlur={e => {
setFieldsValue({
invoicelines: getFieldsValue(
"invoicelines"
).invoicelines.map((item, idx) => {
if (idx === index) {
return {
...item,
actual_cost: !!item.actual_cost
? item.actual_cost
: parseFloat(e.target.value) *
(1 - discount)
};
}
return item;
})
});
}}
/>
</Form.Item>
<Form.Item
label={t("invoicelines.fields.actual_cost")}
key={`${index}actual_cost`}
name={[field.name, "actual_cost"]}
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<CurrencyInput onBlur={() => calculateTotals()} />
</Form.Item>
<Form.Item
label={t("invoicelines.fields.cost_center")}
key={`${index}cost_center`}
name={[field.name, "cost_center"]}
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Select style={{ width: "150px" }}>
{responsibilityCenters.costs.map(item => (
<Select.Option key={item}>{item}</Select.Option>
))}
</Select>
</Form.Item>
<DeleteFilled
onClick={() => {
remove(field.name);
calculateTotals();
}}
/>
</div>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
{t("invoicelines.actions.newline")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
<Row>
<Col span={4}>
{t("invoicelines.labels.entered")}
<CurrencyFormatter>{amounts.enteredTotal || 0}</CurrencyFormatter>
</Col>
<Col span={4}>
{amounts.invoiceTotal - amounts.enteredTotal === 0 ? (
<Tag color="green">{t("invoicelines.labels.reconciled")}</Tag>
) : (
<Tag color="red">
{t("invoicelines.labels.unreconciled")}:
<CurrencyFormatter>
{amounts.invoiceTotal - amounts.enteredTotal}
</CurrencyFormatter>
</Tag>
)}
</Col>
</Row>
</div>
);
}

View File

@@ -0,0 +1,181 @@
import { Button, Descriptions, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
export default function InvoicesListTableComponent({
loading,
invoices,
selectedInvoice,
handleOnRowClick,
}) {
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
});
const columns = [
{
title: t("invoices.fields.vendorname"),
dataIndex: "vendorname",
key: "vendorname",
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
sortOrder:
state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
render: (text, record) => <span>{record.vendor.name}</span>,
},
{
title: t("invoices.fields.invoice_number"),
dataIndex: "invoice_number",
key: "invoice_number",
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
sortOrder:
state.sortedInfo.columnKey === "invoice_number" &&
state.sortedInfo.order,
},
{
title: t("invoices.fields.date"),
dataIndex: "date",
key: "date",
sorter: (a, b) => a.date - b.date,
sortOrder:
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
},
{
title: t("invoices.fields.total"),
dataIndex: "total",
key: "total",
sorter: (a, b) => a.total - b.total,
sortOrder:
state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.total}</CurrencyFormatter>
),
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<Link to={`/manage/invoices/${record.id}`}>
<Button>{t("invoices.actions.edit")}</Button>
</Link>
),
},
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const rowExpander = (record) => {
const columns = [
{
title: t("invoicelines.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
sortOrder:
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
},
{
title: t("invoicelines.fields.retail"),
dataIndex: "actual_price",
key: "actual_price",
sorter: (a, b) => a.actual_price - b.actual_price,
sortOrder:
state.sortedInfo.columnKey === "actual_price" &&
state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.actual_price}</CurrencyFormatter>
),
},
{
title: t("invoicelines.fields.actual_cost"),
dataIndex: "actual_cost",
key: "actual_cost",
sorter: (a, b) => a.actual_cost - b.actual_cost,
sortOrder:
state.sortedInfo.columnKey === "actual_cost" &&
state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.actual_cost}</CurrencyFormatter>
),
},
{
title: t("invoicelines.fields.cost_center"),
dataIndex: "cost_center",
key: "cost_center",
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
sortOrder:
state.sortedInfo.columnKey === "cost_center" &&
state.sortedInfo.order,
},
];
return (
<div>
<Descriptions title="User Info">
<Descriptions.Item label="UserName">Zhou Maomao</Descriptions.Item>
<Descriptions.Item label="Telephone">1810000000</Descriptions.Item>
<Descriptions.Item label="Live">Hangzhou, Zhejiang</Descriptions.Item>
<Descriptions.Item label="Remark">empty</Descriptions.Item>
<Descriptions.Item label="Address">
No. 18, Wantang Road, Xihu District, Hangzhou, Zhejiang, China
</Descriptions.Item>
</Descriptions>
<Table
size="small"
pagination={{ position: "top", defaultPageSize: 25 }}
columns={columns.map((item) => ({ ...item }))}
rowKey="id"
dataSource={record.invoicelines}
/>
</div>
);
};
return (
<Table
loading={loading}
size="small"
expandedRowRender={rowExpander}
pagination={{ position: "top", defaultPageSize: 25 }}
columns={columns.map((item) => ({ ...item }))}
rowKey="id"
dataSource={invoices}
onChange={handleTableChange}
expandable={{
expandedRowKeys: [selectedInvoice],
onExpand: (expanded, record) => {
handleOnRowClick(expanded ? record : null);
},
}}
rowSelection={{
onSelect: (record) => {
handleOnRowClick(record);
},
selectedRowKeys: [selectedInvoice],
type: "radio",
}}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleOnRowClick(record);
}, // click row
onDoubleClick: (event) => {}, // double click row
onContextMenu: (event) => {}, // right button click row
onMouseEnter: (event) => {}, // mouse enter row
onMouseLeave: (event) => {}, // mouse leave row
};
}}
/>
);
}

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

@@ -1,13 +1,21 @@
import {
EditFilled,
FileImageFilled,
PrinterFilled,
ShoppingFilled
} from "@ant-design/icons";
import { useQuery } from "@apollo/react-hooks";
import { Button, Icon, PageHeader, Tag } from "antd";
import { Button, PageHeader, Tag } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries";
import { setModalContext } from "../../redux/modals/modals.actions";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container";
//import JobDetailCardsHeaderComponent from "./job-detail-cards.header.component";
import ScheduleJobModalContainer from "../schedule-job-modal/schedule-job-modal.container";
import JobDetailCardsCustomerComponent from "./job-detail-cards.customer.component";
import JobDetailCardsDamageComponent from "./job-detail-cards.damage.component";
import JobDetailCardsDatesComponent from "./job-detail-cards.dates.component";
@@ -18,38 +26,46 @@ import JobDetailCardsPartsComponent from "./job-detail-cards.parts.component";
import "./job-detail-cards.styles.scss";
import JobDetailCardsTotalsComponent from "./job-detail-cards.totals.component";
const mapDispatchToProps = dispatch => ({
setInvoiceEnterContext: context =>
dispatch(setModalContext({ context: context, modal: "invoiceEnter" })),
setNoteUpsertContext: context =>
dispatch(setModalContext({ context: context, modal: "noteUpsert" }))
});
export default function JobDetailCards({ selectedJob }) {
export function JobDetailCards({
selectedJob,
setInvoiceEnterContext,
setNoteUpsertContext
}) {
const { loading, error, data, refetch } = useQuery(QUERY_JOB_CARD_DETAILS, {
fetchPolicy: "network-only",
variables: { id: selectedJob },
skip: !selectedJob
});
const [noteModalVisible, setNoteModalVisible] = useState(false);
const scheduleModalState = useState(false);
const { t } = useTranslation();
if (!selectedJob) {
return <div>{t("jobs.errors.nojobselected")}</div>;
}
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type='error' />;
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<div className='job-cards-container'>
<NoteUpsertModal
<div className="job-cards-container">
<NoteUpsertModal />
<ScheduleJobModalContainer
scheduleModalState={scheduleModalState}
jobId={data.jobs_by_pk.id}
visible={noteModalVisible}
changeVisibility={setNoteModalVisible}
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>
<span key="job-status">
{data.jobs_by_pk.status ? (
<Tag color="blue">{data.jobs_by_pk.status}</Tag>
) : null}
</span>
}
@@ -62,37 +78,64 @@ export default function JobDetailCards({ selectedJob }) {
? `${t("jobs.fields.ro_number")} ${data.jobs_by_pk.ro_number}`
: `${t("jobs.fields.est_number")} ${
data.jobs_by_pk.est_number
}`}{" "}
}`}
</Link>
)
}
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`}>
key="documents"
to={`/manage/jobs/${data.jobs_by_pk.id}?documents`}
>
<Button>
<Icon type='file-image' />
<FileImageFilled />
{t("jobs.actions.addDocuments")}
</Button>
</Link>,
<Button key='printing'>
<Icon type='printer' />
<Button key="printing">
<PrinterFilled />
{t("jobs.actions.printCenter")}
</Button>,
<Button
key='notes'
actiontype='addNote'
key="notes"
actiontype="addNote"
onClick={() => {
setNoteModalVisible(!noteModalVisible);
}}>
<Icon type='edit' />
setNoteUpsertContext({
actions: { refetch: refetch },
context: {
jobId: data.jobs_by_pk.id
}
});
}}
>
<EditFilled />
{t("jobs.actions.addNote")}
</Button>,
<Button key='postinvoices'>
<Icon type='shopping-cart' />
<Button
key="postinvoices"
onClick={() => {
setInvoiceEnterContext({
actions: { refetch: refetch },
context: {
job: data.jobs_by_pk
}
});
}}
>
<ShoppingFilled />
{t("jobs.actions.postInvoices")}
</Button>
]}>
]}
>
{
// loading ? (
// <LoadingSkeleton />
@@ -113,7 +156,7 @@ export default function JobDetailCards({ selectedJob }) {
// )
}
<section className='job-cards'>
<section className="job-cards">
<JobDetailCardsCustomerComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
@@ -158,3 +201,4 @@ export default function JobDetailCards({ selectedJob }) {
</div>
);
}
export default connect(null, mapDispatchToProps)(JobDetailCards);

View File

@@ -11,10 +11,14 @@ export default function JobDetailCardsCustomerComponent({ loading, data }) {
<CardTemplate
loading={loading}
title={t("jobs.labels.cards.customer")}
extraLink={data && data.owner ? `/manage/owners/${data.owner.id}` : null}>
extraLink={data && data.owner ? `/manage/owners/${data.owner.id}` : null}
>
{data ? (
<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 ||
@@ -31,14 +35,22 @@ export default function JobDetailCardsCustomerComponent({ loading, data }) {
)}
</div>
<div>{`${(data.owner && data.owner.preferred_contact) || ""}`}</div>
{data.vehicle ? (
<Link to={`/manage/vehicles/${data.vehicle.id}`}>
{`${data.vehicle.v_model_yr || ""} ${data.vehicle.v_make_desc ||
""} ${data.vehicle.v_model_desc || ""}`}
</Link>
) : (
<span>{t("jobs.errors.novehicle")}</span>
)}
<div>
{data.vehicle ? (
<Link to={`/manage/vehicles/${data.vehicleid}`}>
{`${data.v_model_yr || ""} ${data.v_make_desc ||
""} ${data.v_model_desc || ""}`}
a
</Link>
) : (
<span>
{`${data.v_model_yr || ""} ${data.v_make_desc ||
""} ${data.v_model_desc || ""}`}
b
</span>
)}
e
</div>
</span>
) : null}
</CardTemplate>

View File

@@ -1,18 +1,19 @@
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}
{area_of_damage ? (
<Car
dmg1={area_of_damage.impact1 || null}
dmg2={area_of_damage.impact2 || null}
/>
) : t("jobs.errors.nodamage")}
</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}

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