Passed
Push — master ( ae5231...1b6047 )
by Guangyu
20:54 queued 10s
created

web/src/components/MyEMS/Tenant/TenantBill.js   A

Complexity

Total Complexity 11
Complexity/F 0

Size

Lines of Code 578
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 11
eloc 508
dl 0
loc 578
rs 10
c 0
b 0
f 0
mnd 11
bc 11
fnc 0
bpm 0
cpm 0
noi 0
1
import React, { Fragment, useEffect, useState } from 'react';
2
import PropTypes from 'prop-types';
3
import {
4
  Breadcrumb,
5
  BreadcrumbItem,
6
  Button,
7
  ButtonGroup,
8
  Row,
9
  Col,
10
  Card,
11
  CardBody,
12
  CardFooter,
13
  Form,
14
  FormGroup,
15
  Input,
16
  Label,
17
  CustomInput,
18
  Table,
19
  Spinner,
20
} from 'reactstrap';
21
import Loader from '../../common/Loader';
22
import createMarkup from '../../../helpers/createMarkup';
23
import Datetime from 'react-datetime';
24
import moment from 'moment';
25
import Cascader from 'rc-cascader';
26
import { isIterableArray } from '../../../helpers/utils';
27
import logoInvoice from '../../../assets/img/logos/myems.png';
28
import { getCookieValue, createCookie } from '../../../helpers/utils';
29
import withRedirect from '../../../hoc/withRedirect';
30
import { withTranslation } from 'react-i18next';
31
import { toast } from 'react-toastify';
32
import ButtonIcon from '../../common/ButtonIcon';
33
import { APIBaseURL } from '../../../config';
34
35
36
const formatCurrency = (number, currency) =>
37
  `${currency}${number.toFixed(2).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,')}`;
