5
.ebextensions/00_cleanup.config
Normal file
5
.ebextensions/00_cleanup.config
Normal file
@@ -0,0 +1,5 @@
|
||||
commands:
|
||||
10_cleanup:
|
||||
command: |
|
||||
sudo rm -f /opt/elasticbeanstalk/hooks/configdeploy/post/*
|
||||
sudo rm -f /etc/nginx/conf.d/*
|
||||
13
.ebextensions/01_setup.config
Normal file
13
.ebextensions/01_setup.config
Normal 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: []
|
||||
105
.ebextensions/02_nginx.config
Normal file
105
.ebextensions/02_nginx.config
Normal 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;
|
||||
}
|
||||
}
|
||||
45
.ebextensions/03_container_commands.config
Normal file
45
.ebextensions/03_container_commands.config
Normal 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
|
||||
11
.ebextensions/04_configdeploy_post_hooks.config
Normal file
11
.ebextensions/04_configdeploy_post_hooks.config
Normal 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
|
||||
8
.ebextensions/05_cron.config
Normal file
8
.ebextensions/05_cron.config
Normal 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
|
||||
9
.elasticbeanstalk/config.yml
Normal file
9
.elasticbeanstalk/config.yml
Normal 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
|
||||
14
README.MD
14
README.MD
@@ -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!
|
||||
|
||||
@@ -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
|
||||
|
||||
92
_reference/AuditTriggerFunctions.sql
Normal file
92
_reference/AuditTriggerFunctions.sql
Normal file
@@ -0,0 +1,92 @@
|
||||
|
||||
|
||||
CREATE SCHEMA audit;
|
||||
|
||||
CREATE TABLE audit_trail (
|
||||
|
||||
id serial PRIMARY KEY,
|
||||
|
||||
tstamp timestamp DEFAULT now(),
|
||||
|
||||
schemaname text,
|
||||
|
||||
tabname text,
|
||||
|
||||
operation text,
|
||||
recordid uuid,
|
||||
|
||||
-- who text DEFAULT current_user,
|
||||
|
||||
new_val json,
|
||||
|
||||
old_val json,
|
||||
useremail text,
|
||||
bodyshopid uuid
|
||||
|
||||
);
|
||||
|
||||
-- More as an example than anything else, I wanted a function that would take two JSONB objects in PostgreSQL, and return how the left-hand side differs from the right-hand side. This means any key that is in the left but not in the right would be returned, along with any key whose value on the left is different from the right.
|
||||
|
||||
-- Here’s a quick example of how to do this in a single SELECT. In real life, you probably want more error checking, but it shows how nice the built-in primitives are:
|
||||
CREATE OR REPLACE FUNCTION json_diff(l JSONB, r JSONB) RETURNS JSONB AS
|
||||
$json_diff$
|
||||
SELECT jsonb_object_agg(a.key, a.value) FROM
|
||||
( SELECT key, value FROM jsonb_each(l) ) a LEFT OUTER JOIN
|
||||
( SELECT key, value FROM jsonb_each(r) ) b ON a.key = b.key
|
||||
WHERE a.value != b.value OR b.key IS NULL;
|
||||
$json_diff$
|
||||
LANGUAGE sql;
|
||||
|
||||
|
||||
CREATE OR REPLACE FUNCTION audit_trigger() RETURNS trigger AS $$
|
||||
|
||||
DECLARE
|
||||
shopid text ;
|
||||
email text;
|
||||
|
||||
BEGIN
|
||||
|
||||
select b.id, u.email INTO shopid, email from users u join associations a on u.email = a.useremail join bodyshops b on b.id = a.shopid where u.authid = current_setting('hasura.user', 't')::jsonb->>'x-hasura-user-id' and a.active = true;
|
||||
|
||||
IF TG_OP = 'INSERT'
|
||||
|
||||
THEN
|
||||
|
||||
INSERT INTO public.audit_trail (tabname, schemaname, operation, new_val, recordid, bodyshopid, useremail)
|
||||
|
||||
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(NEW), NEW.id, shopid, email);
|
||||
|
||||
RETURN NEW;
|
||||
|
||||
ELSIF TG_OP = 'UPDATE'
|
||||
|
||||
THEN
|
||||
|
||||
INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, new_val, recordid, bodyshopid, useremail)
|
||||
|
||||
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP,
|
||||
|
||||
json_diff(to_jsonb(OLD), to_jsonb(NEW)) , json_diff(to_jsonb(NEW), to_jsonb(OLD)), OLD.id, shopid, email);
|
||||
|
||||
RETURN NEW;
|
||||
|
||||
ELSIF TG_OP = 'DELETE'
|
||||
|
||||
THEN
|
||||
|
||||
INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, recordid, bodyshopid, useremail)
|
||||
|
||||
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(OLD), OLD.ID, shopid, email);
|
||||
|
||||
RETURN OLD;
|
||||
|
||||
END IF;
|
||||
|
||||
END;
|
||||
|
||||
$$ LANGUAGE 'plpgsql' SECURITY DEFINER;
|
||||
|
||||
|
||||
|
||||
CREATE TRIGGER audit_trigger_users AFTER INSERT OR UPDATE OR DELETE ON users
|
||||
FOR EACH ROW EXECUTE PROCEDURE audit_trigger();
|
||||
192
_reference/CiecaOpCodesReference.json
Normal file
192
_reference/CiecaOpCodesReference.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
787
client/src/assets/unfolded_car-orig.svg
Normal file
787
client/src/assets/unfolded_car-orig.svg
Normal 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 |
144
client/src/assets/unfolded_car_clean.svg
Normal file
144
client/src/assets/unfolded_car_clean.svg
Normal 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 |
150
client/src/components/_test/test.component.jsx
Normal file
150
client/src/components/_test/test.component.jsx
Normal 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>→<span> This is a full-featured editor demo. </span>Please explore! ←</span></p>
|
||||
<p style="text-align: center;"> </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;"> </p>
|
||||
<p> </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>`;
|
||||
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Alert component should render Alert component 1`] = `ShallowWrapper {}`;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {}`;
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import React, { useState } from "react";
|
||||
import { Table } from "antd";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component";
|
||||
|
||||
export default function AuditTrailListComponent({ loading, data }) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: {}
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
const columns = [
|
||||
{
|
||||
title: t("audit.fields.created"),
|
||||
dataIndex: " created",
|
||||
key: " created",
|
||||
width: "10%",
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.created}</DateTimeFormatter>
|
||||
),
|
||||
sorter: (a, b) => a.created - b.created,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "created" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("audit.fields.operation"),
|
||||
dataIndex: "operation",
|
||||
key: "operation",
|
||||
width: "10%",
|
||||
sorter: (a, b) => alphaSort(a.operation, b.operation),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "operation" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("audit.fields.values"),
|
||||
dataIndex: " old_val",
|
||||
key: " old_val",
|
||||
width: "10%",
|
||||
render: (text, record) => (
|
||||
<AuditTrailValuesComponent
|
||||
oldV={record.old_val}
|
||||
newV={record.new_val}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("audit.fields.useremail"),
|
||||
dataIndex: "useremail",
|
||||
key: "useremail",
|
||||
width: "10%",
|
||||
sorter: (a, b) => alphaSort(a.useremail, b.useremail),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order
|
||||
}
|
||||
];
|
||||
|
||||
const formItemLayout = {
|
||||
labelCol: {
|
||||
xs: { span: 12 },
|
||||
sm: { span: 5 }
|
||||
},
|
||||
wrapperCol: {
|
||||
xs: { span: 24 },
|
||||
sm: { span: 12 }
|
||||
}
|
||||
};
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
return (
|
||||
<Table
|
||||
{...formItemLayout}
|
||||
loading={loading}
|
||||
size="small"
|
||||
pagination={{ position: "top", defaultPageSize: 25 }}
|
||||
columns={columns.map(item => ({ ...item }))}
|
||||
rowKey="id"
|
||||
dataSource={data}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import AuditTrailListComponent from "./audit-trail-list.component";
|
||||
import { useQuery } from "@apollo/react-hooks";
|
||||
import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
|
||||
export default function AuditTrailListContainer({ recordId }) {
|
||||
const { loading, error, data } = useQuery(QUERY_AUDIT_TRAIL, {
|
||||
variables: { id: recordId },
|
||||
fetchPolicy: "network-only"
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
{error ? (
|
||||
<AlertComponent type="error" message={error.message} />
|
||||
) : (
|
||||
<AuditTrailListComponent
|
||||
loading={loading}
|
||||
data={data ? data.audit_trail : null}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
28
client/src/components/breadcrumbs/breadcrumbs.component.jsx
Normal file
28
client/src/components/breadcrumbs/breadcrumbs.component.jsx
Normal 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);
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ShrinkOutlined } from "@ant-design/icons";
|
||||
import { Badge } from "antd";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
openConversation,
|
||||
toggleChatVisible
|
||||
} from "../../redux/messaging/messaging.actions";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
toggleChatVisible: () => dispatch(toggleChatVisible()),
|
||||
openConversation: number => dispatch(openConversation(number))
|
||||
});
|
||||
|
||||
export function ChatConversationListComponent({
|
||||
toggleChatVisible,
|
||||
conversationList,
|
||||
openConversation
|
||||
}) {
|
||||
return (
|
||||
<div className='chat-overlay-open'>
|
||||
<ShrinkOutlined onClick={() => toggleChatVisible()} />
|
||||
{conversationList.map(item => (
|
||||
<Badge count={item.messages_aggregate.aggregate.count || 0}>
|
||||
<div
|
||||
key={item.id}
|
||||
style={{ cursor: "pointer", display: "block" }}
|
||||
onClick={() =>
|
||||
openConversation({ phone_num: item.phone_num, id: item.id })
|
||||
}>
|
||||
<div>
|
||||
<PhoneNumberFormatter>{item.phone_num}</PhoneNumberFormatter>
|
||||
</div>
|
||||
</div>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(null, mapDispatchToProps)(ChatConversationListComponent);
|
||||
@@ -0,0 +1,38 @@
|
||||
import { CloseCircleFilled } from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
closeConversation,
|
||||
sendMessage,
|
||||
toggleConversationVisible
|
||||
} from "../../redux/messaging/messaging.actions";
|
||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
toggleConversationVisible: conversationId =>
|
||||
dispatch(toggleConversationVisible(conversationId)),
|
||||
closeConversation: phone => dispatch(closeConversation(phone)),
|
||||
sendMessage: message => dispatch(sendMessage(message))
|
||||
});
|
||||
|
||||
function ChatConversationClosedComponent({
|
||||
conversation,
|
||||
toggleConversationVisible,
|
||||
closeConversation
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className='chat-conversation-closed'
|
||||
onClick={() => toggleConversationVisible(conversation.id)}>
|
||||
<PhoneFormatter>{conversation.phone_num}</PhoneFormatter>
|
||||
<CloseCircleFilled
|
||||
onClick={() => closeConversation(conversation.phone_num)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(ChatConversationClosedComponent);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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) ||
|
||||
[]
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { toggleConversationVisible } from "../../redux/messaging/messaging.actions";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import ChatMessageListComponent from "../chat-messages-list/chat-message-list.component";
|
||||
import ChatSendMessage from "../chat-send-message/chat-send-message.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import { ShrinkOutlined } from "@ant-design/icons";
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
toggleConversationVisible: conversation =>
|
||||
dispatch(toggleConversationVisible(conversation))
|
||||
});
|
||||
|
||||
export function ChatConversationOpenComponent({
|
||||
conversation,
|
||||
messages,
|
||||
subState,
|
||||
toggleConversationVisible
|
||||
}) {
|
||||
const [loading, error] = subState;
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <AlertComponent message={error.message} type='error' />;
|
||||
|
||||
return (
|
||||
<div className='chat-conversation-open'>
|
||||
<ShrinkOutlined
|
||||
onClick={() => toggleConversationVisible(conversation.id)}
|
||||
/>
|
||||
<ChatMessageListComponent messages={messages} />
|
||||
<ChatSendMessage conversation={conversation} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(null, mapDispatchToProps)(ChatConversationOpenComponent);
|
||||
32
client/src/components/chat-dock/chat-dock.container.jsx
Normal file
32
client/src/components/chat-dock/chat-dock.container.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Affix } from "antd";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectConversations } from "../../redux/messaging/messaging.selectors";
|
||||
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
|
||||
import ChatMessagesButtonContainer from "../chat-messages-button/chat-messages-button.container";
|
||||
import "./chat-dock.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
activeConversations: selectConversations
|
||||
});
|
||||
|
||||
export function ChatOverlayContainer({ activeConversations }) {
|
||||
return (
|
||||
<Affix offsetBottom={0}>
|
||||
<div className='chat-dock'>
|
||||
<ChatMessagesButtonContainer />
|
||||
{activeConversations
|
||||
? activeConversations.map(conversation => (
|
||||
<ChatConversationContainer
|
||||
conversation={conversation}
|
||||
key={conversation.id}
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
</Affix>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(ChatOverlayContainer);
|
||||
188
client/src/components/chat-dock/chat-dock.styles.scss
Normal file
188
client/src/components/chat-dock/chat-dock.styles.scss
Normal 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;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { MessageFilled } from "@ant-design/icons";
|
||||
import { Badge, Card } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
|
||||
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
|
||||
import ChatConversationListComponent from "../chat-conversation-list/chat-conversation-list.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
chatVisible: selectChatVisible
|
||||
});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
toggleChatVisible: () => dispatch(toggleChatVisible())
|
||||
});
|
||||
|
||||
export function ChatWindowComponent({
|
||||
chatVisible,
|
||||
toggleChatVisible,
|
||||
conversationList,
|
||||
unreadCount
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className='chat-conversation'>
|
||||
<Badge count={unreadCount}>
|
||||
<Card size='small'>
|
||||
{chatVisible ? (
|
||||
<ChatConversationListComponent
|
||||
conversationList={conversationList}
|
||||
/>
|
||||
) : (
|
||||
<div onClick={() => toggleChatVisible()}>
|
||||
<MessageFilled />
|
||||
<strong>{t("messaging.labels.messaging")}</strong>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ChatWindowComponent);
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useSubscription } from "@apollo/react-hooks";
|
||||
import React from "react";
|
||||
import { CONVERSATION_LIST_SUBSCRIPTION } from "../../graphql/conversations.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import ChatMessagesButtonComponent from "./chat-messages-button.component";
|
||||
|
||||
export default function ChatMessagesButtonContainer() {
|
||||
const { loading, error, data } = useSubscription(
|
||||
CONVERSATION_LIST_SUBSCRIPTION
|
||||
);
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <AlertComponent message={error.message} type='error' />;
|
||||
|
||||
return (
|
||||
<ChatMessagesButtonComponent
|
||||
conversationList={(data && data.conversations) || []}
|
||||
unreadCount={
|
||||
(data &&
|
||||
data.conversations.reduce((acc, val) => {
|
||||
return (acc = acc + val.messages_aggregate.aggregate.count);
|
||||
}, 0)) ||
|
||||
0
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { CheckCircleOutlined, CheckOutlined } from "@ant-design/icons";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
|
||||
export default function ChatMessageListComponent({ messages }) {
|
||||
const messagesEndRef = useRef(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
console.log("use");
|
||||
!!messagesEndRef.current &&
|
||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
useEffect(scrollToBottom, [messages]);
|
||||
const StatusRender = status => {
|
||||
switch (status) {
|
||||
case "sent":
|
||||
return <CheckOutlined style={{ margin: "2px", float: "right" }} />;
|
||||
case "delivered":
|
||||
return (
|
||||
<CheckCircleOutlined style={{ margin: "2px", float: "right" }} />
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='messages'>
|
||||
<ul>
|
||||
{messages.map(item => (
|
||||
<li
|
||||
key={item.id}
|
||||
className={`${item.isoutbound ? "replies" : "sent"}`}>
|
||||
<p>
|
||||
{item.text}
|
||||
{StatusRender(item.status)}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
<li ref={messagesEndRef} />
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -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);
|
||||
@@ -1,5 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export default function ChatWindowComponent() {
|
||||
return <div>Chat Windows and more</div>;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
119
client/src/components/contract-cars/contract-cars.component.jsx
Normal file
119
client/src/components/contract-cars/contract-cars.component.jsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { Input, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
|
||||
export default function ContractsCarsComponent({
|
||||
loading,
|
||||
data,
|
||||
selectedCar,
|
||||
handleSelect
|
||||
}) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" },
|
||||
search: ""
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("courtesycars.fields.fleetnumber"),
|
||||
dataIndex: "fleetnumber",
|
||||
key: "fleetnumber",
|
||||
sorter: (a, b) => alphaSort(a.fleetnumber, b.fleetnumber),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "fleetnumber" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.year"),
|
||||
dataIndex: "year",
|
||||
key: "year",
|
||||
sorter: (a, b) => alphaSort(a.year, b.year),
|
||||
sortOrder: state.sortedInfo.columnKey === "year" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.make"),
|
||||
dataIndex: "make",
|
||||
key: "make",
|
||||
sorter: (a, b) => alphaSort(a.make, b.make),
|
||||
sortOrder: state.sortedInfo.columnKey === "make" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.model"),
|
||||
dataIndex: "model",
|
||||
key: "model",
|
||||
sorter: (a, b) => alphaSort(a.model, b.model),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "model" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.plate"),
|
||||
dataIndex: "plate",
|
||||
key: "plate",
|
||||
sorter: (a, b) => alphaSort(a.plate, b.plate),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "plate" && state.sortedInfo.order
|
||||
}
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
const filteredData =
|
||||
state.search === ""
|
||||
? data
|
||||
: data.filter(
|
||||
cc =>
|
||||
(cc.fleetnumber || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(cc.status || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(cc.year || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(cc.make || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(cc.model || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(cc.plate || "").toLowerCase().includes(state.search.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<Table
|
||||
loading={loading}
|
||||
title={() => (
|
||||
<Input.Search
|
||||
placeholder={t("general.labels.search")}
|
||||
value={state.search}
|
||||
onChange={e => setState({ ...state, search: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
size="small"
|
||||
pagination={{ position: "top" }}
|
||||
columns={columns.map(item => ({ ...item }))}
|
||||
rowKey="id"
|
||||
dataSource={filteredData}
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelect: handleSelect,
|
||||
type: "radio",
|
||||
selectedRowKeys: [selectedCar]
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useQuery } from "@apollo/react-hooks";
|
||||
import React from "react";
|
||||
import { QUERY_AVAILABLE_CC } from "../../graphql/courtesy-car.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import ContractCarsComponent from "./contract-cars.component";
|
||||
|
||||
|
||||
|
||||
export default function ContractCarsContainer({ selectedCarState, bodyshop }) {
|
||||
const { loading, error, data } = useQuery(QUERY_AVAILABLE_CC);
|
||||
|
||||
const [selectedCar, setSelectedCar] = selectedCarState;
|
||||
|
||||
const handleSelect = record => {
|
||||
setSelectedCar(record.id);
|
||||
};
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
return (
|
||||
<div>
|
||||
<ContractCarsComponent
|
||||
handleSelect={handleSelect}
|
||||
selectedCar={selectedCar}
|
||||
loading={loading}
|
||||
data={data ? data.courtesycars : []}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Descriptions, Card } from "antd";
|
||||
import { Link } from "react-router-dom";
|
||||
export default function ContractCourtesyCarBlock({ courtesyCar }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Link to={`/manage/courtesycars/${courtesyCar && courtesyCar.id}`}>
|
||||
<Card title={t("courtesycars.labels.courtesycar")}>
|
||||
<Descriptions size="small" column={1}>
|
||||
<Descriptions.Item label={t("courtesycars.fields.fleetnumber")}>
|
||||
{(courtesyCar && courtesyCar.fleetnumber) || ""}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("courtesycars.fields.plate")}>
|
||||
{(courtesyCar && courtesyCar.plate) || ""}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("courtesycars.labels.vehicle")}>
|
||||
{`${(courtesyCar && courtesyCar.year) || ""} ${(courtesyCar &&
|
||||
courtesyCar.make) ||
|
||||
""} ${(courtesyCar && courtesyCar.model) || ""}`}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
264
client/src/components/contract-form/contract-form.component.jsx
Normal file
264
client/src/components/contract-form/contract-form.component.jsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, Input, DatePicker, InputNumber, Button } from "antd";
|
||||
import aamva from "aamva";
|
||||
import InputPhone from "../form-items-formatted/phone-form-item.component";
|
||||
import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
|
||||
|
||||
export default function ContractFormComponent() {
|
||||
const [state, setState] = useState("");
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<div style={{ background: "#f00" }}>
|
||||
TEST AREA
|
||||
<Input value={state} onChange={e => setState(e.target.value)} />
|
||||
<Button
|
||||
onClick={() => {
|
||||
console.log("state", state);
|
||||
//let data = state;
|
||||
|
||||
var data =
|
||||
"%FLDELRAY BEACH^DOE$JOHN$^4818 S FEDERAL BLVD^ ? ;6360100462172082009=2101198299090=? #! 33435 I 1600 ECCECC00000?";
|
||||
data = data.replace(/\n/, "");
|
||||
// replace spaces with regular space
|
||||
data = data.replace(/\s/g, " ");
|
||||
var track = data.match(/(.*?\?)(.*?\?)(.*?\?)/);
|
||||
console.log("data", data);
|
||||
console.log("track", track);
|
||||
const a = aamva.stripe(data);
|
||||
console.log(JSON.stringify(a));
|
||||
}}
|
||||
>
|
||||
Decode
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label={t("contracts.fields.status")}
|
||||
name="status"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<ContractStatusSelector />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.start")}
|
||||
name="start"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.scheduledreturn")}
|
||||
name="scheduledreturn"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.actualreturn")} name="actualreturn">
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.kmstart")}
|
||||
name="kmstart"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.kmend")} name="kmend">
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_dlnumber")}
|
||||
name="driver_dlnumber"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_dlexpiry")}
|
||||
name="driver_dlexpiry"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_dlst")}
|
||||
name="driver_dlst"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_fn")}
|
||||
name="driver_fn"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_ln")}
|
||||
name="driver_ln"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_addr1")}
|
||||
name="driver_addr1"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.driver_addr2")} name="driver_addr2">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_city")}
|
||||
name="driver_city"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_state")}
|
||||
name="driver_state"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_zip")}
|
||||
name="driver_zip"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_ph1")}
|
||||
name="driver_ph1"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputPhone />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_dob")}
|
||||
name="driver_dob"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.cc_num")}
|
||||
name="cc_num"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.cc_expiry")}
|
||||
name="cc_expiry"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.cc_cardholder")}
|
||||
name="cc_cardholder"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Descriptions, Card } from "antd";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export default function ContractJobBlock({ job }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Link to={`/manage/jobs/${job && job.id}`}>
|
||||
<Card title={t("jobs.labels.job")}>
|
||||
<Descriptions size="small" column={1}>
|
||||
<Descriptions.Item label={t("jobs.fields.ro_number")}>
|
||||
{(job && job.ro_number) || ""}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("jobs.fields.vehicle")}>
|
||||
{`${(job && job.v_model_yr) || ""} ${(job && job.v_make_desc) ||
|
||||
""} ${(job && job.v_model_desc) || ""}`}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("jobs.fields.owner")}>
|
||||
{`${(job && job.ownr_fn) || ""} ${(job && job.ownr_ln) ||
|
||||
""} ${(job && job.ownr_co_nm) || ""}`}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
187
client/src/components/contract-jobs/contract-jobs.component.jsx
Normal file
187
client/src/components/contract-jobs/contract-jobs.component.jsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import { Table, Input } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
|
||||
export default function ContractsJobsComponent({
|
||||
loading,
|
||||
data,
|
||||
selectedJob,
|
||||
handleSelect
|
||||
}) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" },
|
||||
search: ""
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
width: "8%",
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
a.ro_number ? a.ro_number : "EST-" + a.est_number,
|
||||
b.ro_number ? b.ro_number : "EST-" + b.est_number
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
|
||||
render: (text, record) => (
|
||||
<span>
|
||||
{record.ro_number ? record.ro_number : "EST-" + record.est_number}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
width: "25%",
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.owner ? (
|
||||
<span>
|
||||
{record.ownr_fn} {record.ownr_ln}
|
||||
</span>
|
||||
) : (
|
||||
<span>{`${record.ownr_fn} ${record.ownr_ln}`}</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
width: "10%",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.status || t("general.labels.na");
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
width: "15%",
|
||||
ellipsis: true,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<span>
|
||||
{`${record.v_model_yr || ""} ${record.v_make_desc ||
|
||||
""} ${record.v_model_desc || ""}`}
|
||||
</span>
|
||||
) : (
|
||||
t("jobs.errors.novehicle")
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("vehicles.fields.plate_no"),
|
||||
dataIndex: "plate_no",
|
||||
key: "plate_no",
|
||||
width: "8%",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.plate_no ? (
|
||||
<span>{record.plate_no}</span>
|
||||
) : (
|
||||
t("general.labels.unknown")
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.clm_no"),
|
||||
dataIndex: "clm_no",
|
||||
key: "clm_no",
|
||||
width: "12%",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.clm_no ? (
|
||||
<span>{record.clm_no}</span>
|
||||
) : (
|
||||
t("general.labels.unknown")
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
const filteredData =
|
||||
state.search === ""
|
||||
? data
|
||||
: data.filter(
|
||||
j =>
|
||||
(j.est_number || "")
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(j.ro_number || "")
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(j.ownr_fn || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(j.ownr_ln || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(j.clm_no || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(j.v_make_desc || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(j.v_model_desc || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(j.plate_no || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase())
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<Table
|
||||
loading={loading}
|
||||
title={() => (
|
||||
<Input.Search
|
||||
placeholder={t("general.labels.search")}
|
||||
value={state.search}
|
||||
onChange={e => setState({ ...state, search: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
size="small"
|
||||
pagination={{ position: "top" }}
|
||||
columns={columns.map(item => ({ ...item }))}
|
||||
rowKey="id"
|
||||
dataSource={filteredData}
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelect: handleSelect,
|
||||
type: "radio",
|
||||
selectedRowKeys: [selectedJob]
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useQuery } from "@apollo/react-hooks";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import ContractJobsComponent from "./contract-jobs.component";
|
||||
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
export function ContractJobsContainer({ selectedJobState, bodyshop }) {
|
||||
const { loading, error, data } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
|
||||
variables: {
|
||||
statuses: bodyshop.md_ro_statuses.open_statuses || ["Open"]
|
||||
}
|
||||
});
|
||||
const [selectedJob, setSelectedJob] = selectedJobState;
|
||||
|
||||
const handleSelect = record => {
|
||||
setSelectedJob(record.id);
|
||||
};
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
return (
|
||||
<div>
|
||||
<ContractJobsComponent
|
||||
handleSelect={handleSelect}
|
||||
selectedJob={selectedJob}
|
||||
loading={loading}
|
||||
data={data ? data.jobs : []}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, null)(ContractJobsContainer);
|
||||
@@ -0,0 +1,35 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Select } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const { Option } = Select;
|
||||
|
||||
const ContractStatusComponent = ({
|
||||
value = "contracts.status.new",
|
||||
onChange
|
||||
}) => {
|
||||
const [option, setOption] = useState(value);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (onChange) {
|
||||
onChange(option);
|
||||
}
|
||||
}, [option, onChange]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={option}
|
||||
style={{
|
||||
width: 100
|
||||
}}
|
||||
onChange={setOption}
|
||||
>
|
||||
<Option value="contracts.status.new">{t("contracts.status.new")}</Option>
|
||||
<Option value="contracts.status.out">{t("contracts.status.out")}</Option>
|
||||
<Option value="contracts.status.returned">
|
||||
{t("contracts.status.returned")}
|
||||
</Option>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
export default ContractStatusComponent;
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
|
||||
export default function ContractsList({ loading, contracts }) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" }
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("contracts.fields.agreementnumber"),
|
||||
dataIndex: "agreementnumber",
|
||||
key: "agreementnumber",
|
||||
sorter: (a, b) => a.agreementnumber - b.agreementnumber,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "agreementnumber" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/courtesycars/contracts/${record.id}`}>
|
||||
{record.agreementnumber || ""}
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "job.ro_number",
|
||||
key: "job.ro_number",
|
||||
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "job.ro_number" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/jobs/${record.job.id}`}>
|
||||
{record.job.ro_number || ""}
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.driver"),
|
||||
dataIndex: "driver_ln",
|
||||
key: "driver_ln",
|
||||
sorter: (a, b) => alphaSort(a.driver_ln, b.driver_ln),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "driver_ln" && state.sortedInfo.order,
|
||||
render: (text, record) =>
|
||||
`${record.driver_fn || ""} ${record.driver_ln || ""}`
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => t(record.status)
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.start"),
|
||||
dataIndex: "start",
|
||||
key: "start",
|
||||
sorter: (a, b) => alphaSort(a.start, b.start),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
|
||||
render: (text, record) => <DateFormatter>{record.start}</DateFormatter>
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.scheduledreturn"),
|
||||
dataIndex: "scheduledreturn",
|
||||
key: "scheduledreturn",
|
||||
sorter: (a, b) => alphaSort(a.scheduledreturn, b.scheduledreturn),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "scheduledreturn" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<DateFormatter>{record.scheduledreturn}</DateFormatter>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
return (
|
||||
<Table
|
||||
loading={loading}
|
||||
size="small"
|
||||
pagination={{ position: "top" }}
|
||||
columns={columns.map(item => ({ ...item }))}
|
||||
rowKey="id"
|
||||
dataSource={contracts}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
|
||||
export default function CourtesyCarContractListComponent({ contracts }) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" }
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("contracts.fields.agreementnumber"),
|
||||
dataIndex: "agreementnumber",
|
||||
key: "agreementnumber",
|
||||
sorter: (a, b) => a.agreementnumber - b.agreementnumber,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "agreementnumber" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/courtesycars/contracts/${record.id}`}>
|
||||
{record.agreementnumber || ""}
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "job.ro_number",
|
||||
key: "job.ro_number",
|
||||
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "job.ro_number" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/jobs/${record.job.id}`}>
|
||||
{record.job.ro_number || ""}
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.driver"),
|
||||
dataIndex: "driver_ln",
|
||||
key: "driver_ln",
|
||||
sorter: (a, b) => alphaSort(a.driver_ln, b.driver_ln),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "driver_ln" && state.sortedInfo.order,
|
||||
render: (text, record) =>
|
||||
`${record.driver_fn || ""} ${record.driver_ln || ""}`
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => t(record.status)
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.start"),
|
||||
dataIndex: "start",
|
||||
key: "start",
|
||||
sorter: (a, b) => alphaSort(a.start, b.start),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
|
||||
render: (text, record) => <DateFormatter>{record.start}</DateFormatter>
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.scheduledreturn"),
|
||||
dataIndex: "scheduledreturn",
|
||||
key: "scheduledreturn",
|
||||
sorter: (a, b) => a.scheduledreturn - b.scheduledreturn,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "scheduledreturn" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<DateFormatter>{record.scheduledreturn}</DateFormatter>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
return (
|
||||
<Table
|
||||
size="small"
|
||||
pagination={{ position: "top" }}
|
||||
columns={columns.map(item => ({ ...item }))}
|
||||
rowKey="id"
|
||||
dataSource={contracts}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import React from "react";
|
||||
import { Form, Input, InputNumber, DatePicker, Button } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component";
|
||||
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
|
||||
|
||||
export default function CourtesyCarCreateFormComponent() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.make")}
|
||||
name="make"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.model")}
|
||||
name="model"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.year")}
|
||||
name="year"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.plate")}
|
||||
name="plate"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.color")}
|
||||
name="color"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.vin")}
|
||||
name="vin"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.fleetnumber")}
|
||||
name="fleetnumber"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.purchasedate")}
|
||||
name="purchasedate"
|
||||
>
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.servicestartdate")}
|
||||
name="servicestartdate"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.serviceenddate")}
|
||||
name="serviceenddate"
|
||||
>
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.leaseenddate")}
|
||||
name="leaseenddate"
|
||||
>
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.status")}
|
||||
name="status"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CourtesyCarStatus />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.nextservicekm")}
|
||||
name="nextservicekm"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.nextservicedate")}
|
||||
name="nextservicedate"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("courtesycars.fields.damage")} name="damage">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("courtesycars.fields.notes")} name="notes">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.fuel")}
|
||||
name="fuel"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CourtesyCarFuelSlider />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.registrationexpires")}
|
||||
name="registrationexpires"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.insuranceexpires")}
|
||||
name="insuranceexpires"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("courtesycars.fields.dailycost")} name="dailycost">
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Slider } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const CourtesyCarFuelComponent = ({ value = 100, onChange }) => {
|
||||
const [option, setOption] = useState(value);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (onChange) {
|
||||
onChange(option);
|
||||
}
|
||||
}, [option, onChange]);
|
||||
|
||||
const marks = {
|
||||
0: {
|
||||
style: {
|
||||
color: "#f50"
|
||||
},
|
||||
label: t("courtesycars.labels.fuel.empty")
|
||||
},
|
||||
13: t("courtesycars.labels.fuel.18"),
|
||||
25: t("courtesycars.labels.fuel.14"),
|
||||
38: t("courtesycars.labels.fuel.38"),
|
||||
50: t("courtesycars.labels.fuel.12"),
|
||||
63: t("courtesycars.labels.fuel.58"),
|
||||
75: t("courtesycars.labels.fuel.34"),
|
||||
88: t("courtesycars.labels.fuel.78"),
|
||||
100: {
|
||||
style: {
|
||||
color: "#008000"
|
||||
},
|
||||
label: <strong>{t("courtesycars.labels.fuel.full")}</strong>
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Slider
|
||||
marks={marks}
|
||||
defaultValue={value}
|
||||
onChange={setOption}
|
||||
step={null}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default CourtesyCarFuelComponent;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Form, DatePicker, InputNumber } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
|
||||
|
||||
export default function CourtesyCarReturnModalComponent() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.actualreturn")}
|
||||
name="actualreturn"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.kmend")}
|
||||
name="kmend"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.fuel")}
|
||||
name={["courtesycar", "data", "fuel"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CourtesyCarFuelSlider />
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,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);
|
||||
@@ -0,0 +1,39 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Select } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const { Option } = Select;
|
||||
|
||||
const CourtesyCarStatusComponent = ({
|
||||
value = "courtesycars.status.in",
|
||||
onChange
|
||||
}) => {
|
||||
const [option, setOption] = useState(value);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (onChange) {
|
||||
onChange(option);
|
||||
}
|
||||
}, [option, onChange]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={option}
|
||||
style={{
|
||||
width: 100
|
||||
}}
|
||||
onChange={setOption}
|
||||
>
|
||||
<Option value="courtesycars.status.in">
|
||||
{t("courtesycars.status.in")}
|
||||
</Option>
|
||||
<Option value="courtesycars.status.inservice">
|
||||
{t("courtesycars.status.inservice")}
|
||||
</Option>
|
||||
<Option value="courtesycars.status.out">
|
||||
{t("courtesycars.status.out")}
|
||||
</Option>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
export default CourtesyCarStatusComponent;
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
|
||||
export default function CourtesyCarsList({ loading, courtesycars }) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" }
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("courtesycars.fields.fleetnumber"),
|
||||
dataIndex: "fleetnumber",
|
||||
key: "fleetnumber",
|
||||
sorter: (a, b) => alphaSort(a.fleetnumber, b.fleetnumber),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "fleetnumber" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.vin"),
|
||||
dataIndex: "vin",
|
||||
key: "vin",
|
||||
sorter: (a, b) => alphaSort(a.vin, b.vin),
|
||||
sortOrder: state.sortedInfo.columnKey === "vin" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/courtesycars/${record.id}`}>{record.vin}</Link>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => t(record.status)
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.year"),
|
||||
dataIndex: "year",
|
||||
key: "year",
|
||||
sorter: (a, b) => alphaSort(a.year, b.year),
|
||||
sortOrder: state.sortedInfo.columnKey === "year" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.make"),
|
||||
dataIndex: "make",
|
||||
key: "make",
|
||||
sorter: (a, b) => alphaSort(a.make, b.make),
|
||||
sortOrder: state.sortedInfo.columnKey === "make" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.model"),
|
||||
dataIndex: "model",
|
||||
key: "model",
|
||||
sorter: (a, b) => alphaSort(a.model, b.model),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "model" && state.sortedInfo.order
|
||||
}
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
return (
|
||||
<Table
|
||||
loading={loading}
|
||||
size="small"
|
||||
pagination={{ position: "top" }}
|
||||
columns={columns.map(item => ({ ...item }))}
|
||||
rowKey="id"
|
||||
dataSource={courtesycars}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Card } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { Responsive, WidthProvider } from "react-grid-layout";
|
||||
import styled from "styled-components";
|
||||
//Combination of the following:
|
||||
// /node_modules/react-grid-layout/css/styles.css
|
||||
// /node_modules/react-resizable/css/styles.css
|
||||
import "./dashboard-grid.styles.css";
|
||||
|
||||
const Sdiv = styled.div`
|
||||
position: absolute;
|
||||
height: 80%;
|
||||
width: 80%;
|
||||
top: 10%;
|
||||
left: 10%;
|
||||
// background-color: #ffcc00;
|
||||
`;
|
||||
|
||||
const ResponsiveReactGridLayout = WidthProvider(Responsive);
|
||||
|
||||
export default function DashboardGridComponent() {
|
||||
const [state, setState] = useState({
|
||||
layout: [
|
||||
{ i: "1", x: 0, y: 0, w: 2, h: 2 },
|
||||
{ i: "2", x: 2, y: 0, w: 2, h: 2 },
|
||||
{ i: "3", x: 4, y: 0, w: 2, h: 2 }
|
||||
]
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
className: "layout",
|
||||
breakpoints: { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }
|
||||
// cols: { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 },
|
||||
// rowHeight: 100
|
||||
};
|
||||
|
||||
// We're using the cols coming back from this to calculate where to add new items.
|
||||
const onBreakpointChange = (breakpoint, cols) => {
|
||||
console.log("breakpoint, cols", breakpoint, cols);
|
||||
// setState({ ...state, breakpoint: breakpoint, cols: cols });
|
||||
};
|
||||
if (true) return null;
|
||||
return (
|
||||
<Sdiv>
|
||||
The Grid.
|
||||
<ResponsiveReactGridLayout
|
||||
{...defaultProps}
|
||||
onBreakpointChange={onBreakpointChange}
|
||||
width='100%'
|
||||
onLayoutChange={layout => {
|
||||
console.log("layout", layout);
|
||||
setState({ ...state, layout });
|
||||
}}>
|
||||
{state.layout.map((item, index) => {
|
||||
return (
|
||||
<Card style={{ width: "100px" }} key={item.i} data-grid={item}>
|
||||
A Card {index}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</ResponsiveReactGridLayout>
|
||||
</Sdiv>
|
||||
);
|
||||
}
|
||||
126
client/src/components/dashboard-grid/dashboard-grid.styles.css
Normal file
126
client/src/components/dashboard-grid/dashboard-grid.styles.css
Normal file
@@ -0,0 +1,126 @@
|
||||
.react-resizable {
|
||||
position: relative;
|
||||
}
|
||||
.react-resizable-handle {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-repeat: no-repeat;
|
||||
background-origin: content-box;
|
||||
box-sizing: border-box;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iNnB4Ij48ZyBvcGFjaXR5PSIwLjMwMiI+PHBhdGggZD0iTSA2IDYgTCAwIDYgTCAwIDQuMiBMIDQgNC4yIEwgNC4yIDQuMiBMIDQuMiAwIEwgNiAwIEwgNiA2IEwgNiA2IFoiIGZpbGw9IiMwMDAwMDAiLz48L2c+PC9zdmc+");
|
||||
background-position: bottom right;
|
||||
padding: 0 3px 3px 0;
|
||||
}
|
||||
.react-resizable-handle-sw {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
cursor: sw-resize;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.react-resizable-handle-se {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
cursor: se-resize;
|
||||
}
|
||||
.react-resizable-handle-nw {
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: nw-resize;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.react-resizable-handle-ne {
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: ne-resize;
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
.react-resizable-handle-w,
|
||||
.react-resizable-handle-e {
|
||||
top: 50%;
|
||||
margin-top: -10px;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
.react-resizable-handle-w {
|
||||
left: 0;
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
.react-resizable-handle-e {
|
||||
right: 0;
|
||||
transform: rotate(315deg);
|
||||
}
|
||||
.react-resizable-handle-n,
|
||||
.react-resizable-handle-s {
|
||||
left: 50%;
|
||||
margin-left: -10px;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
.react-resizable-handle-n {
|
||||
top: 0;
|
||||
transform: rotate(225deg);
|
||||
}
|
||||
.react-resizable-handle-s {
|
||||
bottom: 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.react-grid-layout {
|
||||
position: relative;
|
||||
transition: height 200ms ease;
|
||||
}
|
||||
.react-grid-item {
|
||||
transition: all 200ms ease;
|
||||
transition-property: left, top;
|
||||
}
|
||||
.react-grid-item.cssTransforms {
|
||||
transition-property: transform;
|
||||
}
|
||||
.react-grid-item.resizing {
|
||||
z-index: 1;
|
||||
will-change: width, height;
|
||||
}
|
||||
|
||||
.react-grid-item.react-draggable-dragging {
|
||||
transition: none;
|
||||
z-index: 3;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.react-grid-item.dropping {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.react-grid-item.react-grid-placeholder {
|
||||
background: red;
|
||||
opacity: 0.2;
|
||||
transition-duration: 100ms;
|
||||
z-index: 2;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.react-grid-item > .react-resizable-handle {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
cursor: se-resize;
|
||||
}
|
||||
|
||||
.react-grid-item > .react-resizable-handle::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 3px;
|
||||
bottom: 3px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-right: 2px solid rgba(0, 0, 0, 0.4);
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.react-resizable-hide > .react-resizable-handle {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
145
client/src/components/email-overlay/email-overlay.container.jsx
Normal file
145
client/src/components/email-overlay/email-overlay.container.jsx
Normal 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);
|
||||
0
client/src/components/email-overlay/email-setup.md
Normal file
0
client/src/components/email-overlay/email-setup.md
Normal file
@@ -0,0 +1,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;
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { InputNumber } from "antd";
|
||||
import React, { forwardRef } from "react";
|
||||
function FormItemCurrency(props, ref) {
|
||||
return (
|
||||
<InputNumber
|
||||
{...props}
|
||||
//formatter={value => `$ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
|
||||
// parser={value => value.replace(/\$\s?|(,*)/g, "")}
|
||||
precision={2}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(FormItemCurrency);
|
||||
@@ -1,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 />
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
// );
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.header{
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user