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
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
React App:
|
React App:
|
||||||
React Hooks are used for Authentication ONLY to ensure the correct web token is passed.
|
|
||||||
|
Yarn Dependency Management:
|
||||||
|
To force upgrades for some packages: yarn upgrade-interactive --latest
|
||||||
|
|
||||||
GraphQL API:
|
GraphQL API:
|
||||||
Hasura is hosted on another dyno. Several environmental variables are required, including disabling the console.
|
Hasura is hosted on another dyno. Several environmental variables are required, including disabling the console.
|
||||||
@@ -9,4 +11,4 @@ To Start Hasura CLI:
|
|||||||
npx hasura console --admin-secret Dev-BodyShopAppBySnaptSoftware!
|
npx hasura console --admin-secret Dev-BodyShopAppBySnaptSoftware!
|
||||||
|
|
||||||
Migrating to Staging:
|
Migrating to Staging:
|
||||||
npx hasura migrate apply --up 10 --endpoint https://bodyshop-staging-db.herokuapp.com/ --admin-secret Staging-BodyShopAppBySnaptSoftware!
|
npx hasura migrate apply --up 10 --endpoint https://bodyshop-staging-db.herokuapp.com/ --admin-secret Staging-BodyShopAppBySnaptSoftware!
|
||||||
|
|||||||
@@ -1,5 +1,51 @@
|
|||||||
**Required items**
|
**Required items**
|
||||||
|
|
||||||
|
|
||||||
-Bodyshop Record
|
-Bodyshop Record
|
||||||
|
..\*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
|
-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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,3 +9,6 @@ Bucket=
|
|||||||
__React Based__
|
__React Based__
|
||||||
REACT_APP_GRAPHQL_ENDPOINT
|
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,
|
"private": true,
|
||||||
"proxy": "https://localhost:5000",
|
"proxy": "https://localhost:5000",
|
||||||
"dependencies": {
|
"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-boost": "^0.4.4",
|
||||||
"apollo-link-context": "^1.0.19",
|
"apollo-link-context": "^1.0.19",
|
||||||
"apollo-link-error": "^1.1.12",
|
"apollo-link-error": "^1.1.12",
|
||||||
"apollo-link-logger": "^1.2.3",
|
"apollo-link-logger": "^1.2.3",
|
||||||
|
"apollo-link-retry": "^2.2.15",
|
||||||
"apollo-link-ws": "^1.0.19",
|
"apollo-link-ws": "^1.0.19",
|
||||||
"axios": "^0.19.1",
|
"axios": "^0.19.2",
|
||||||
"chart.js": "^2.9.3",
|
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"firebase": "^7.5.0",
|
"firebase": "^7.13.1",
|
||||||
"graphql": "^14.5.8",
|
"graphql": "^14.6.0",
|
||||||
"i18next": "^19.0.2",
|
"i18next": "^19.3.4",
|
||||||
"node-sass": "^4.13.0",
|
"i18next-browser-languagedetector": "^4.1.1",
|
||||||
"react": "^16.12.0",
|
"node-sass": "^4.13.1",
|
||||||
|
"query-string": "^6.11.1",
|
||||||
|
"react": "^16.13.1",
|
||||||
"react-apollo": "^3.1.3",
|
"react-apollo": "^3.1.3",
|
||||||
"react-chartjs-2": "^2.8.0",
|
"react-barcode": "^1.4.0",
|
||||||
"react-dom": "^16.12.0",
|
"react-big-calendar": "^0.24.1",
|
||||||
"react-i18next": "^11.2.7",
|
"react-dom": "^16.13.1",
|
||||||
"react-icons": "^3.8.0",
|
"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-image-file-resizer": "^0.2.1",
|
||||||
"react-moment": "^0.9.7",
|
"react-moment": "^0.9.7",
|
||||||
"react-number-format": "^4.3.1",
|
"react-number-format": "^4.4.1",
|
||||||
|
"react-redux": "^7.2.0",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-scripts": "3.2.0",
|
"react-scripts": "3.4.1",
|
||||||
"react-trello": "^2.2.3",
|
"redux": "^4.0.5",
|
||||||
"styled-components": "^4.4.1",
|
"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"
|
"subscriptions-transport-ws": "^0.9.16"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
@@ -56,6 +70,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@apollo/react-testing": "^3.1.3",
|
"@apollo/react-testing": "^3.1.3",
|
||||||
"enzyme": "^3.11.0",
|
"enzyme": "^3.11.0",
|
||||||
"enzyme-adapter-react-16": "^1.15.2"
|
"enzyme-adapter-react-16": "^1.15.2",
|
||||||
|
"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",
|
"name": "Bodyshop Management System",
|
||||||
|
"description": "The ultimate bodyshop management system",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
@@ -20,6 +21,6 @@
|
|||||||
],
|
],
|
||||||
"start_url": ".",
|
"start_url": ".",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"theme_color": "#002366",
|
"theme_color": "#fff",
|
||||||
"background_color": "#000000"
|
"background_color": "#fff"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,22 @@
|
|||||||
import React, { Component } from "react";
|
import { ApolloProvider } from "@apollo/react-common";
|
||||||
|
import { ApolloLink } from "apollo-boost";
|
||||||
import App from "./App";
|
import { InMemoryCache } from "apollo-cache-inmemory";
|
||||||
import Spin from "../components/loading-spinner/loading-spinner.component";
|
|
||||||
|
|
||||||
import ApolloClient from "apollo-client";
|
import ApolloClient from "apollo-client";
|
||||||
import { split } from "apollo-link";
|
import { split } from "apollo-link";
|
||||||
|
import { setContext } from "apollo-link-context";
|
||||||
import { HttpLink } from "apollo-link-http";
|
import { HttpLink } from "apollo-link-http";
|
||||||
|
import apolloLogger from "apollo-link-logger";
|
||||||
|
import { RetryLink } from "apollo-link-retry";
|
||||||
import { WebSocketLink } from "apollo-link-ws";
|
import { WebSocketLink } from "apollo-link-ws";
|
||||||
import { getMainDefinition } from "apollo-utilities";
|
import { getMainDefinition } from "apollo-utilities";
|
||||||
import { InMemoryCache } from "apollo-cache-inmemory";
|
import React, { Component } from "react";
|
||||||
import { setContext } from "apollo-link-context";
|
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
||||||
import { resolvers, typeDefs } from "../graphql/resolvers";
|
import { auth } from "../firebase/firebase.utils";
|
||||||
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 errorLink from "../graphql/apollo-error-handling";
|
import errorLink from "../graphql/apollo-error-handling";
|
||||||
|
import App from "./App";
|
||||||
class AppContainer extends Component {
|
export default class AppContainer extends Component {
|
||||||
state = {
|
constructor() {
|
||||||
client: null,
|
super();
|
||||||
loaded: false
|
|
||||||
};
|
|
||||||
async componentDidMount() {
|
|
||||||
const httpLink = new HttpLink({
|
const httpLink = new HttpLink({
|
||||||
uri: process.env.REACT_APP_GRAPHQL_ENDPOINT
|
uri: process.env.REACT_APP_GRAPHQL_ENDPOINT
|
||||||
});
|
});
|
||||||
@@ -34,8 +26,9 @@ class AppContainer extends Component {
|
|||||||
options: {
|
options: {
|
||||||
lazy: true,
|
lazy: true,
|
||||||
reconnect: true,
|
reconnect: true,
|
||||||
connectionParams: () => {
|
connectionParams: async () => {
|
||||||
const token = localStorage.getItem("token");
|
//const token = localStorage.getItem("token");
|
||||||
|
const token = await auth.currentUser.getIdToken(true);
|
||||||
if (token) {
|
if (token) {
|
||||||
return {
|
return {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -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(
|
const link = split(
|
||||||
// split based on operation type
|
// split based on operation type
|
||||||
@@ -69,22 +69,29 @@ class AppContainer extends Component {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const authLink = setContext((_, { headers }) => {
|
const authLink = setContext((_, { headers }) => {
|
||||||
// get the authentication token from local storage if it exists
|
return auth.currentUser.getIdToken().then(token => {
|
||||||
const token = localStorage.getItem("token");
|
if (token) {
|
||||||
// return the headers to the context so httpLink can read them
|
return {
|
||||||
if (token) {
|
headers: {
|
||||||
// if (shouldRefreshToken) {
|
...headers,
|
||||||
// refreshToken();
|
authorization: token ? `Bearer ${token}` : ""
|
||||||
// }
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return { headers };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
const retryLink = new RetryLink({
|
||||||
headers: {
|
delay: {
|
||||||
...headers,
|
initial: 300,
|
||||||
authorization: token ? `Bearer ${token}` : ""
|
max: 5,
|
||||||
}
|
jitter: true
|
||||||
};
|
},
|
||||||
} else {
|
attempts: {
|
||||||
return { headers };
|
max: 5,
|
||||||
|
retryIf: (error, _operation) => !!error
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,55 +99,27 @@ class AppContainer extends Component {
|
|||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
middlewares.push(apolloLogger);
|
middlewares.push(apolloLogger);
|
||||||
}
|
}
|
||||||
middlewares.push(errorLink.concat(authLink.concat(link)));
|
|
||||||
|
middlewares.push(retryLink.concat(errorLink.concat(authLink.concat(link))));
|
||||||
|
|
||||||
const cache = new InMemoryCache();
|
const cache = new InMemoryCache();
|
||||||
|
|
||||||
const client = new ApolloClient({
|
const client = new ApolloClient({
|
||||||
link: ApolloLink.from(middlewares),
|
link: ApolloLink.from(middlewares),
|
||||||
cache,
|
cache,
|
||||||
typeDefs,
|
|
||||||
resolvers,
|
|
||||||
connectToDevTools: true
|
connectToDevTools: true
|
||||||
});
|
});
|
||||||
|
|
||||||
client.writeData({
|
this.state = { client };
|
||||||
data: initialState
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await persistCache({
|
|
||||||
cache,
|
|
||||||
storage: window.sessionStorage,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error restoring Apollo cache", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
client,
|
|
||||||
loaded: true
|
|
||||||
});
|
|
||||||
|
|
||||||
//Init local state.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { client, loaded } = this.state;
|
const { client } = this.state;
|
||||||
|
|
||||||
if (!loaded) {
|
|
||||||
return <Spin />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ApolloProvider client={client}>
|
<ApolloProvider client={client}>
|
||||||
|
<GlobalLoadingBar />
|
||||||
<App />
|
<App />
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppContainer;
|
|
||||||
|
|||||||
@@ -1,27 +1 @@
|
|||||||
@import "~antd/dist/antd.css";
|
@import "~antd/dist/antd.css";
|
||||||
|
|
||||||
/* .ant-layout-header {
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
height: 5vh;
|
|
||||||
right: 0px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.ant-layout-content {
|
|
||||||
position: absolute;
|
|
||||||
top: 5vh;
|
|
||||||
bottom: 3vh;
|
|
||||||
left: 0px;
|
|
||||||
right: 0px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-layout-footer {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0px;
|
|
||||||
height: 3vh;
|
|
||||||
left: 0px;
|
|
||||||
right: 0px;
|
|
||||||
overflow: hidden;
|
|
||||||
} */
|
|
||||||
|
|||||||
@@ -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 i18next from "i18next";
|
||||||
|
import React, { lazy, Suspense, useEffect } from "react";
|
||||||
import "./App.css";
|
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
|
//Component Imports
|
||||||
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
|
||||||
import AlertComponent from "../components/alert/alert.component";
|
import { checkUserSession } from "../redux/user/user.actions";
|
||||||
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
|
import { selectCurrentUser } from "../redux/user/user.selectors";
|
||||||
|
|
||||||
import { auth } from "../firebase/firebase.utils";
|
|
||||||
import { UPSERT_USER } from "../graphql/user.queries";
|
|
||||||
import { GET_CURRENT_USER, GET_LANGUAGE } from "../graphql/local.queries";
|
|
||||||
// import { QUERY_BODYSHOP } from "../graphql/bodyshop.queries";
|
// import { QUERY_BODYSHOP } from "../graphql/bodyshop.queries";
|
||||||
|
|
||||||
import PrivateRoute from "../utils/private-route";
|
import PrivateRoute from "../utils/private-route";
|
||||||
|
import "./App.css";
|
||||||
|
|
||||||
const LandingPage = lazy(() => import("../pages/landing/landing.page"));
|
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 SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
|
||||||
const Unauthorized = lazy(() =>
|
const Unauthorized = lazy(() =>
|
||||||
import("../pages/unauthorized/unauthorized.component")
|
import("../pages/unauthorized/unauthorized.component")
|
||||||
);
|
);
|
||||||
|
|
||||||
export default () => {
|
const mapStateToProps = createStructuredSelector({
|
||||||
const apolloClient = useApolloClient();
|
currentUser: selectCurrentUser
|
||||||
const [loaded, setloaded] = useState(false);
|
});
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
checkUserSession: () => dispatch(checkUserSession())
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(({ checkUserSession, currentUser }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
//Run the auth code only on the first render.
|
checkUserSession();
|
||||||
const unsubscribeFromAuth = auth.onAuthStateChanged(async user => {
|
return () => {};
|
||||||
console.log("Auth State Changed.");
|
}, [checkUserSession]);
|
||||||
setloaded(true);
|
|
||||||
if (user) {
|
|
||||||
let token;
|
|
||||||
token = await user.getIdToken();
|
|
||||||
const idTokenResult = await user.getIdTokenResult();
|
|
||||||
const hasuraClaim =
|
|
||||||
idTokenResult.claims["https://hasura.io/jwt/claims"];
|
|
||||||
if (!hasuraClaim) {
|
|
||||||
// Check if refresh is required.
|
|
||||||
const metadataRef = firebase
|
|
||||||
.database()
|
|
||||||
.ref("metadata/" + user.uid + "/refreshTime");
|
|
||||||
|
|
||||||
metadataRef.on("value", async () => {
|
const { t } = useTranslation();
|
||||||
// Force refresh to pick up the latest custom claims changes.
|
if (currentUser && currentUser.language)
|
||||||
token = await user.getIdToken(true);
|
i18next.changeLanguage(currentUser.language, err => {
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//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) => {
|
|
||||||
if (err)
|
if (err)
|
||||||
return console.log("Error encountered when changing languages.", err);
|
return console.log("Error encountered when changing languages.", err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (currentUser.authorized === null) {
|
||||||
|
return <LoadingSpinner message={t("general.labels.loggingin")} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Switch>
|
<Switch>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Suspense fallback={<LoadingSpinner />}>
|
<Suspense fallback={<LoadingSpinner message='App.Js Suspense' />}>
|
||||||
<Route exact path='/' component={LandingPage} />
|
<Route exact path='/' component={LandingPage} />
|
||||||
<Route exact path='/unauthorized' component={Unauthorized} />
|
<Route exact path='/unauthorized' component={Unauthorized} />
|
||||||
<Route
|
|
||||||
exact
|
<Route exact path='/signin' component={SignInPage} />
|
||||||
path='/signin'
|
|
||||||
render={() =>
|
|
||||||
HookCurrentUser.data.currentUser ? (
|
|
||||||
<Redirect to='/manage' />
|
|
||||||
) : (
|
|
||||||
<SignInPage />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<PrivateRoute
|
<PrivateRoute
|
||||||
isAuthorized={HookCurrentUser.data.currentUser ? true : false}
|
isAuthorized={currentUser.authorized}
|
||||||
path='/manage'
|
path='/manage'
|
||||||
component={ManagePage}
|
component={ManagePage}
|
||||||
/>
|
/>
|
||||||
@@ -136,4 +67,4 @@ export default () => {
|
|||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</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 ReactDOM from "react-dom";
|
||||||
import Alert from "./alert.component";
|
import Alert from "./alert.component";
|
||||||
import { MockedProvider } from "@apollo/react-testing";
|
import { MockedProvider } from "@apollo/react-testing";
|
||||||
import { shallow } from "enzyme";
|
import { shallow, mount } from "enzyme";
|
||||||
|
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
|
|
||||||
it("renders without crashing", () => {
|
describe("Alert component", () => {
|
||||||
shallow(<Alert type="error" />);
|
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() {
|
export default function FooterComponent() {
|
||||||
return (
|
return (
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={8} offset={9}>
|
<Col span={8} offset={8}>
|
||||||
Copyright Snapt Software 2019. All rights reserved.
|
Copyright Snapt Software 2019. All rights reserved.
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</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";
|
import React, { forwardRef } from "react";
|
||||||
function FormItemEmail(props, ref) {
|
function FormItemEmail(props, ref) {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
{...props}
|
{...props}
|
||||||
addonAfter={
|
addonAfter={
|
||||||
<a href={`mailto:${props.email}`}>
|
props.email ? (
|
||||||
<Icon type="mail" />
|
<a href={`mailto:${props.email}`}>
|
||||||
</a>
|
<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 Icon, {
|
||||||
import { Col, Icon, Menu, Row } from "antd";
|
CarFilled,
|
||||||
|
FileAddFilled,
|
||||||
|
FileFilled,
|
||||||
|
GlobalOutlined,
|
||||||
|
HomeFilled,
|
||||||
|
TeamOutlined,
|
||||||
|
DollarCircleFilled,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import { Avatar, Col, Menu, Row } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { FaCalendarAlt, FaCarCrash } from "react-icons/fa";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import CurrentUserDropdown from "../current-user-dropdown/current-user-dropdown.component";
|
import UserImage from "../../assets/User.svg";
|
||||||
import GlobalSearch from "../global-search/global-search.component";
|
|
||||||
import ManageSignInButton from "../manage-sign-in-button/manage-sign-in-button.component";
|
import ManageSignInButton from "../manage-sign-in-button/manage-sign-in-button.component";
|
||||||
import "./header.styles.scss";
|
|
||||||
|
|
||||||
export default ({ landingHeader, navItems, selectedNavItem }) => {
|
import { connect } from "react-redux";
|
||||||
const apolloClient = useApolloClient();
|
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 { t } = useTranslation();
|
||||||
const handleClick = e => {
|
//TODO Add
|
||||||
apolloClient.writeData({ data: { selectedNavItem: e.key } });
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<Row type='flex' justify='space-around'>
|
<Row type="flex" justify="space-around" align="middle">
|
||||||
<Col span={16}>
|
{logo ? (
|
||||||
<Menu
|
<Col span={3}>
|
||||||
theme='dark'
|
<img alt="Shop Logo" src={logo} style={{ height: "40px" }} />
|
||||||
className='header'
|
</Col>
|
||||||
onClick={handleClick}
|
) : null}
|
||||||
selectedKeys={selectedNavItem}
|
<Col span={21}>
|
||||||
mode='horizontal'>
|
{landingHeader ? (
|
||||||
<Menu.Item>
|
<Menu
|
||||||
<GlobalSearch />
|
theme="dark"
|
||||||
</Menu.Item>
|
className="header"
|
||||||
|
selectedKeys={selectedNavItem}
|
||||||
|
mode="horizontal"
|
||||||
|
onClick={handleMenuClick}
|
||||||
|
>
|
||||||
|
<ManageSignInButton />
|
||||||
|
|
||||||
<Menu.Item key='home'>
|
<Menu.SubMenu
|
||||||
<Link to='/manage'>
|
title={
|
||||||
<Icon type='home' />
|
<div>
|
||||||
{t("menus.header.home")}
|
<Avatar
|
||||||
</Link>
|
size="medium"
|
||||||
</Menu.Item>
|
alt="Avatar"
|
||||||
<Menu.SubMenu title={t("menus.header.jobs")}>
|
src={
|
||||||
<Menu.Item key='jobs'>
|
currentUser.photoURL ? currentUser.photoURL : UserImage
|
||||||
<Link to='/manage/jobs'>
|
}
|
||||||
<Icon type='home' />
|
style={{ margin: "10px" }}
|
||||||
{t("menus.header.activejobs")}
|
/>
|
||||||
|
{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>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item key='availablejobs'>
|
<Menu.SubMenu
|
||||||
<Link to='/manage/available'>
|
title={
|
||||||
<Icon type='home' />
|
<span>
|
||||||
{t("menus.header.availablejobs")}
|
<Icon component={FaCarCrash} />
|
||||||
</Link>
|
<span>{t("menus.header.jobs")}</span>
|
||||||
</Menu.Item>
|
</span>
|
||||||
</Menu.SubMenu>
|
}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
{
|
<Menu.SubMenu
|
||||||
// navItems.map(navItem => (
|
title={
|
||||||
// <Menu.Item key={navItem.title}>
|
<span>
|
||||||
// <Link to={navItem.path}>
|
<CarFilled />
|
||||||
// {navItem.icontype ? <Icon type={navItem.icontype} /> : null}
|
<span>{t("menus.header.courtesycars")}</span>
|
||||||
// {navItem.title}
|
</span>
|
||||||
// </Link>
|
}
|
||||||
// </Menu.Item>
|
>
|
||||||
// ))
|
<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.SubMenu
|
||||||
<Menu.Item>
|
title={
|
||||||
<ManageSignInButton />
|
<span>
|
||||||
</Menu.Item>
|
<DollarCircleFilled />
|
||||||
)}
|
<span>{t("menus.header.accounting")}</span>
|
||||||
</Menu>
|
</span>
|
||||||
</Col>
|
}
|
||||||
<Col span={6} offset={2}>
|
>
|
||||||
{!landingHeader ? <CurrentUserDropdown /> : null}
|
<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>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(Header);
|
||||||
|
|||||||
@@ -1,46 +1,52 @@
|
|||||||
import React from "react";
|
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 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 mapStateToProps = createStructuredSelector({
|
||||||
const hookSelectedNavItem = useQuery(GET_CURRENT_SELECTED_NAV_ITEM);
|
currentUser: selectCurrentUser,
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
// let hookNavItems;
|
const mapDispatchToProps = dispatch => ({
|
||||||
// if (landingHeader) {
|
signOutStart: () => dispatch(signOutStart()),
|
||||||
// hookNavItems = useQuery(GET_LANDING_NAV_ITEMS, {
|
setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
// fetchPolicy: "network-only"
|
});
|
||||||
// });
|
|
||||||
// } else {
|
|
||||||
// hookNavItems = useQuery(GET_NAV_ITEMS, {
|
|
||||||
// fetchPolicy: "network-only"
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (hookNavItems.loading || hookSelectedNavItem.loading)
|
export default connect(
|
||||||
// return <LoadingSpinner />;
|
mapStateToProps,
|
||||||
// if (hookNavItems.error)
|
mapDispatchToProps
|
||||||
// return <AlertComponent message={hookNavItems.error.message} />;
|
)(function HeaderContainer({
|
||||||
// if (hookSelectedNavItem.error)
|
landingHeader,
|
||||||
// return console.log(
|
currentUser,
|
||||||
// "Unable to load Selected Navigation Item.",
|
bodyshop,
|
||||||
// hookSelectedNavItem.error
|
signOutStart,
|
||||||
// );
|
setUserLanguage
|
||||||
|
}) {
|
||||||
const { selectedNavItem } = hookSelectedNavItem.data;
|
const handleMenuClick = e => {
|
||||||
// const navItems = JSON.parse(hookNavItems.data.masterdata_by_pk.value);
|
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 (
|
return (
|
||||||
<HeaderComponent
|
<HeaderComponent
|
||||||
|
handleMenuClick={handleMenuClick}
|
||||||
|
signOutStart={signOutStart}
|
||||||
landingHeader={landingHeader}
|
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 { useQuery } from "@apollo/react-hooks";
|
||||||
import { Button, Icon, PageHeader, Tag } from "antd";
|
import { Button, PageHeader, Tag } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries";
|
import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries";
|
||||||
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container";
|
import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container";
|
||||||
//import 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 JobDetailCardsCustomerComponent from "./job-detail-cards.customer.component";
|
||||||
import JobDetailCardsDamageComponent from "./job-detail-cards.damage.component";
|
import JobDetailCardsDamageComponent from "./job-detail-cards.damage.component";
|
||||||
import JobDetailCardsDatesComponent from "./job-detail-cards.dates.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 "./job-detail-cards.styles.scss";
|
||||||
import JobDetailCardsTotalsComponent from "./job-detail-cards.totals.component";
|
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 function JobDetailCards({
|
||||||
export default function JobDetailCards({ selectedJob }) {
|
selectedJob,
|
||||||
|
setInvoiceEnterContext,
|
||||||
|
setNoteUpsertContext
|
||||||
|
}) {
|
||||||
const { loading, error, data, refetch } = useQuery(QUERY_JOB_CARD_DETAILS, {
|
const { loading, error, data, refetch } = useQuery(QUERY_JOB_CARD_DETAILS, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
variables: { id: selectedJob },
|
variables: { id: selectedJob },
|
||||||
skip: !selectedJob
|
skip: !selectedJob
|
||||||
});
|
});
|
||||||
const [noteModalVisible, setNoteModalVisible] = useState(false);
|
const scheduleModalState = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!selectedJob) {
|
if (!selectedJob) {
|
||||||
return <div>{t("jobs.errors.nojobselected")}</div>;
|
return <div>{t("jobs.errors.nojobselected")}</div>;
|
||||||
}
|
}
|
||||||
if (loading) return <LoadingSpinner />;
|
if (loading) return <LoadingSpinner />;
|
||||||
if (error) return <AlertComponent message={error.message} type='error' />;
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='job-cards-container'>
|
<div className="job-cards-container">
|
||||||
<NoteUpsertModal
|
<NoteUpsertModal />
|
||||||
|
<ScheduleJobModalContainer
|
||||||
|
scheduleModalState={scheduleModalState}
|
||||||
jobId={data.jobs_by_pk.id}
|
jobId={data.jobs_by_pk.id}
|
||||||
visible={noteModalVisible}
|
|
||||||
changeVisibility={setNoteModalVisible}
|
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
ghost={false}
|
ghost={false}
|
||||||
onBack={() => window.history.back()}
|
|
||||||
tags={
|
tags={
|
||||||
<span key='job-status'>
|
<span key="job-status">
|
||||||
{data.jobs_by_pk.job_status ? (
|
{data.jobs_by_pk.status ? (
|
||||||
<Tag color='blue'>{data.jobs_by_pk.job_status.name}</Tag>
|
<Tag color="blue">{data.jobs_by_pk.status}</Tag>
|
||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -62,37 +78,64 @@ export default function JobDetailCards({ selectedJob }) {
|
|||||||
? `${t("jobs.fields.ro_number")} ${data.jobs_by_pk.ro_number}`
|
? `${t("jobs.fields.ro_number")} ${data.jobs_by_pk.ro_number}`
|
||||||
: `${t("jobs.fields.est_number")} ${
|
: `${t("jobs.fields.est_number")} ${
|
||||||
data.jobs_by_pk.est_number
|
data.jobs_by_pk.est_number
|
||||||
}`}{" "}
|
}`}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
extra={[
|
extra={[
|
||||||
|
<Button
|
||||||
|
key="schedule"
|
||||||
|
//TODO Enabled logic based on status.
|
||||||
|
onClick={() => {
|
||||||
|
scheduleModalState[1](true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("jobs.actions.schedule")}
|
||||||
|
</Button>,
|
||||||
<Link
|
<Link
|
||||||
key='documents'
|
key="documents"
|
||||||
to={`/manage/jobs/${data.jobs_by_pk.id}#documents`}>
|
to={`/manage/jobs/${data.jobs_by_pk.id}?documents`}
|
||||||
|
>
|
||||||
<Button>
|
<Button>
|
||||||
<Icon type='file-image' />
|
<FileImageFilled />
|
||||||
{t("jobs.actions.addDocuments")}
|
{t("jobs.actions.addDocuments")}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>,
|
</Link>,
|
||||||
<Button key='printing'>
|
<Button key="printing">
|
||||||
<Icon type='printer' />
|
<PrinterFilled />
|
||||||
{t("jobs.actions.printCenter")}
|
{t("jobs.actions.printCenter")}
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button
|
<Button
|
||||||
key='notes'
|
key="notes"
|
||||||
actiontype='addNote'
|
actiontype="addNote"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setNoteModalVisible(!noteModalVisible);
|
setNoteUpsertContext({
|
||||||
}}>
|
actions: { refetch: refetch },
|
||||||
<Icon type='edit' />
|
context: {
|
||||||
|
jobId: data.jobs_by_pk.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EditFilled />
|
||||||
{t("jobs.actions.addNote")}
|
{t("jobs.actions.addNote")}
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button key='postinvoices'>
|
<Button
|
||||||
<Icon type='shopping-cart' />
|
key="postinvoices"
|
||||||
|
onClick={() => {
|
||||||
|
setInvoiceEnterContext({
|
||||||
|
actions: { refetch: refetch },
|
||||||
|
context: {
|
||||||
|
job: data.jobs_by_pk
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ShoppingFilled />
|
||||||
{t("jobs.actions.postInvoices")}
|
{t("jobs.actions.postInvoices")}
|
||||||
</Button>
|
</Button>
|
||||||
]}>
|
]}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
// loading ? (
|
// loading ? (
|
||||||
// <LoadingSkeleton />
|
// <LoadingSkeleton />
|
||||||
@@ -113,7 +156,7 @@ export default function JobDetailCards({ selectedJob }) {
|
|||||||
// )
|
// )
|
||||||
}
|
}
|
||||||
|
|
||||||
<section className='job-cards'>
|
<section className="job-cards">
|
||||||
<JobDetailCardsCustomerComponent
|
<JobDetailCardsCustomerComponent
|
||||||
loading={loading}
|
loading={loading}
|
||||||
data={data ? data.jobs_by_pk : null}
|
data={data ? data.jobs_by_pk : null}
|
||||||
@@ -158,3 +201,4 @@ export default function JobDetailCards({ selectedJob }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export default connect(null, mapDispatchToProps)(JobDetailCards);
|
||||||
|
|||||||
@@ -11,10 +11,14 @@ export default function JobDetailCardsCustomerComponent({ loading, data }) {
|
|||||||
<CardTemplate
|
<CardTemplate
|
||||||
loading={loading}
|
loading={loading}
|
||||||
title={t("jobs.labels.cards.customer")}
|
title={t("jobs.labels.cards.customer")}
|
||||||
extraLink={data && data.owner ? `/manage/owners/${data.owner.id}` : null}>
|
extraLink={data && data.owner ? `/manage/owners/${data.owner.id}` : null}
|
||||||
|
>
|
||||||
{data ? (
|
{data ? (
|
||||||
<span>
|
<span>
|
||||||
<div>{`${data.ownr_fn || ""} ${data.ownr_ln || ""}`}</div>
|
<div>
|
||||||
|
<Link to={`/manage/owners/${data.owner.id}`}>{`${data.ownr_fn ||
|
||||||
|
""} ${data.ownr_ln || ""}`}</Link>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{t("jobs.fields.phoneshort")}:
|
{t("jobs.fields.phoneshort")}:
|
||||||
<PhoneFormatter>{`${data.ownr_ph1 ||
|
<PhoneFormatter>{`${data.ownr_ph1 ||
|
||||||
@@ -31,14 +35,22 @@ export default function JobDetailCardsCustomerComponent({ loading, data }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>{`${(data.owner && data.owner.preferred_contact) || ""}`}</div>
|
<div>{`${(data.owner && data.owner.preferred_contact) || ""}`}</div>
|
||||||
{data.vehicle ? (
|
<div>
|
||||||
<Link to={`/manage/vehicles/${data.vehicle.id}`}>
|
{data.vehicle ? (
|
||||||
{`${data.vehicle.v_model_yr || ""} ${data.vehicle.v_make_desc ||
|
<Link to={`/manage/vehicles/${data.vehicleid}`}>
|
||||||
""} ${data.vehicle.v_model_desc || ""}`}
|
{`${data.v_model_yr || ""} ${data.v_make_desc ||
|
||||||
</Link>
|
""} ${data.v_model_desc || ""}`}
|
||||||
) : (
|
a
|
||||||
<span>{t("jobs.errors.novehicle")}</span>
|
</Link>
|
||||||
)}
|
) : (
|
||||||
|
<span>
|
||||||
|
{`${data.v_model_yr || ""} ${data.v_make_desc ||
|
||||||
|
""} ${data.v_model_desc || ""}`}
|
||||||
|
b
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
e
|
||||||
|
</div>
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</CardTemplate>
|
</CardTemplate>
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import CardTemplate from "./job-detail-cards.template.component";
|
import CardTemplate from "./job-detail-cards.template.component";
|
||||||
import UnfoldedCar from "../../assets/unfolded_car.svg";
|
import Car from "../job-damage-visual/job-damage-visual.component";
|
||||||
|
|
||||||
export default function JobDetailCardsDamageComponent({ loading, data }) {
|
export default function JobDetailCardsDamageComponent({ loading, data }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { area_of_damage } = data;
|
||||||
return (
|
return (
|
||||||
<CardTemplate loading={loading} title={t("jobs.labels.cards.damage")}>
|
<CardTemplate loading={loading} title={t("jobs.labels.cards.damage")}>
|
||||||
{data ? (
|
{area_of_damage ? (
|
||||||
<span>
|
<Car
|
||||||
<img src={UnfoldedCar} alt='Damaged Area' width={200} height={200} />
|
dmg1={area_of_damage.impact1 || null}
|
||||||
</span>
|
dmg2={area_of_damage.impact2 || null}
|
||||||
) : null}
|
/>
|
||||||
|
) : t("jobs.errors.nodamage")}
|
||||||
</CardTemplate>
|
</CardTemplate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { Timeline } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import CardTemplate from "./job-detail-cards.template.component";
|
import CardTemplate from "./job-detail-cards.template.component";
|
||||||
import Moment from "react-moment";
|
|
||||||
import { Timeline } from "antd";
|
|
||||||
|
|
||||||
export default function JobDetailCardsDatesComponent({ loading, data }) {
|
export default function JobDetailCardsDatesComponent({ loading, data }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -31,90 +31,84 @@ export default function JobDetailCardsDatesComponent({ loading, data }) {
|
|||||||
{data.actual_in ? (
|
{data.actual_in ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
{t("jobs.fields.actual_in")}
|
{t("jobs.fields.actual_in")}
|
||||||
<Moment format='MM/DD/YYYY'>{data.actual_in || ""}</Moment>
|
<DateFormatter>{data.actual_in}</DateFormatter>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{data.scheduled_completion ? (
|
{data.scheduled_completion ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
{t("jobs.fields.scheduled_completion")}
|
{t("jobs.fields.scheduled_completion")}
|
||||||
<Moment format='MM/DD/YYYY'>
|
<DateFormatter>{data.scheduled_completion}</DateFormatter>
|
||||||
{data.scheduled_completion || ""}
|
|
||||||
</Moment>
|
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{data.scheduled_in ? (
|
{data.scheduled_in ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
{t("jobs.fields.scheduled_in")}
|
{t("jobs.fields.scheduled_in")}
|
||||||
<Moment format='MM/DD/YYYY'>{data.scheduled_in || ""}</Moment>
|
<DateFormatter>{data.scheduled_in}</DateFormatter>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{data.actual_completion ? (
|
{data.actual_completion ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
{t("jobs.fields.actual_completion")}
|
{t("jobs.fields.actual_completion")}
|
||||||
<Moment format='MM/DD/YYYY'>
|
<DateFormatter>{data.actual_completion}</DateFormatter>
|
||||||
{data.actual_completion || ""}
|
|
||||||
</Moment>
|
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{data.scheduled_delivery ? (
|
{data.scheduled_delivery ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
{t("jobs.fields.scheduled_delivery")}
|
{t("jobs.fields.scheduled_delivery")}
|
||||||
<Moment format='MM/DD/YYYY'>
|
<DateFormatter>{data.scheduled_delivery}</DateFormatter>
|
||||||
{data.scheduled_delivery || ""}
|
|
||||||
</Moment>
|
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{data.actual_delivery ? (
|
{data.actual_delivery ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
{t("jobs.fields.actual_delivery")}
|
{t("jobs.fields.actual_delivery")}
|
||||||
<Moment format='MM/DD/YYYY'>{data.actual_delivery || ""}</Moment>
|
<DateFormatter>{data.actual_delivery}</DateFormatter>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{data.date_estimated ? (
|
{data.date_estimated ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
{t("jobs.fields.date_estimated")}
|
{t("jobs.fields.date_estimated")}
|
||||||
<Moment format='MM/DD/YYYY'>{data.date_estimated || ""}</Moment>
|
<DateFormatter>{data.date_estimated}</DateFormatter>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{data.date_open ? (
|
{data.date_open ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
{t("jobs.fields.date_open")}
|
{t("jobs.fields.date_open")}
|
||||||
<Moment format='MM/DD/YYYY'>{data.date_open || ""}</Moment>
|
<DateFormatter>{data.date_open}</DateFormatter>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{data.date_scheduled ? (
|
{data.date_scheduled ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
{t("jobs.fields.date_scheduled")}
|
{t("jobs.fields.date_scheduled")}
|
||||||
<Moment format='MM/DD/YYYY'>{data.date_scheduled || ""}</Moment>
|
<DateFormatter>{data.date_scheduled}</DateFormatter>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{data.date_invoiced ? (
|
{data.date_invoiced ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
{t("jobs.fields.date_invoiced")}
|
{t("jobs.fields.date_invoiced")}
|
||||||
<Moment format='MM/DD/YYYY'>{data.date_invoiced || ""}</Moment>
|
<DateFormatter>{data.date_invoiced}</DateFormatter>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{data.date_closed ? (
|
{data.date_closed ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
{t("jobs.fields.date_closed")}
|
{t("jobs.fields.date_closed")}
|
||||||
<Moment format='MM/DD/YYYY'>{data.date_closed || ""}</Moment>
|
<DateFormatter>{data.date_closed}</DateFormatter>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{data.date_exported ? (
|
{data.date_exported ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
{t("jobs.fields.date_exported")}
|
{t("jobs.fields.date_exported")}
|
||||||
<Moment format='MM/DD/YYYY'>{data.date_exported || ""}</Moment>
|
<DateFormatter>{data.date_exported}</DateFormatter>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
</Timeline>
|
</Timeline>
|
||||||
|
|||||||
@@ -18,13 +18,12 @@ export default function JobDetailCardsDocumentsComponent({ loading, data }) {
|
|||||||
<CardTemplate
|
<CardTemplate
|
||||||
loading={loading}
|
loading={loading}
|
||||||
title={t("jobs.labels.cards.documents")}
|
title={t("jobs.labels.cards.documents")}
|
||||||
extraLink={`/manage/jobs/${data.id}#documents`}>
|
extraLink={`/manage/jobs/${data.id}?documents`}
|
||||||
{data.documents.count > 0 ? (
|
>
|
||||||
|
{data.documents.length > 0 ? (
|
||||||
<Carousel autoplay>
|
<Carousel autoplay>
|
||||||
{data.documents.map(item => (
|
{data.documents.map(item => (
|
||||||
<div key={item.id}>
|
<img key={item.id} src={item.thumb_url} alt={item.name} />
|
||||||
<img src={item.thumb_url} alt={item.name} />
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</Carousel>
|
</Carousel>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
.ant-carousel .slick-slide {
|
.ant-carousel .slick-slide {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
height: 160px;
|
height: 50px;
|
||||||
line-height: 160px;
|
width: 50px;
|
||||||
background: #364d79;
|
line-height: 50px;
|
||||||
overflow: hidden;
|
background: #364d79;
|
||||||
}
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.ant-carousel .slick-slide h3 {
|
.ant-carousel .slick-slide h3 {
|
||||||
color: #fff;
|
color: #ccddaa;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,45 +10,45 @@ export default function JobDetailCardsInsuranceComponent({ loading, data }) {
|
|||||||
<CardTemplate loading={loading} title={t("jobs.labels.cards.insurance")}>
|
<CardTemplate loading={loading} title={t("jobs.labels.cards.insurance")}>
|
||||||
{data ? (
|
{data ? (
|
||||||
<span>
|
<span>
|
||||||
<div>{data?.ins_co_nm || 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>{data.clm_no || t("general.labels.unknown")}</div>
|
||||||
<div>
|
<div>
|
||||||
{t("jobs.labels.cards.filehandler")}
|
{t("jobs.labels.cards.filehandler")}
|
||||||
{data?.ins_ea ? (
|
{data.ins_ea ? (
|
||||||
<a href={`mailto:${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>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<div>{`${data?.ins_ct_fn || ""} ${data?.ins_ct_ln || ""}`}</div>
|
<div>{`${data.ins_ct_fn || ""} ${data.ins_ct_ln || ""}`}</div>
|
||||||
)}
|
)}
|
||||||
{data?.ins_ph1 ? (
|
{data.ins_ph1 ? (
|
||||||
<PhoneFormatter>{data?.ins_ph1}</PhoneFormatter>
|
<PhoneFormatter>{data.ins_ph1}</PhoneFormatter>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{t("jobs.labels.cards.appraiser")}
|
{t("jobs.labels.cards.appraiser")}
|
||||||
{data?.est_ea ? (
|
{data.est_ea ? (
|
||||||
<a href={`mailto:${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>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<div>{`${data?.ins_ct_fn || ""} ${data?.ins_ct_ln || ""}`}</div>
|
<div>{`${data.ins_ct_fn || ""} ${data.ins_ct_ln || ""}`}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{t("jobs.labels.cards.estimator")}
|
{t("jobs.labels.cards.estimator")}
|
||||||
{data?.est_ea ? (
|
{data.est_ea ? (
|
||||||
<a href={`mailto:${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>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<div>{`${data?.est_ct_fn || ""} ${data?.est_ct_ln || ""}`}</div>
|
<div>{`${data.est_ct_fn || ""} ${data.est_ct_ln || ""}`}</div>
|
||||||
)}
|
)}
|
||||||
{data?.est_ph1 ? (
|
{data.est_ph1 ? (
|
||||||
<PhoneFormatter>{data?.est_ph1}</PhoneFormatter>
|
<PhoneFormatter>{data.est_ph1}</PhoneFormatter>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user