38
39
const ProductTr = ({ name, description, startdate, enddate, subtotalinput, unit, subtotalcost }) => {
40
  return (
41
    <tr>
42
      <td className="align-middle">
43
        <h6 className="mb-0 text-nowrap">{name}</h6>
44
        <p className="mb-0">{description}</p>
45
      </td>
46
      <td className="align-middle text-center">{startdate}</td>
47
      <td className="align-middle text-center">{enddate}</td>
48
      <td className="align-middle text-center">{subtotalinput.toFixed(3).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,')}</td>
49
      <td className="align-middle text-right">{unit}</td>
50
      <td className="align-middle text-right">{(subtotalcost).toFixed(2).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,')}</td>
51
    </tr>
52
  );
53
};
54
55
ProductTr.propTypes = {
56
  name: PropTypes.string.isRequired,
57
  description: PropTypes.string,
58
  startdate: PropTypes.string.isRequired,
59
  enddate: PropTypes.string.isRequired,
60
  subtotalinput: PropTypes.number.isRequired,
61
  unit: PropTypes.string.isRequired,
62
  subtotalcost: PropTypes.number.isRequired,
63
};
64
65
const InvoiceHeader = ({ institution, logo, address, t }) => (
66
  <Row className="align-items-center text-center mb-3">
67
    <Col sm={6} className="text-sm-left">
68
      <img src={logo} alt="invoice" width={150} />
69
    </Col>
70
    <Col className="text-sm-right mt-3 mt-sm-0">
71
      <h2 className="mb-3">{t('Payment Notice')}</h2>
72
      <h5>{institution}</h5>
73
      {address && <p className="fs--1 mb-0" dangerouslySetInnerHTML={createMarkup(address)} />}
74
    </Col>
75
    <Col xs={12}>
76
      <hr />
77
    </Col>
78
  </Row>
79
);
80
81
InvoiceHeader.propTypes = {
82
  institution: PropTypes.string.isRequired,
83
  logo: PropTypes.string.isRequired,
84
  address: PropTypes.string
85
};
86
87
const Invoice = ({ setRedirect, setRedirectUrl, t }) => {
88
  let current_moment = moment();
89
  useEffect(() => {
90
    let is_logged_in = getCookieValue('is_logged_in');
91
    let user_name = getCookieValue('user_name');
92
    let user_display_name = getCookieValue('user_display_name');
93
    let user_uuid = getCookieValue('user_uuid');
94
    let token = getCookieValue('token');
95
    if (is_logged_in === null || !is_logged_in) {
96
      setRedirectUrl(`/authentication/basic/login`);
97
      setRedirect(true);
98
    } else {
99
      //update expires time of cookies
100
      createCookie('is_logged_in', true, 1000 * 60 * 60 * 8);
101
      createCookie('user_name', user_name, 1000 * 60 * 60 * 8);
102
      createCookie('user_display_name', user_display_name, 1000 * 60 * 60 * 8);
103
      createCookie('user_uuid', user_uuid, 1000 * 60 * 60 * 8);
104
      createCookie('token', token, 1000 * 60 * 60 * 8);
105
    }
106
  });
107
  //State
108
  // Query Parameters
109
 
110
  const [selectedSpaceName, setSelectedSpaceName] = useState(undefined);
111
  const [selectedSpaceID, setSelectedSpaceID] = useState(undefined);
112
  const [tenantList, setTenantList] = useState([]);
113
  const [selectedTenant, setSelectedTenant] = useState(undefined);
114
  const [reportingPeriodBeginsDatetime, setReportingPeriodBeginsDatetime] = useState(current_moment.clone().subtract(1, 'months').startOf('month'));
115
  const [reportingPeriodEndsDatetime, setReportingPeriodEndsDatetime] = useState(current_moment.clone().subtract(1, 'months').endOf('month'));
116
  const [cascaderOptions, setCascaderOptions] = useState(undefined);
117
118
  // buttons
119
  const [submitButtonDisabled, setSubmitButtonDisabled] = useState(true);
120
  const [spinnerHidden, setSpinnerHidden] = useState(true);
121
  const [exportButtonHidden, setExportButtonHidden] = useState(true);
122
  
123
  //Results
124
  const [invoice, setInvoice] = useState(undefined);
125
  const [subtotal, setSubtotal] = useState(0);
126
  const [taxRate, setTaxRate] = useState(0.00);
127
  const [tax, setTax] = useState(0);
128
  const [total, setTotal] = useState(0);
129
  const [excelBytesBase64, setExcelBytesBase64] = useState(undefined);
130
  
131
  useEffect(() => {
132
    let isResponseOK = false;
133
    fetch(APIBaseURL + '/spaces/tree', {
134
      method: 'GET',
135
      headers: {
136
        "Content-type": "application/json",
137
        "User-UUID": getCookieValue('user_uuid'),
138
        "Token": getCookieValue('token')
139
      },
140
      body: null,
141
142
    }).then(response => {
143
      console.log(response);
144
      if (response.ok) {
145
        isResponseOK = true;
146
      }
147
      return response.json();
148
    }).then(json => {
149
      console.log(json);
150
      if (isResponseOK) {
151
        // rename keys 
152
        json = JSON.parse(JSON.stringify([json]).split('"id":').join('"value":').split('"name":').join('"label":'));
153
        setCascaderOptions(json);
154
        setSelectedSpaceName([json[0]].map(o => o.label));
155
        setSelectedSpaceID([json[0]].map(o => o.value));
156
        // get Tenants by root Space ID
157
        let isResponseOK = false;
158
        fetch(APIBaseURL + '/spaces/' + [json[0]].map(o => o.value) + '/tenants', {
159
          method: 'GET',
160
          headers: {
161
            "Content-type": "application/json",
162
            "User-UUID": getCookieValue('user_uuid'),
163
            "Token": getCookieValue('token')
164
          },
165
          body: null,
166
167
        }).then(response => {
168
          if (response.ok) {
169
            isResponseOK = true;
170
          }
171
          return response.json();
172
        }).then(json => {
173
          if (isResponseOK) {
174
            json = JSON.parse(JSON.stringify([json]).split('"id":').join('"value":').split('"name":').join('"label":'));
175
            console.log(json);
176
            setTenantList(json[0]);
177
            if (json[0].length > 0) {
178
              setSelectedTenant(json[0][0].value);
179
              // enable submit button
180
              setSubmitButtonDisabled(false);
181
            } else {
182
              setSelectedTenant(undefined);
183
              // disable submit button
184
              setSubmitButtonDisabled(true);
185
            }
186
          } else {
187
            toast.error(json.description)
188
          }
189
        }).catch(err => {
190
          console.log(err);
191
        });
192
        // end of get Tenants by root Space ID
193
      } else {
194
        toast.error(json.description);
195
      }
196
    }).catch(err => {
197
      console.log(err);
198
    });
199
200
  }, []);
201
202
  const labelClasses = 'ls text-uppercase text-600 font-weight-semi-bold mb-0';
203
204
  let onSpaceCascaderChange = (value, selectedOptions) => {
205
    setSelectedSpaceName(selectedOptions.map(o => o.label).join('/'));
206
    setSelectedSpaceID(value[value.length - 1]);
207
208
    let isResponseOK = false;
209
    fetch(APIBaseURL + '/spaces/' + value[value.length - 1] + '/tenants', {
210
      method: 'GET',
211
      headers: {
212
        "Content-type": "application/json",
213
        "User-UUID": getCookieValue('user_uuid'),
214
        "Token": getCookieValue('token')
215
      },
216
      body: null,
217
218
    }).then(response => {
219
      if (response.ok) {
220
        isResponseOK = true;
221
      }
222
      return response.json();
223
    }).then(json => {
224
      if (isResponseOK) {
225
        json = JSON.parse(JSON.stringify([json]).split('"id":').join('"value":').split('"name":').join('"label":'));
226
        console.log(json)
227
        setTenantList(json[0]);
228
        if (json[0].length > 0) {
229
          setSelectedTenant(json[0][0].value);
230
          // enable submit button
231
          setSubmitButtonDisabled(false);
232
        } else {
233
          setSelectedTenant(undefined);
234
          // disable submit button
235
          setSubmitButtonDisabled(true);
236
        }
237
      } else {
238
        toast.error(json.description)
239
      }
240
    }).catch(err => {
241
      console.log(err);
242
    });
243
  }
244
245
246
  let onReportingPeriodBeginsDatetimeChange = (newDateTime) => {
247
    setReportingPeriodBeginsDatetime(newDateTime);
248
  }
249
250
  let onReportingPeriodEndsDatetimeChange = (newDateTime) => {
251
    setReportingPeriodEndsDatetime(newDateTime);
252
  }
253
254
  var getValidReportingPeriodBeginsDatetimes = function (currentDate) {
255
    return currentDate.isBefore(moment(reportingPeriodEndsDatetime, 'MM/DD/YYYY, hh:mm:ss a'));
256
  }
257
258
  var getValidReportingPeriodEndsDatetimes = function (currentDate) {
259
    return currentDate.isAfter(moment(reportingPeriodBeginsDatetime, 'MM/DD/YYYY, hh:mm:ss a'));
260
  }
261
262
  // Handler
263
  const handleSubmit = e => {
264
    e.preventDefault();
265
    console.log('handleSubmit');
266
    console.log(selectedSpaceID);
267
    console.log(selectedTenant);
268
    console.log(reportingPeriodBeginsDatetime.format('YYYY-MM-DDTHH:mm:ss'));
269
    console.log(reportingPeriodEndsDatetime.format('YYYY-MM-DDTHH:mm:ss'));
270
    
271
    // disable submit button
272
    setSubmitButtonDisabled(true);
273
    // show spinner
274
    setSpinnerHidden(false);
275
    // hide export buttion
276
    setExportButtonHidden(true)
277
278
    let isResponseOK = false;
279
    fetch(APIBaseURL + '/reports/tenantbill?' +
280
      'tenantid=' + selectedTenant +
281
      '&reportingperiodstartdatetime=' + reportingPeriodBeginsDatetime.format('YYYY-MM-DDTHH:mm:ss') +
282
      '&reportingperiodenddatetime=' + reportingPeriodEndsDatetime.format('YYYY-MM-DDTHH:mm:ss'), {
283
      method: 'GET',
284
      headers: {
285
        "Content-type": "application/json",
286
        "User-UUID": getCookieValue('user_uuid'),
287
        "Token": getCookieValue('token')
288
      },
289
      body: null,
290
291
    }).then(response => {
292
      if (response.ok) {
293
        isResponseOK = true;
294
      }
295
      return response.json();
296
    }).then(json => {
297
      if (isResponseOK) {
298
        console.log(json);
299
        
300
        let productArray = []
301
        json['reporting_period']['names'].forEach((currentValue, index) => {
302
          let productItem = {}
303
          productItem['name'] = json['reporting_period']['names'][index];
304
          productItem['unit'] = json['reporting_period']['units'][index];
305
          productItem['startdate'] = reportingPeriodBeginsDatetime.format('YYYY-MM-DD');
306
          productItem['enddate'] = reportingPeriodEndsDatetime.format('YYYY-MM-DD');
307
          productItem['subtotalinput'] = json['reporting_period']['subtotals_input'][index];
308
          productItem['subtotalcost'] = json['reporting_period']['subtotals_cost'][index];
309
          productArray.push(productItem);
310
        });
311
312
        setInvoice({
313
          institution: json['tenant']['name'],
314
          logo: logoInvoice,
315
          address: json['tenant']['rooms'] + '<br />' + json['tenant']['floors'] + '<br />' + json['tenant']['buildings'],
316
          tax: 0.01,
317
          currency: json['reporting_period']['currency_unit'],
318
          user: {
319
            name: json['tenant']['name'],
320
            address: json['tenant']['rooms'] + '<br />' + json['tenant']['floors'] + '<br />' + json['tenant']['buildings'],
321
            email: json['tenant']['email'],
322
            cell: json['tenant']['phone']
323
          },
324
          summary: {
325
            invoice_no: current_moment.format('YYYYMMDDHHmmss'),
326
            lease_number: json['tenant']['lease_number'],
327
            invoice_date: current_moment.format('YYYY-MM-DD'),
328
            payment_due: current_moment.clone().add(7, 'days').format('YYYY-MM-DD'),
329
            amount_due: json['reporting_period']['total_cost']
330
          },
331
          products: productArray
332
        });
333
334
        setSubtotal(json['reporting_period']['total_cost']);
335
        
336
        setTax(json['reporting_period']['total_cost'] * taxRate);
337
        
338
        setTotal(json['reporting_period']['total_cost'] * (1.00 + taxRate));
339
        
340
        setExcelBytesBase64(json['excel_bytes_base64']);
341
  
342
        // enable submit button
343
        setSubmitButtonDisabled(false);
344
        // hide spinner
345
        setSpinnerHidden(true);
346
        // show export buttion
347
        setExportButtonHidden(false)
348
        
349
      } else {
350
        toast.error(json.description)
351
      }
352
    }).catch(err => {
353
      console.log(err);
354
    });
355
  };
356
357
  const handleExport = e => {
358
    e.preventDefault();
359
    const mimeType='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
360
    const fileName = 'tenantbill.xlsx'
361
    var fileUrl = "data:" + mimeType + ";base64," + excelBytesBase64;
362
    fetch(fileUrl)
363
        .then(response => response.blob())
364
        .then(blob => {
365
            var link = window.document.createElement("a");
366
            link.href = window.URL.createObjectURL(blob, { type: mimeType });
367
            link.download = fileName;
368
            document.body.appendChild(link);
369
            link.click();
370
            document.body.removeChild(link);
371
        });
372
  };
373
  
374
375
  return (
376
    <Fragment>
377
      <div>
378
        <Breadcrumb>
379
          <BreadcrumbItem>{t('Tenant Data')}</BreadcrumbItem><BreadcrumbItem active>{t('Tenant Bill')}</BreadcrumbItem>
380
        </Breadcrumb>
381
      </div>
382
      <Card className="bg-light mb-3">
383
        <CardBody className="p-3">
384
          <Form onSubmit={handleSubmit}>
385
            <Row form>
386
              <Col xs="auto">
387
                <FormGroup className="form-group">
388
                  <Label className={labelClasses} for="space">
389
                    {t('Space')}
390
                  </Label>
391
                  <br />
392
                  <Cascader options={cascaderOptions}
393
                    onChange={onSpaceCascaderChange}
394
                    changeOnSelect
395
                    expandTrigger="hover">
396
                    <Input value={selectedSpaceName || ''} readOnly />
397
                  </Cascader>
398
                </FormGroup>
399
              </Col>
400
              <Col xs="auto">
401
                <FormGroup>
402
                  <Label className={labelClasses} for="tenantSelect">
403
                    {t('Tenant')}
404
                  </Label>
405
                  <CustomInput type="select" id="tenantSelect" name="tenantSelect" onChange={({ target }) => setSelectedTenant(target.value)}
406
                  >
407
                    {tenantList.map((tenant, index) => (
408
                      <option value={tenant.value} key={tenant.value}>
409
                        {tenant.label}
410
                      </option>
411
                    ))}
412
                  </CustomInput>
413
                </FormGroup>
414
              </Col>
415
              <Col xs="auto">
416
                <FormGroup className="form-group">
417
                  <Label className={labelClasses} for="reportingPeriodBeginsDatetime">
418
                    {t('Reporting Period Begins')}
419
                  </Label>
420
                  <Datetime id='reportingPeriodBeginsDatetime'
421
                    value={reportingPeriodBeginsDatetime}
422
                    onChange={onReportingPeriodBeginsDatetimeChange}
423
                    isValidDate={getValidReportingPeriodBeginsDatetimes}
424
                    closeOnSelect={true} />
425
                </FormGroup>
426
              </Col>
427
              <Col xs="auto">
428
                <FormGroup className="form-group">
429
                  <Label className={labelClasses} for="reportingPeriodEndsDatetime">
430
                    {t('Reporting Period Ends')}
431
                  </Label>
432
                  <Datetime id='reportingPeriodEndsDatetime'
433
                    value={reportingPeriodEndsDatetime}
434
                    onChange={onReportingPeriodEndsDatetimeChange}
435
                    isValidDate={getValidReportingPeriodEndsDatetimes}
436
                    closeOnSelect={true} />
437
                </FormGroup>
438
              </Col>
439
              <Col xs="auto">
440
                <FormGroup>
441
                  <br></br>
442
                  <ButtonGroup id="submit">
443
                    <Button color="success" disabled={submitButtonDisabled} >{t('Submit')}</Button>
444
                  </ButtonGroup>
445
                </FormGroup>
446
              </Col>
447
              <Col xs="auto">
448
                <FormGroup>
449
                  <br></br>
450
                  <Spinner color="primary" hidden={spinnerHidden}  />
451
                </FormGroup>
452
              </Col>
453
              <Col xs="auto">
454
                  <br></br>
455
                  <ButtonIcon icon="external-link-alt" transform="shrink-3 down-2" color="falcon-default" 
456
                  hidden={exportButtonHidden}
457
                  onClick={handleExport} >
458
                    {t('Export')}
459
                  </ButtonIcon>
460
              </Col>
461
            </Row>
462
          </Form>
463
        </CardBody>
464
      </Card>
465
      <Card className="mb-3">
466
        {invoice !== undefined &&
467
        <CardBody>
468
          <Row className="justify-content-between align-items-center">
469
            <Col md>
470
              <h5 className="mb-2 mb-md-0">{t('Lease Contract Number')}: {invoice.summary.lease_number}</h5>
471
            </Col>
472
          </Row>
473
        </CardBody>
474
        }
475
      </Card>
476
477
      <Card>
478
        {invoice !== undefined &&
479
        <CardBody>
480
          <InvoiceHeader institution={invoice.institution} logo={invoice.logo} address={invoice.address} t={t} />
481
          <Row className="justify-content-between align-items-center">
482
            <Col>
483
              <h6 className="text-500">{t('Bill To')}</h6>
484
              <h5>{invoice.user.name}</h5>
485
              <p className="fs--1" dangerouslySetInnerHTML={createMarkup(invoice.user.address)} />
486
              <p className="fs--1">
487
                <a href={`mailto:${invoice.user.email}`}>{invoice.user.email}</a>
488
                <br />
489
                <a href={`tel:${invoice.user.cell.split('-').join('')}`}>{invoice.user.cell}</a>
490
              </p>
491
            </Col>
492
            <Col sm="auto" className="ml-auto">
493
              <div className="table-responsive">
494
                <Table size="sm" borderless className="fs--1">
495
                  <tbody>
496
                    <tr>
497
                      <th className="text-sm-right">{t('Bill Number')}:</th>
498
                      <td>{invoice.summary.invoice_no}</td>
499
                    </tr>
500
                    <tr>
501
                      <th className="text-sm-right">{t('Lease Contract Number')}:</th>
502
                      <td>{invoice.summary.lease_number}</td>
503
                    </tr>
504
                    <tr>
505
                      <th className="text-sm-right">{t('Bill Date')}:</th>
506
                      <td>{invoice.summary.invoice_date}</td>
507
                    </tr>
508
                    <tr>
509
                      <th className="text-sm-right">{t('Payment Due Date')}:</th>
510
                      <td>{invoice.summary.payment_due}</td>
511
                    </tr>
512
                    <tr className="alert-success font-weight-bold">
513
                      <th className="text-sm-right">{t('Amount Payable')}:</th>
514
                      <td>{formatCurrency(invoice.summary.amount_due, invoice.currency)}</td>
515
                    </tr>
516
                  </tbody>
517
                </Table>
518
              </div>
519
            </Col>
520
          </Row>
521
          <div className="table-responsive mt-4 fs--1">
522
            <Table striped className="border-bottom">
523
              <thead>
524
                <tr className="bg-primary text-white">
525
                  <th className="border-0">{t('Energy Category')}</th>
526
                  <th className="border-0 text-center">{t('Billing Period Start')}</th>
527
                  <th className="border-0 text-center">{t('Billing Period End')}</th>
528
                  <th className="border-0 text-center">{t('Quantity')}</th>
529
                  <th className="border-0 text-right">{t('Unit')}</th>
530
                  <th className="border-0 text-right">{t('Amount')}</th>
531
                </tr>
532
              </thead>
533
              <tbody>
534
                {isIterableArray(invoice.products) &&
535
                  invoice.products.map((product, index) => <ProductTr {...product} key={index} />)}
536
              </tbody>
537
            </Table>
538
          </div>
539
          <Row noGutters className="justify-content-end">
540
            <Col xs="auto">
541
              <Table size="sm" borderless className="fs--1 text-right">
542
                <tbody>
543
                  <tr>
544
                    <th className="text-900">{t('Subtotal')}:</th>
545
                    <td className="font-weight-semi-bold">{formatCurrency(subtotal, invoice.currency)}</td>
546
                  </tr>
547
                  <tr>
548
                    <th className="text-900">{t('VAT Output Tax')}:</th>
549
                    <td className="font-weight-semi-bold">{formatCurrency(tax, invoice.currency)}</td>
550
                  </tr>
551
                  <tr className="border-top">
552
                    <th className="text-900">{t('Total Amount Payable')}:</th>
553
                    <td className="font-weight-semi-bold">{formatCurrency(total, invoice.currency)}</td>
554
                  </tr>
555
                </tbody>
556
              </Table>
557
            </Col>
558
          </Row>
559
        </CardBody>
560
        }
561
        
562
        {//todo: get the bank account infomation from API
563
        /* <CardFooter className="bg-light">
564
          <p className="fs--1 mb-0">
565
            <strong>{t('Please make sure to pay on or before the payment due date above')}, {t('Send money to the following account')}:</strong><br />
566
            {t('Acount Name')}: MyEMS商场有限公司<br />
567
            {t('Bank Name')}: 中国银行股份有限公司北京王府井支行<br />
568
            {t('Bank Address')}: 中国北京市东城区王府井大街<br />
569
            {t('RMB Account')}: 1188228822882288<br />
570
          </p>
571
        </CardFooter> */}
572
      </Card>
573
    </Fragment>
574
  );
575
};
576
577
export default withTranslation()(withRedirect(Invoice));
578