Passed
Push — master ( 3cf56c...4a4ccf )
by Guangyu
04:43 queued 10s
created

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

Complexity

Total Complexity 11
Complexity/F 0

Size

Lines of Code 588
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 11
eloc 518
mnd 11
bc 11
fnc 0
dl 0
loc 588
rs 10
bpm 0
cpm 0
noi 0
c 0
b 0
f 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
  
296
      // enable submit button
297
      setSubmitButtonDisabled(false);
298
      // hide spinner
299
      setSpinnerHidden(true);
300
      // show export buttion
301
      setExportButtonHidden(false)
302
303
      return response.json();
304
    }).then(json => {
305
      if (isResponseOK) {
306
        console.log(json);
307
        
308
        let productArray = []
309
        json['reporting_period']['names'].forEach((currentValue, index) => {
310
          let productItem = {}
311
          productItem['name'] = json['reporting_period']['names'][index];
312
          productItem['unit'] = json['reporting_period']['units'][index];
313
          productItem['startdate'] = reportingPeriodBeginsDatetime.format('YYYY-MM-DD');
314
          productItem['enddate'] = reportingPeriodEndsDatetime.format('YYYY-MM-DD');
315
          productItem['subtotalinput'] = json['reporting_period']['subtotals_input'][index];
316
          productItem['subtotalcost'] = json['reporting_period']['subtotals_cost'][index];
317
          productArray.push(productItem);
318
        });
319
320
        setInvoice({
321
          institution: json['tenant']['name'],
322
          logo: logoInvoice,
323
          address: json['tenant']['rooms'] + '<br />' + json['tenant']['floors'] + '<br />' + json['tenant']['buildings'],
324
          tax: 0.01,
325
          currency: json['reporting_period']['currency_unit'],
326
          user: {
327
            name: json['tenant']['name'],
328
            address: json['tenant']['rooms'] + '<br />' + json['tenant']['floors'] + '<br />' + json['tenant']['buildings'],
329
            email: json['tenant']['email'],
330
            cell: json['tenant']['phone']
331
          },
332
          summary: {
333
            invoice_no: current_moment.format('YYYYMMDDHHmmss'),
334
            lease_number: json['tenant']['lease_number'],
335
            invoice_date: current_moment.format('YYYY-MM-DD'),
336
            payment_due: current_moment.clone().add(7, 'days').format('YYYY-MM-DD'),
337
            amount_due: json['reporting_period']['total_cost']
338
          },
339
          products: productArray
340
        });
341
342
        setSubtotal(json['reporting_period']['total_cost']);
343
        
344
        setTax(json['reporting_period']['total_cost'] * taxRate);
345
        
346
        setTotal(json['reporting_period']['total_cost'] * (1.00 + taxRate));
347
        
348
        setExcelBytesBase64(json['excel_bytes_base64']);
349
        
350
      } else {
351
        toast.error(json.description)
352
      }
353
    }).catch(err => {
354
      console.log(err);
355
    });
356
  };
357
358
  const handleExport = e => {
359
    e.preventDefault();
360
    const mimeType='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
361
    const fileName = 'tenantbill.xlsx'
362
    var fileUrl = "data:" + mimeType + ";base64," + excelBytesBase64;
363
    fetch(fileUrl)
364
        .then(response => response.blob())
365
        .then(blob => {
366
            var link = window.document.createElement("a");
367
            link.href = window.URL.createObjectURL(blob, { type: mimeType });
368
            link.download = fileName;
369
            document.body.appendChild(link);
370
            link.click();
371
            document.body.removeChild(link);
372
        });
373
  };
374
  
375
376
  return (
377
    <Fragment>
378
      <div>
379
        <Breadcrumb>
380
          <BreadcrumbItem>{t('Tenant Data')}</BreadcrumbItem><BreadcrumbItem active>{t('Tenant Bill')}</BreadcrumbItem>
381
        </Breadcrumb>
382
      </div>
383
      <Card className="bg-light mb-3">
384
        <CardBody className="p-3">
385
          <Form onSubmit={handleSubmit}>
386
            <Row form>
387
              <Col xs="auto">
388
                <FormGroup className="form-group">
389
                  <Label className={labelClasses} for="space">
390
                    {t('Space')}
391
                  </Label>
392
                  <br />
393
                  <Cascader options={cascaderOptions}
394
                    onChange={onSpaceCascaderChange}
395
                    changeOnSelect
396
                    expandTrigger="hover">
397
                    <Input value={selectedSpaceName || ''} readOnly />
398
                  </Cascader>
399
                </FormGroup>
400
              </Col>
401
              <Col xs="auto">
402
                <FormGroup>
403
                  <Label className={labelClasses} for="tenantSelect">
404
                    {t('Tenant')}
405
                  </Label>
406
                  <CustomInput type="select" id="tenantSelect" name="tenantSelect" onChange={({ target }) => setSelectedTenant(target.value)}
407
                  >
408
                    {tenantList.map((tenant, index) => (
409
                      <option value={tenant.value} key={tenant.value}>
410
                        {tenant.label}
411
                      </option>
412
                    ))}
413
                  </CustomInput>
414
                </FormGroup>
415
              </Col>
416
              <Col xs="auto">
417
                <FormGroup className="form-group">
418
                  <Label className={labelClasses} for="reportingPeriodBeginsDatetime">
419
                    {t('Reporting Period Begins')}
420
                  </Label>
421
                  <Datetime id='reportingPeriodBeginsDatetime'
422
                    value={reportingPeriodBeginsDatetime}
423
                    onChange={onReportingPeriodBeginsDatetimeChange}
424
                    isValidDate={getValidReportingPeriodBeginsDatetimes}
425
                    closeOnSelect={true} />
426
                </FormGroup>
427
              </Col>
428
              <Col xs="auto">
429
                <FormGroup className="form-group">
430
                  <Label className={labelClasses} for="reportingPeriodEndsDatetime">
431
                    {t('Reporting Period Ends')}
432
                  </Label>
433
                  <Datetime id='reportingPeriodEndsDatetime'
434
                    value={reportingPeriodEndsDatetime}
435
                    onChange={onReportingPeriodEndsDatetimeChange}
436
                    isValidDate={getValidReportingPeriodEndsDatetimes}
437
                    closeOnSelect={true} />
438
                </FormGroup>
439
              </Col>
440
              <Col xs="auto">
441
                <FormGroup>
442
                  <br></br>
443
                  <ButtonGroup id="submit">
444
                    <Button color="success" disabled={submitButtonDisabled} >{t('Submit')}</Button>
445
                  </ButtonGroup>
446
                </FormGroup>
447
              </Col>
448
              <Col xs="auto">
449
                <FormGroup>
450
                  <br></br>
451
                  <Spinner color="primary" hidden={spinnerHidden}  />
452
                </FormGroup>
453
              </Col>
454
              <Col xs="auto">
455
                  <br></br>
456
                  <ButtonIcon icon="external-link-alt" transform="shrink-3 down-2" color="falcon-default" 
457
                  hidden={exportButtonHidden}
458
                  onClick={handleExport} >
459
                    {t('Export')}
460
                  </ButtonIcon>
461
              </Col>
462
            </Row>
463
          </Form>
464
        </CardBody>
465
      </Card>
466
      <Card className="mb-3">
467
        {invoice !== undefined &&
468
        <CardBody>
469
          <Row className="justify-content-between align-items-center">
470
            <Col md>
471
              <h5 className="mb-2 mb-md-0">{t('Lease Contract Number')}: {invoice.summary.lease_number}</h5>
472
            </Col>
473
            <Col xs="auto">
474
              <ButtonIcon color="falcon-default" size="sm" icon="arrow-down" className="mr-2 mb-2 mb-sm-0">
475
                {t('Download')} (.pdf)
476
              </ButtonIcon>
477
              <ButtonIcon color="falcon-default" size="sm" icon="print" className="mr-2 mb-2 mb-sm-0">
478
                {t('Print')}
479
              </ButtonIcon>
480
481
            </Col>
482
          </Row>
483
        </CardBody>
484
        }
485
      </Card>
486
487
      <Card>
488
        {invoice !== undefined &&
489
        <CardBody>
490
          <InvoiceHeader institution={invoice.institution} logo={invoice.logo} address={invoice.address} t={t} />
491
          <Row className="justify-content-between align-items-center">
492
            <Col>
493
              <h6 className="text-500">{t('Bill To')}</h6>
494
              <h5>{invoice.user.name}</h5>
495
              <p className="fs--1" dangerouslySetInnerHTML={createMarkup(invoice.user.address)} />
496
              <p className="fs--1">
497
                <a href={`mailto:${invoice.user.email}`}>{invoice.user.email}</a>
498
                <br />
499
                <a href={`tel:${invoice.user.cell.split('-').join('')}`}>{invoice.user.cell}</a>
500
              </p>
501
            </Col>
502
            <Col sm="auto" className="ml-auto">
503
              <div className="table-responsive">
504
                <Table size="sm" borderless className="fs--1">
505
                  <tbody>
506
                    <tr>
507
                      <th className="text-sm-right">{t('Bill Number')}:</th>
508
                      <td>{invoice.summary.invoice_no}</td>
509
                    </tr>
510
                    <tr>
511
                      <th className="text-sm-right">{t('Lease Contract Number')}:</th>
512
                      <td>{invoice.summary.lease_number}</td>
513
                    </tr>
514
                    <tr>
515
                      <th className="text-sm-right">{t('Bill Date')}:</th>
516
                      <td>{invoice.summary.invoice_date}</td>
517
                    </tr>
518
                    <tr>
519
                      <th className="text-sm-right">{t('Payment Due Date')}:</th>
520
                      <td>{invoice.summary.payment_due}</td>
521
                    </tr>
522
                    <tr className="alert-success font-weight-bold">
523
                      <th className="text-sm-right">{t('Amount Payable')}:</th>
524
                      <td>{formatCurrency(invoice.summary.amount_due, invoice.currency)}</td>
525
                    </tr>
526
                  </tbody>
527
                </Table>
528
              </div>
529
            </Col>
530
          </Row>
531
          <div className="table-responsive mt-4 fs--1">
532
            <Table striped className="border-bottom">
533
              <thead>
534
                <tr className="bg-primary text-white">
535
                  <th className="border-0">{t('Energy Category')}</th>
536
                  <th className="border-0 text-center">{t('Billing Period Start')}</th>
537
                  <th className="border-0 text-center">{t('Billing Period End')}</th>
538
                  <th className="border-0 text-center">{t('Quantity')}</th>
539
                  <th className="border-0 text-right">{t('Unit')}</th>
540
                  <th className="border-0 text-right">{t('Amount')}</th>
541
                </tr>
542
              </thead>
543
              <tbody>
544
                {isIterableArray(invoice.products) &&
545
                  invoice.products.map((product, index) => <ProductTr {...product} key={index} />)}
546
              </tbody>
547
            </Table>
548
          </div>
549
          <Row noGutters className="justify-content-end">
550
            <Col xs="auto">
551
              <Table size="sm" borderless className="fs--1 text-right">
552
                <tbody>
553
                  <tr>
554
                    <th className="text-900">{t('Subtotal')}:</th>
555
                    <td className="font-weight-semi-bold">{formatCurrency(subtotal, invoice.currency)}</td>
556
                  </tr>
557
                  <tr>
558
                    <th className="text-900">{t('VAT Output Tax')}:</th>
559
                    <td className="font-weight-semi-bold">{formatCurrency(tax, invoice.currency)}</td>
560
                  </tr>
561
                  <tr className="border-top">
562
                    <th className="text-900">{t('Total Amount Payable')}:</th>
563
                    <td className="font-weight-semi-bold">{formatCurrency(total, invoice.currency)}</td>
564
                  </tr>
565
                </tbody>
566
              </Table>
567
            </Col>
568
          </Row>
569
        </CardBody>
570
        }
571
        
572
        {//todo: get the bank account infomation from API
573
        /* <CardFooter className="bg-light">
574
          <p className="fs--1 mb-0">
575
            <strong>{t('Please make sure to pay on or before the payment due date above')}, {t('Send money to the following account')}:</strong><br />
576
            {t('Acount Name')}: MyEMS商场有限公司<br />
577
            {t('Bank Name')}: 中国银行股份有限公司北京王府井支行<br />
578
            {t('Bank Address')}: 中国北京市东城区王府井大街<br />
579
            {t('RMB Account')}: 1188228822882288<br />
580
          </p>
581
        </CardFooter> */}
582
      </Card>
583
    </Fragment>
584
  );
585
};
586
587
export default withTranslation()(withRedirect(Invoice));
588