├── src
├── store
│ ├── conout
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── actions.js
│ ├── discovery
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── actions.js
│ ├── aemo_discovery
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── actions.js
│ ├── banking
│ │ ├── data
│ │ │ ├── index.js
│ │ │ ├── reducer.js
│ │ │ └── actions.js
│ │ ├── comparison
│ │ │ ├── index.js
│ │ │ ├── actions.js
│ │ │ └── reducer.js
│ │ └── selection
│ │ │ ├── index.js
│ │ │ ├── actions.js
│ │ │ └── reducer.js
│ ├── data-source
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── actions.js
│ ├── energy
│ │ ├── data
│ │ │ ├── index.js
│ │ │ ├── reducer.js
│ │ │ └── actions.js
│ │ ├── comparison
│ │ │ ├── index.js
│ │ │ ├── actions.js
│ │ │ └── reducer.js
│ │ └── selection
│ │ │ ├── index.js
│ │ │ ├── actions.js
│ │ │ └── reducer.js
│ ├── version-info
│ │ ├── index.js
│ │ ├── actions.js
│ │ └── reducer.js
│ └── index.js
├── components
│ ├── header
│ │ ├── CDS-logo.png
│ │ └── index.js
│ ├── data
│ │ ├── energy
│ │ │ ├── ExternalLink.js
│ │ │ ├── Type.js
│ │ │ ├── FuelType.js
│ │ │ ├── CustomerType.js
│ │ │ ├── IntrinsicGreenPower.js
│ │ │ ├── Rate.js
│ │ │ ├── EnergyPlanEligibility.js
│ │ │ ├── EnergyPlanIncentive.js
│ │ │ ├── EnergyPlanFee.js
│ │ │ ├── MeteringCharge.js
│ │ │ ├── Geography.js
│ │ │ ├── AdditionalInfo.js
│ │ │ ├── SingleRate.js
│ │ │ ├── EnergyPlanDiscount.js
│ │ │ ├── EnergyPlanGreenPowerCharge.js
│ │ │ ├── ControlledLoad.js
│ │ │ ├── EnergyPlanList.js
│ │ │ ├── TimeOfUseRate.js
│ │ │ ├── EnergyPlanSolarFeedInTariff.js
│ │ │ ├── EnergyPlanTariffPeriod.js
│ │ │ ├── PlanContract.js
│ │ │ ├── Plan.js
│ │ │ └── EnergyPanel.js
│ │ ├── banking
│ │ │ ├── AdditionalInformationUris.js
│ │ │ ├── RateCondition.js
│ │ │ ├── Bundle.js
│ │ │ ├── Constraint.js
│ │ │ ├── ProductCategory.js
│ │ │ ├── RateSubTier.js
│ │ │ ├── CardArt.js
│ │ │ ├── DiscountEligibility.js
│ │ │ ├── RateTier.js
│ │ │ ├── Eligibility.js
│ │ │ ├── AdditionalInfo.js
│ │ │ ├── DepositRate.js
│ │ │ ├── Fee.js
│ │ │ ├── FeeDiscount.js
│ │ │ ├── Feature.js
│ │ │ ├── LendingRate.js
│ │ │ ├── BankingProductList.js
│ │ │ ├── BankingPanel.js
│ │ │ └── Product.js
│ │ ├── DateTime.js
│ │ ├── Duration.js
│ │ ├── discovery
│ │ │ ├── StatusOutages.js
│ │ │ ├── AEMODiscoveryInfo.js
│ │ │ └── DiscoveryInfo.js
│ │ └── ConsolePanel.js
│ ├── Page.js
│ ├── comparison
│ │ ├── EnergyComparisonPanel.js
│ │ └── BankingComparisonPanel.js
│ └── data-source
│ │ ├── DataSourcePanel.js
│ │ └── DataSource.js
├── utils
│ ├── async-actions.js
│ ├── enum-comp.js
│ ├── datetime.js
│ ├── cors.js
│ ├── url.js
│ └── dict.js
├── index.js
├── index.css
└── serviceWorker.js
├── public
├── manifest.json
├── override.json
└── index.html
├── .travis.yml
├── .gitignore
├── LICENSE
├── .github
└── workflows
│ └── deploy.yml
├── package.json
└── README.md
/src/store/conout/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/discovery/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/aemo_discovery/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/banking/data/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/data-source/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/energy/data/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/version-info/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/banking/comparison/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/banking/selection/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/energy/comparison/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/energy/selection/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "product-comparator-demo",
3 | "name": "Product Comparator demo",
4 | "start_url": "."
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/header/CDS-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ConsumerDataStandardsAustralia/product-comparator-demo/HEAD/src/components/header/CDS-logo.png
--------------------------------------------------------------------------------
/src/store/banking/comparison/actions.js:
--------------------------------------------------------------------------------
1 | export const COMPARE_PRODUCTS = 'COMPARE_PRODUCTS'
2 |
3 | export const compareProducts = (products) => ({
4 | type: COMPARE_PRODUCTS,
5 | payload: products
6 | })
7 |
--------------------------------------------------------------------------------
/src/store/energy/comparison/actions.js:
--------------------------------------------------------------------------------
1 | export const COMPARE_ENERGY_PLANS = 'COMPARE_ENERGY_PLANS'
2 |
3 | export const comparePlans = (plans) => ({
4 | type: COMPARE_ENERGY_PLANS,
5 | payload: plans
6 | })
7 |
--------------------------------------------------------------------------------
/src/utils/async-actions.js:
--------------------------------------------------------------------------------
1 | export const pending = actionType => `${actionType}_PENDING`
2 | export const fulfilled = actionType => `${actionType}_FULFILLED`
3 | export const rejected = actionType => `${actionType}_REJECTED`
4 |
--------------------------------------------------------------------------------
/src/utils/enum-comp.js:
--------------------------------------------------------------------------------
1 | const ecomp = (a, b) => {
2 | if (b === 'OTHER') return -1
3 | if (a === 'OTHER') return 1
4 | if (a < b) return -1
5 | if (a === b) return 0
6 | return 1
7 | }
8 |
9 | export default ecomp
10 |
--------------------------------------------------------------------------------
/src/components/data/energy/ExternalLink.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const ExternalLink = ({link, children}) => (
4 | {children}
5 | )
6 |
7 | export default ExternalLink
8 |
--------------------------------------------------------------------------------
/src/components/data/energy/Type.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const typeDict = {
4 | STANDING: 'Standing',
5 | MARKET: 'Market',
6 | REGULATED: 'Regulated'
7 | }
8 |
9 | const Type = ({type}) => (
10 | {typeDict[type]}
11 | )
12 |
13 | export default Type
14 |
--------------------------------------------------------------------------------
/src/store/banking/comparison/reducer.js:
--------------------------------------------------------------------------------
1 | import { COMPARE_PRODUCTS } from './actions'
2 |
3 | export default function bankingComparison(state=[], action) {
4 | if (action.type === COMPARE_PRODUCTS) {
5 | return [...action.payload]
6 | } else {
7 | return state
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/store/energy/comparison/reducer.js:
--------------------------------------------------------------------------------
1 | import { COMPARE_ENERGY_PLANS } from './actions'
2 |
3 | export default function energyComparison(state=[], action) {
4 | if (action.type === COMPARE_ENERGY_PLANS) {
5 | return [...action.payload]
6 | } else {
7 | return state
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/data/energy/FuelType.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const fuelTypeDict = {
4 | ELECTRICITY: 'Electricity',
5 | GAS: 'Gas',
6 | DUAL: 'Dual'
7 | }
8 |
9 | const FuelType = ({fuelType}) => (
10 | {fuelTypeDict[fuelType]}
11 | )
12 |
13 | export default FuelType
14 |
--------------------------------------------------------------------------------
/src/components/data/energy/CustomerType.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const customerTypeDict = {
4 | RESIDENTIAL: 'Residential',
5 | BUSINESS: 'Business'
6 | }
7 |
8 | const CustomerType = ({customerType}) => (
9 | {customerTypeDict[customerType]}
10 | )
11 |
12 | export default CustomerType
13 |
--------------------------------------------------------------------------------
/src/components/data/energy/IntrinsicGreenPower.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const IntrinsicGreenPower = ({intrinsicGreenPower}) => {
4 | const {greenPercentage} = intrinsicGreenPower
5 | return (
6 |
Green Percentage: {(greenPercentage * 100).toFixed(2)}%
7 | )
8 | }
9 |
10 | export default IntrinsicGreenPower
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "14.18.1"
4 | cache:
5 | directories:
6 | - node_modules
7 | script:
8 | - yarn build
9 | before_deploy:
10 | - cd $TRAVIS_BUILD_DIR
11 | deploy:
12 | provider: pages
13 | skip_cleanup: true
14 | github_token: $github_token
15 | local_dir: build
16 | on:
17 | branch: master
18 | target-branch: gh-pages
19 |
--------------------------------------------------------------------------------
/src/utils/datetime.js:
--------------------------------------------------------------------------------
1 | import {pattern} from 'iso8601-duration'
2 |
3 | export function format(rfc3339DateTime) {
4 | const date = new Date(Date.parse(rfc3339DateTime)).toString()
5 | const index = date.indexOf('GMT')
6 | if (index > 0) return date.substring(0, index)
7 | return date
8 | }
9 |
10 | export function isDuration(str) {
11 | return pattern.test(str)
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/data/banking/AdditionalInformationUris.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const AdditionalInformationUris = ({title, uris}) => (
4 |
10 | )
11 |
12 | export default AdditionalInformationUris
13 |
--------------------------------------------------------------------------------
/src/components/data/DateTime.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {format} from '../../utils/datetime'
3 | import {makeStyles} from '@material-ui/core'
4 |
5 | const useStyles = makeStyles(() => ({
6 | datetime: {
7 | textDecoration: 'underline'
8 | }
9 | }))
10 |
11 | const DateTime = ({rfc3339}) => {
12 | const classes = useStyles()
13 | return ({format(rfc3339)} )
14 | }
15 |
16 | export default DateTime
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | .eslintcache
25 |
26 | .yarn
27 | .yarnrc.yml
28 |
--------------------------------------------------------------------------------
/src/components/data/banking/RateCondition.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const RateCondition = (props) => {
4 | const {additionalInfo, additionalInfoUri} = props.rateCondition
5 | return (
6 |
7 | {!!additionalInfo &&
{additionalInfo}
}
8 | {!!additionalInfoUri &&
}
9 |
10 | )
11 | }
12 |
13 | export default RateCondition
14 |
--------------------------------------------------------------------------------
/public/override.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "AGL",
4 | "energyPrd": "https://cdr.energymadeeasy.gov.au/agl"
5 | },
6 | {
7 | "name": "Origin Energy",
8 | "energyPrd": "https://cdr.energymadeeasy.gov.au/origin",
9 | "url": "https://public.mydata.cdr.originenergy.com.au"
10 | },
11 | {
12 | "name": "EnergyAustralia",
13 | "energyPrd": "https://cdr.energymadeeasy.gov.au/energyaustralia",
14 | "url": "https://authncdr.energyaustralia.com.au/"
15 | }
16 | ]
17 |
--------------------------------------------------------------------------------
/src/components/data/energy/Rate.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Rate = ({rate, defaultUnit = 'KWH'}) => {
4 | const {unitPrice, measureUnit, volume} = rate
5 | return (
6 |
7 |
8 | 1{measureUnit || defaultUnit}:
9 | ${unitPrice} (exclusive of GST)
10 | {volume && (
11 | - up to: {volume}{measureUnit || defaultUnit}
12 | )}
13 |
14 |
15 | )
16 | }
17 |
18 | export default Rate
19 |
--------------------------------------------------------------------------------
/src/components/data/banking/Bundle.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Bundle = (props) => {
4 | const {bundle} = props
5 | return (
6 |
7 | {bundle.name}
8 | {bundle.description}
9 | {!!bundle.additionalInfo && {bundle.additionalInfo}
}
10 | {!!bundle.additionalInfoUri && }
11 |
12 | )
13 | }
14 |
15 | export default Bundle
16 |
--------------------------------------------------------------------------------
/src/components/data/energy/EnergyPlanEligibility.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const EnergyPlanEligibility = ({eligibility}) => {
4 | const {type, information, description} = eligibility
5 | return (
6 |
7 | Type: {type}
8 | Information: {information}
9 | {description && (
10 | Description: {description}
11 | )}
12 |
13 | )
14 | }
15 |
16 | export default EnergyPlanEligibility
17 |
--------------------------------------------------------------------------------
/src/components/data/energy/EnergyPlanIncentive.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const EnergyPlanIncentive = ({incentive}) => {
4 | const {displayName, description, category, eligibility} = incentive
5 | return (
6 |
7 | Display Name: {displayName}
8 | Description: {description}
9 | Category: {category}
10 | {eligibility && (
11 | Eligibility: {eligibility}
12 | )}
13 |
14 | )
15 | }
16 |
17 | export default EnergyPlanIncentive
18 |
--------------------------------------------------------------------------------
/src/components/data/banking/Constraint.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {translateConstraintType} from '../../../utils/dict'
3 |
4 | const Constraint = (props) => {
5 | const {constraintType, additionalInfo, additionalValue, additionalInfoUri} = props.constraint
6 | return (
7 |
8 | {translateConstraintType(constraintType)} - ${additionalValue}
9 | {!!additionalInfo && {additionalInfo}
}
10 | {!!additionalInfoUri && }
11 |
12 | )
13 | }
14 |
15 | export default Constraint
16 |
--------------------------------------------------------------------------------
/src/store/energy/selection/actions.js:
--------------------------------------------------------------------------------
1 | export const SELECT_ENERGY_PLAN = 'SELECT_ENERGY_PLAN'
2 | export const DESELECT_ENERGY_PLAN = 'DESELECT_ENERGY_PLAN'
3 | export const CLEAR_ENERGY_SELECTION = 'CLEAR_ENERGY_SELECTION'
4 |
5 | export const selectPlan = (dataSourceIdx, plan) => ({
6 | type: SELECT_ENERGY_PLAN,
7 | payload: { dataSourceIdx, plan }
8 | })
9 |
10 | export const deselectPlan = (dataSourceIdx, plan) => ({
11 | type: DESELECT_ENERGY_PLAN,
12 | payload: { dataSourceIdx, plan }
13 | })
14 |
15 | export const clearSelection = (dataSourceIdx) => ({
16 | type: CLEAR_ENERGY_SELECTION,
17 | payload: dataSourceIdx
18 | })
19 |
--------------------------------------------------------------------------------
/src/components/data/energy/EnergyPlanFee.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const EnergyPlanFee = ({fee}) => {
4 | const {type, term, amount, rate, description} = fee
5 | return (
6 |
7 | Type: {type}
8 | Term: {term}
9 | {amount && (
10 | Amount: ${amount}
11 | )}
12 | {rate && (
13 | Rate: {(rate * 100).toFixed(2)}%
14 | )}
15 | {description && (
16 | Description: {description}
17 | )}
18 |
19 | )
20 | }
21 |
22 | export default EnergyPlanFee
23 |
--------------------------------------------------------------------------------
/src/store/conout/reducer.js:
--------------------------------------------------------------------------------
1 | import { CONSOLE_OUT, CONSOLE_REFRESH, CONSOLE_CLEAN } from './actions'
2 |
3 | export default function conout(state = {actions: []}, action) {
4 | switch (action.type) {
5 | case CONSOLE_OUT:
6 | const { payload } = action
7 | if (typeof payload.obj === 'undefined') {
8 | console[payload.lvl](payload.txt)
9 | } else {
10 | console[payload.lvl](payload.txt, payload.obj)
11 | }
12 | state.actions.push(action)
13 | return state
14 | case CONSOLE_REFRESH:
15 | return {...state}
16 | case CONSOLE_CLEAN:
17 | return {actions: []}
18 | default:
19 | return state
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/store/banking/selection/actions.js:
--------------------------------------------------------------------------------
1 | export const SELECT_BANKING_PRODUCT = 'SELECT_BANKING_PRODUCT'
2 | export const DESELECT_BANKING_PRODUCT = 'DESELECT_BANKING_PRODUCT'
3 | export const CLEAR_BANKING_SELECTION = 'CLEAR_BANKING_SELECTION'
4 |
5 | export const selectProduct = (dataSourceIdx, product) => ({
6 | type: SELECT_BANKING_PRODUCT,
7 | payload: { dataSourceIdx, product }
8 | })
9 |
10 | export const deselectProduct = (dataSourceIdx, product) => ({
11 | type: DESELECT_BANKING_PRODUCT,
12 | payload: { dataSourceIdx, product }
13 | })
14 |
15 | export const clearSelection = (dataSourceIdx) => ({
16 | type: CLEAR_BANKING_SELECTION,
17 | payload: dataSourceIdx
18 | })
19 |
--------------------------------------------------------------------------------
/src/components/data/banking/ProductCategory.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Product from './Product'
3 | import {translateProductCategory} from '../../../utils/dict'
4 |
5 | const strcomp = (a, b) => {
6 | if ( a < b) return -1
7 | else if (a === b) return 0
8 | else return 1
9 | }
10 |
11 | const ProductCategory = (props) => {
12 | const { dataSourceIndex, category, products } = props
13 | return (
14 |
15 |
{translateProductCategory(category)}
16 | {products.sort((a,b)=>(strcomp(a.name, b.name))).map(
17 | (product, index) =>
)}
18 |
19 | )
20 | }
21 |
22 | export default ProductCategory
23 |
--------------------------------------------------------------------------------
/src/store/energy/selection/reducer.js:
--------------------------------------------------------------------------------
1 | import {SELECT_ENERGY_PLAN, DESELECT_ENERGY_PLAN, CLEAR_ENERGY_SELECTION} from './actions'
2 |
3 | export default function energySelection(state = [], action) {
4 | switch (action.type) {
5 | case SELECT_ENERGY_PLAN:
6 | return [...state, action.payload]
7 | case DESELECT_ENERGY_PLAN:
8 | const { payload } = action
9 | return state.filter(selection => (selection.dataSourceIdx !== payload.dataSourceIdx || selection.plan.planId !== payload.plan.planId))
10 | case CLEAR_ENERGY_SELECTION:
11 | return state.filter(selection => (selection.dataSourceIdx !== action.payload))
12 | default:
13 | return state
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/store/banking/selection/reducer.js:
--------------------------------------------------------------------------------
1 | import {SELECT_BANKING_PRODUCT, DESELECT_BANKING_PRODUCT, CLEAR_BANKING_SELECTION} from './actions'
2 |
3 | export default function bankingSelection(state = [], action) {
4 | switch (action.type) {
5 | case SELECT_BANKING_PRODUCT:
6 | return [...state, action.payload]
7 | case DESELECT_BANKING_PRODUCT:
8 | const { payload } = action
9 | return state.filter(prd => (prd.dataSourceIdx !== payload.dataSourceIdx || prd.product.productId !== payload.product.productId))
10 | case CLEAR_BANKING_SELECTION:
11 | return state.filter(prd => (prd.dataSourceIdx !== action.payload))
12 | default:
13 | return state
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Product Comparator (demo)
10 |
11 |
12 |
13 |
14 |
15 | You need to enable JavaScript to run this app.
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/store/version-info/actions.js:
--------------------------------------------------------------------------------
1 | export const LOAD_VERSION_INFO = 'LOAD_VERSION_INFO'
2 | export const SAVE_VERSION_INFO = 'SAVE_VERSION_INFO'
3 | export const SET_VERSIONS_EDITABLE = 'SET_VERSIONS_EDITABLE'
4 | export const SET_VERSIONS_READ_ONLY = 'SET_VERSIONS_READ_ONLY'
5 |
6 | export function loadVersionInfo() {
7 | return {
8 | type: LOAD_VERSION_INFO
9 | }
10 | }
11 |
12 | export function saveVersionInfo(versionInfo) {
13 | return {
14 | type: SAVE_VERSION_INFO,
15 | versionInfo: versionInfo
16 | }
17 | }
18 |
19 | export function setVersionsEditable() {
20 | return {
21 | type: SET_VERSIONS_EDITABLE
22 | }
23 | }
24 |
25 | export function setVersionsReadOnly() {
26 | return {
27 | type: SET_VERSIONS_READ_ONLY
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/data/energy/MeteringCharge.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Duration from '../Duration'
3 |
4 | const MeteringCharge = ({meteringCharge}) => {
5 | const {displayName, description, minimumValue, maximumValue, period} = meteringCharge
6 | return (
7 |
8 | Display Name: {displayName}
9 | {description && (
10 | Description: {description}
11 | )}
12 | Minimum Value: {minimumValue}
13 | {maximumValue && (
14 | Maximum Value: {maximumValue}
15 | )}
16 | {period && (
17 | Period:
18 | )}
19 |
20 | )
21 | }
22 |
23 | export default MeteringCharge
24 |
--------------------------------------------------------------------------------
/src/components/header/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { makeStyles } from '@material-ui/core'
3 | import logo from './CDS-logo.png'
4 | import Typography from '@material-ui/core/Typography'
5 |
6 | const useStyles = makeStyles(theme => ({
7 | header: {
8 | marginTop: theme.spacing(2),
9 | display: 'flex',
10 | alignItems: 'center',
11 | justifyContent: 'space-between'
12 | },
13 | title: {
14 | color: '#202020',
15 | fontSize: theme.typography.pxToRem(30),
16 | fontWeight: 'bold'
17 | }
18 | }))
19 |
20 | export default function Header(props) {
21 | const classes = useStyles()
22 | return (
23 |
24 |
25 |
{props.title}
26 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/src/utils/cors.js:
--------------------------------------------------------------------------------
1 | import {conoutHtmlError, conoutError} from '../store/conout/actions'
2 |
3 | export function createConoutError(error, url) {
4 | return conoutError('Caught ' + error + ' while requesting ' + url + (error.name === 'TypeError' ?
5 | ' Possibly caused by the endpoint not supporting Cross-Origin Requests (CORS)' : ''))
6 | }
7 |
8 | export function checkExposedHeaders(response, fullUrl, dispatch) {
9 | if (!response.headers.get('x-v')) {
10 | const msg = `Response for ${fullUrl}: doesn't expose header x-v: possibly caused by incomplete `
11 | const corsSupport = 'CORS support'
12 | dispatch(conoutHtmlError(
13 | msg + corsSupport,
14 | `${msg}${corsSupport} `
15 | ))
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/store/aemo_discovery/reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | RETRIEVE_AEMO_STATUS,
3 | RETRIEVE_AEMO_OUTAGES
4 | } from './actions'
5 | import {fulfilled} from '../../utils/async-actions'
6 |
7 | export default function discovery(state = {}, action) {
8 | switch (action.type) {
9 | case fulfilled(RETRIEVE_AEMO_STATUS): {
10 | const s = {...state}
11 | if (action.payload) {
12 | const response = action.payload
13 | s.statusDetails = response ? response.data : null
14 | }
15 | return s
16 | }
17 | case fulfilled(RETRIEVE_AEMO_OUTAGES): {
18 | const s = {...state}
19 | if (action.payload) {
20 | const response = action.payload
21 | s.outagesDetails = response ? response.data : null
22 | }
23 | return s
24 | }
25 | default:
26 | return state
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/data/banking/RateSubTier.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import RateCondition from './RateCondition'
3 | import {translateRateApplicationMethod, translateUnitOfMeasure} from '../../../utils/dict'
4 |
5 | const RateSubTier = (props) => {
6 | const {name, unitOfMeasure, minimumValue, maximumValue, rateApplicationMethod, applicabilityConditions} = props.subTier
7 | return (
8 |
9 |
{name}
10 |
Minimum {minimumValue} {translateUnitOfMeasure(unitOfMeasure)}
11 | {!!maximumValue &&
Maximum {maximumValue} {translateUnitOfMeasure(unitOfMeasure)}
}
12 | {!!rateApplicationMethod &&
Applied on {translateRateApplicationMethod(rateApplicationMethod)}
}
13 | {!!applicabilityConditions &&
}
14 |
15 | )
16 | }
17 |
18 | export default RateSubTier
19 |
--------------------------------------------------------------------------------
/src/components/data/Duration.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { pattern, parse } from 'iso8601-duration'
3 | import pluralize from 'pluralize'
4 |
5 | const Duration = props => {
6 | const { prefix, value } = props
7 | let showNum
8 | if (!value || value.length === 0) {
9 | return false
10 | }
11 | if (!pattern.test(value)) {
12 | return {value}
13 | }
14 | const duration = parse(value)
15 | const units = ['year', 'month', 'day', 'hour', 'minute', 'second']
16 | return {prefix ? prefix + ' ' : ''}{units.map((unit, idx) => {
17 | const num = duration[unit + 's']
18 | if (!num) {
19 | return false
20 | }
21 | showNum ||= num > 1 || units.filter(u => u !== unit && duration[u + 's']).length > 0
22 | return {showNum ? ' ' + num : ''} {pluralize(unit, num)}
23 | })}
24 | }
25 |
26 | export default Duration
27 |
--------------------------------------------------------------------------------
/src/store/discovery/reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | RETRIEVE_STATUS,
3 | RETRIEVE_OUTAGES
4 | } from './actions'
5 | import {fulfilled} from '../../utils/async-actions'
6 |
7 | export default function discovery(state = [], action) {
8 | switch (action.type) {
9 | case fulfilled(RETRIEVE_STATUS): {
10 | const s = [...state]
11 | if (action.payload) {
12 | const {idx, response} = action.payload
13 | if (!s[idx]) {
14 | s[idx] = {}
15 | }
16 | s[idx].statusDetails = response ? response.data : null
17 | }
18 | return s
19 | }
20 | case fulfilled(RETRIEVE_OUTAGES): {
21 | const s = [...state]
22 | if (action.payload) {
23 | const {idx, response} = action.payload
24 | if (!s[idx]) {
25 | s[idx] = {}
26 | }
27 | s[idx].outagesDetails = response ? response.data : null
28 | }
29 | return s
30 | }
31 | default:
32 | return state
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/data/banking/CardArt.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {makeStyles} from '@material-ui/core'
3 |
4 | const useStyles = makeStyles(() => ({
5 | cardArt: {
6 | display: 'table-row',
7 | '& img': {
8 | maxWidth: 100,
9 | maxHeight: 100,
10 | display: 'table-cell',
11 | verticalAlign: 'middle',
12 | paddingRight: 5
13 | },
14 | '& span': {
15 | display: 'table-cell',
16 | verticalAlign: 'middle'
17 | }
18 | }
19 | }))
20 |
21 | function CardArt(props) {
22 | const {cardArt} = props
23 | const classes = useStyles()
24 | return (
25 |
26 | {!!cardArt.title && {cardArt.title}
}
27 |
33 |
34 | )
35 | }
36 |
37 | export default CardArt
--------------------------------------------------------------------------------
/src/utils/url.js:
--------------------------------------------------------------------------------
1 | const localhostDomainRE = /^https?:\/\/localhost[:?\d]*(?:[^:?\d]\S*)?$/
2 | const nonLocalhostDomainRE = /^https?:\/\/[^\s.]+\.\S{2,}$/
3 |
4 | export default function isUrl(s) {
5 | if (typeof s !== 'string') {
6 | return false;
7 | }
8 |
9 | const lowerCase = s.toLowerCase()
10 |
11 | return localhostDomainRE.test(lowerCase) ||
12 | nonLocalhostDomainRE.test(lowerCase);
13 | }
14 |
15 | function getLocation(url) {
16 | var l = document.createElement("a")
17 | l.href = url
18 | return l
19 | }
20 |
21 | export function normalise(url) {
22 | if (url.endsWith('/')) url = url.substr(0, url.length - 1)
23 | if (!url.endsWith('/cds-au/v1') && getLocation(url).hostname !== 'localhost') {
24 | url += '/cds-au/v1'
25 | }
26 | return url
27 | }
28 |
29 | export function encodeRFC3986URIComponent(str) {
30 | return encodeURIComponent(str).replace(
31 | /[!'()*]/g,
32 | (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/src/store/conout/actions.js:
--------------------------------------------------------------------------------
1 | export const CONSOLE_OUT = 'CONSOLE_OUT'
2 | export const CONSOLE_REFRESH = 'CONSOLE_REFRESH'
3 | export const CONSOLE_CLEAN = 'CONSOLE_CLEAN'
4 |
5 | export const conoutInfo = (txt, obj) => {
6 | return createLogEntry({lvl: 'log', txt, obj})
7 | }
8 |
9 | export const conoutTrace = (txt, obj) => {
10 | return createLogEntry({lvl: 'trace', txt, obj})
11 | }
12 |
13 | export const conoutWarn = (txt, obj) => {
14 | return createLogEntry({lvl: 'warn', txt, obj})
15 | }
16 |
17 | export const conoutError = (txt, obj) => {
18 | return createLogEntry({lvl: 'error', txt, obj})
19 | }
20 |
21 | export const conoutHtmlError = (txt, html, obj) => {
22 | return createLogEntry({lvl: 'error', txt, html, obj})
23 | }
24 |
25 | function createLogEntry(payload) {
26 | return {
27 | type: CONSOLE_OUT,
28 | payload,
29 | timestamp: new Date()
30 | }
31 | }
32 |
33 | export const refreshConout = () => ({
34 | type: CONSOLE_REFRESH
35 | })
36 |
37 | export const cleanConout = () => ({
38 | type: CONSOLE_CLEAN
39 | })
40 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import './index.css'
4 | import Page from './components/Page'
5 | import * as serviceWorker from './serviceWorker'
6 | import { Provider as StoreProvider } from 'react-redux'
7 | import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'
8 | import blue from '@material-ui/core/colors/blue'
9 | import store from './store'
10 |
11 | const theme = createMuiTheme({
12 | palette: {
13 | primary: blue
14 | },
15 | typography: {
16 | useNextVariants: true,
17 | },
18 | })
19 |
20 | const App = () => {
21 | return
22 |
23 |
24 |
25 |
26 | }
27 |
28 | ReactDOM.render( , document.getElementById('root'))
29 |
30 | // If you want your app to work offline and load faster, you can change
31 | // unregister() to register() below. Note this comes with some pitfalls.
32 | // Learn more about service workers: https://bit.ly/CRA-PWA
33 | serviceWorker.unregister()
34 |
--------------------------------------------------------------------------------
/src/store/version-info/reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | LOAD_VERSION_INFO,
3 | SAVE_VERSION_INFO,
4 | SET_VERSIONS_EDITABLE,
5 | SET_VERSIONS_READ_ONLY
6 | } from './actions'
7 |
8 | export default function versionInfo(state={vHeaders: {xV: '4', xMinV: '1'}}, action) {
9 | const vHeaders = {
10 | xV: loadVersionField("x-v") || state.vHeaders.xV,
11 | xMinV: loadVersionField("x-min-v") || state.vHeaders.xMinV
12 | }
13 | switch (action.type) {
14 | case LOAD_VERSION_INFO:
15 | return {vHeaders}
16 | case SAVE_VERSION_INFO:
17 | const vi = action.versionInfo
18 | saveVersionField("x-v", vi.xV)
19 | saveVersionField("x-min-v", vi.xMinV)
20 | return {vHeaders: vi}
21 | case SET_VERSIONS_EDITABLE:
22 | return {editable: true, vHeaders}
23 | case SET_VERSIONS_READ_ONLY:
24 | return {vHeaders}
25 | default:
26 | return state
27 | }
28 | }
29 |
30 | function loadVersionField(vf) {
31 | return window.localStorage.getItem(vf)
32 | }
33 |
34 | function saveVersionField(vf, value) {
35 | window.localStorage.setItem(vf, value)
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/data/energy/Geography.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {makeStyles} from '@material-ui/core'
3 |
4 | const useStyles = makeStyles(() => ({
5 | sectionContent: {
6 | marginTop: 0,
7 | marginBottom: 0,
8 | paddingLeft: 20
9 | }
10 | }))
11 |
12 | const Geography = ({geography}) => {
13 | const classes = useStyles()
14 | const {excludedPostcodes, includedPostcodes, distributors} = geography
15 | return (
16 | <>
17 | {excludedPostcodes && excludedPostcodes.length > 0 && (
18 | Excluded Postcodes: {excludedPostcodes.join(', ')}
19 | )}
20 | {includedPostcodes && includedPostcodes.length > 0 && (
21 | Included Postcodes: {includedPostcodes.join(', ')}
22 | )}
23 | {distributors && (
24 | <>
25 | Distributors:
26 |
27 | {distributors.map((distributor, idx) => {distributor} )}
28 |
29 | >
30 | )}
31 | >
32 | )
33 | }
34 |
35 | export default Geography
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Consumer Data Standards Australia
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux'
2 | import thunk from 'redux-thunk'
3 | import promise from 'redux-promise-middleware'
4 | import { combineReducers } from 'redux'
5 | import conout from './conout/reducer'
6 | import dataSources from './data-source/reducer'
7 | import versionInfo from './version-info/reducer'
8 | import banking from './banking/data/reducer'
9 | import energy from './energy/data/reducer'
10 | import energySelection from './energy/selection/reducer'
11 | import discovery from './discovery/reducer'
12 | import aemoDiscovery from './aemo_discovery/reducer'
13 | import bankingSelection from './banking/selection/reducer'
14 | import bankingComparison from './banking/comparison/reducer'
15 | import energyComparison from './energy/comparison/reducer'
16 |
17 | const rootReducer = combineReducers({
18 | conout,
19 | dataSources,
20 | versionInfo,
21 | banking,
22 | energy,
23 | discovery,
24 | aemoDiscovery,
25 | bankingSelection,
26 | energySelection,
27 | bankingComparison,
28 | energyComparison
29 | })
30 |
31 | const middleWares = [thunk, promise]
32 | export default createStore(rootReducer, applyMiddleware(...middleWares))
33 |
--------------------------------------------------------------------------------
/src/components/data/energy/AdditionalInfo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {makeStyles} from '@material-ui/core'
3 | import ExternalLink from './ExternalLink'
4 |
5 | const useStyles = makeStyles(() => ({
6 | ul: {
7 | marginTop: 0,
8 | marginBottom: 0,
9 | paddingLeft: 20
10 | },
11 | tableCell: {
12 | marginTop: 0,
13 | marginBottom: 0,
14 | padding: 0
15 | }
16 | }))
17 |
18 | const AdditionalInfo = ({tableCell, additionalInfo}) => {
19 | const {overviewUri, termsUri, eligibilityUri, pricingUri, bundleUri} = additionalInfo
20 | const classes = useStyles()
21 | return (
22 |
23 | {!!overviewUri && Overview }
24 | {!!termsUri && Terms and Conditions }
25 | {!!eligibilityUri && Eligibility }
26 | {!!pricingUri && Pricing }
27 | {!!bundleUri && Bundle }
28 |
29 | )
30 | }
31 |
32 | export default AdditionalInfo
33 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Build React app and deploy to GitHub Pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | workflow_dispatch:
9 |
10 | jobs:
11 | deploy:
12 | runs-on: ubuntu-22.04
13 | permissions:
14 | contents: write
15 | concurrency:
16 | group: ${{ github.workflow }}-${{ github.ref }}
17 | steps:
18 | - uses: actions/checkout@v3
19 |
20 | - name: Setup Node
21 | uses: actions/setup-node@v3
22 | with:
23 | node-version: '14.18.1'
24 |
25 | - name: Cache dependencies
26 | uses: actions/cache@v3
27 | with:
28 | path: ~/.npm
29 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
30 | restore-keys: |
31 | ${{ runner.os }}-node-
32 |
33 | - name: Install Packages
34 | run: npm install
35 | - name: Build The Comparator
36 | run: npm run build
37 |
38 | - name: Deploy
39 | uses: peaceiris/actions-gh-pages@v3
40 | if: ${{ github.ref == 'refs/heads/master' }}
41 | with:
42 | github_token: ${{ secrets.GITHUB_TOKEN }}
43 | publish_dir: ./build
44 | force_orphan: true
45 |
--------------------------------------------------------------------------------
/src/components/data/energy/SingleRate.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { makeStyles } from '@material-ui/core'
3 | import Rate from './Rate'
4 | import Duration from '../Duration'
5 |
6 | const useStyles = makeStyles(() => ({
7 | sectionContent: {
8 | marginTop: 0,
9 | marginBottom: 0,
10 | paddingLeft: 20
11 | }
12 | }))
13 |
14 | const SingleRate = ({singleRate}) => {
15 | const classes = useStyles()
16 | const {displayName, description, dailySupplyCharge, generalUnitPrice, rates, period} = singleRate
17 | return (
18 | <>
19 | Display Name: {displayName}
20 | {description && (
21 | Description: {description}
22 | )}
23 | {dailySupplyCharge && (
24 | Daily Supply Charge: ${dailySupplyCharge} (exclusive of GST)
25 | )}
26 | {generalUnitPrice && (
27 | General Unit Price: ${generalUnitPrice} (exclusive of GST)
28 | )}
29 | Rates:
30 |
31 | {rates.map((rate, index) => )}
32 |
33 | Period:
34 | >
35 | )
36 | }
37 |
38 | export default SingleRate
39 |
--------------------------------------------------------------------------------
/src/components/data/energy/EnergyPlanDiscount.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import DateTime from '../DateTime'
3 |
4 | const EnergyPlanDiscount = ({discount}) => {
5 | const {displayName, description, type, category, endDate, methodUType, percentOfBill, percentOfUse, fixedAmount, percentOverThreshold} = discount
6 | return (
7 |
8 | Display Name: {displayName}
9 | {description && (
10 | Description: {description}
11 | )}
12 | Type: {type}
13 | {category && (
14 | Category: {category}
15 | )}
16 | {endDate && End Date:
}
17 | Method: {methodUType}
18 | {percentOfBill && (
19 | Percent Of Bill: {(percentOfBill.rate * 100).toFixed(2)}%
20 | )}
21 | {percentOfUse && (
22 | Percent Of Use: {(percentOfUse.rate * 100).toFixed(2)}%
23 | )}
24 | {fixedAmount && (
25 | Fixed Amount: ${fixedAmount.amount}
26 | )}
27 | {percentOverThreshold && (
28 | Percent Over Threshold (${percentOverThreshold.usageAmount}): {(percentOverThreshold.rate * 100).toFixed(2)}%
29 | )}
30 |
31 | )
32 | }
33 |
34 | export default EnergyPlanDiscount
35 |
--------------------------------------------------------------------------------
/src/components/data/banking/DiscountEligibility.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {translateEligibilityType} from '../../../utils/dict'
3 |
4 | const DiscountEligibility = (props) => {
5 | const {discountEligibilityType, additionalValue, additionalInfo, additionalInfoUri} = props.eligibility
6 | const additionalInfoDescription = discountEligibilityType === 'OTHER' && - {additionalInfo}
7 | return (
8 |
9 |
10 | {translateEligibilityType(discountEligibilityType)}
11 | {
12 | ( discountEligibilityType === 'MIN_AGE' ||
13 | discountEligibilityType === 'MAX_AGE' ||
14 | discountEligibilityType === 'EMPLOYMENT_STATUS' ||
15 | discountEligibilityType === 'RESIDENCY_STATUS') ?
16 | - {additionalValue} : additionalInfoDescription
17 | }
18 | {
19 | ( discountEligibilityType === 'PENSION_RECIPIENT' ||
20 | discountEligibilityType === 'STUDENT') && !!additionalValue &&
21 | - {additionalValue}
22 | }
23 | {(discountEligibilityType === 'MIN_INCOME' || discountEligibilityType === 'MIN_TURNOVER') && - ${additionalValue} }
24 |
25 | {!additionalInfoDescription && !!additionalInfo && {additionalInfo}
}
26 | {!!additionalInfoUri && }
27 |
28 | )
29 | }
30 |
31 | export default DiscountEligibility
32 |
--------------------------------------------------------------------------------
/src/components/data/energy/EnergyPlanGreenPowerCharge.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { makeStyles } from '@material-ui/core'
3 |
4 | const useStyles = makeStyles(() => ({
5 | sectionTitle: {
6 | fontStyle: 'italic'
7 | },
8 | sectionContent: {
9 | marginTop: 0,
10 | marginBottom: 0,
11 | paddingLeft: 20
12 | }
13 | }))
14 |
15 | const EnergyPlanGreenPowerCharge = ({greenPowerCharge}) => {
16 | const classes = useStyles()
17 | const {displayName, description, scheme, type, tiers} = greenPowerCharge
18 | return (
19 |
20 | Display Name: {displayName}
21 | {description && (
22 | Description: {description}
23 | )}
24 | Scheme: {scheme}
25 | Type: {type}
26 | <>
27 | Tiers:
28 |
43 | >
44 |
45 | )
46 | }
47 |
48 | export default EnergyPlanGreenPowerCharge
49 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "homepage": "https://consumerdatastandardsaustralia.github.io/product-comparator-demo/",
3 | "name": "product-comparator-demo",
4 | "version": "0.1.2",
5 | "private": true,
6 | "resolutions": {
7 | "lodash.template": "^4.5.0",
8 | "path-parse": "^1.0.7",
9 | "ssri": "^8.0.1",
10 | "caniuse-lite": "1.0.30001632",
11 | "immer": "^8.0.1"
12 | },
13 | "overrides": {
14 | "caniuse-lite": "1.0.30001632"
15 | },
16 | "dependencies": {
17 | "@material-ui/core": "^4.9.10",
18 | "@material-ui/icons": "^4.2.1",
19 | "@material-ui/lab": "^4.0.0-alpha.56",
20 | "caniuse-lite": "^1.0.30001632",
21 | "clsx": "^1.0.4",
22 | "gh-pages": "^2.0.1",
23 | "iso8601-duration": "^1.2.0",
24 | "lodash": "^4.17.20",
25 | "moment": "^2.24.0",
26 | "pluralize": "^8.0.0",
27 | "react": "^16.8.6",
28 | "react-dom": "^16.8.6",
29 | "react-redux": "^7.1.0",
30 | "react-scripts": "^4.0.1",
31 | "redux": "^4.0.1",
32 | "redux-logger": "^3.0.6",
33 | "redux-promise-middleware": "^6.1.1",
34 | "redux-thunk": "^2.3.0"
35 | },
36 | "optionalDependencies": {
37 | "fsevents": "^2.3.2"
38 | },
39 | "scripts": {
40 | "preinstall": "npx npm-force-resolutions",
41 | "start": "react-scripts start",
42 | "build": "react-scripts build",
43 | "test": "react-scripts test",
44 | "eject": "react-scripts eject",
45 | "deploy": "gh-pages -d build"
46 | },
47 | "eslintConfig": {
48 | "extends": "react-app"
49 | },
50 | "browserslist": [
51 | ">0.2%",
52 | "not dead",
53 | "not op_mini all"
54 | ]
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/data/energy/ControlledLoad.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { makeStyles } from '@material-ui/core'
3 | import DateTime from '../DateTime'
4 | import SingleRate from './SingleRate'
5 | import TimeOfUseRate from './TimeOfUseRate'
6 |
7 | const useStyles = makeStyles(() => ({
8 | sectionTitle: {
9 | fontStyle: 'italic'
10 | },
11 | sectionContent: {
12 | marginTop: 0,
13 | marginBottom: 0,
14 | paddingLeft: 20
15 | }
16 | }))
17 |
18 | const ControlledLoad = ({controlledLoad}) => {
19 | const classes = useStyles()
20 | const {displayName, rateBlockUType, startDate, endDate, singleRate, timeOfUseRates} = controlledLoad
21 | return (
22 |
23 | Display Name: {displayName}
24 | Rate Type: {rateBlockUType}
25 | {startDate && Start Date:
}
26 | {endDate && End Date:
}
27 | {singleRate && (
28 | <>
29 | Single Rate:
30 |
31 |
32 |
33 | >
34 | )}
35 | {timeOfUseRates && (
36 | <>
37 | Time Of Use Rates:
38 |
39 | {timeOfUseRates.map((timeOfUseRate, index) => )}
40 |
41 | >
42 | )}
43 |
44 | )
45 | }
46 |
47 | export default ControlledLoad
48 |
--------------------------------------------------------------------------------
/src/store/energy/data/reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | START_RETRIEVE_PLAN_LIST,
3 | RETRIEVE_PLAN_LIST,
4 | RETRIEVE_PLAN_DETAIL,
5 | CLEAR_DATA
6 | } from './actions'
7 | import {fulfilled} from '../../../utils/async-actions'
8 |
9 | export default function energy(state = [], action) {
10 | switch (action.type) {
11 | case START_RETRIEVE_PLAN_LIST: {
12 | const s = [...state]
13 | const {idx} = action.payload
14 | const item = s[idx]
15 | if (item) {
16 | item.progress = action.type
17 | } else {
18 | s[idx] = {
19 | progress: action.type,
20 | detailRecords: 0,
21 | failedDetailRecords: 0,
22 | plans: {}
23 | }
24 | }
25 | return s
26 | }
27 | case fulfilled(RETRIEVE_PLAN_LIST): {
28 | const s = [...state]
29 | const {idx, response} = action.payload
30 | const item = s[idx]
31 | item.progress = action.type
32 | item.totalRecords = response.meta.totalRecords
33 | response.data.plans.forEach(plan => item.plans[plan.planId] = plan)
34 | return s
35 | }
36 | case fulfilled(RETRIEVE_PLAN_DETAIL): {
37 | const s = [...state]
38 | const {idx, response} = action.payload
39 | const item = s[idx]
40 | if (response) {
41 | item.plans[response.data.planId] = response.data
42 | item.detailRecords++
43 | } else {
44 | item.failedDetailRecords++
45 | }
46 | return s
47 | }
48 | case CLEAR_DATA: {
49 | const s = [...state]
50 | s[action.payload] = null
51 | return s
52 | }
53 | default:
54 | return state
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/data/energy/EnergyPlanList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {connect} from 'react-redux'
3 | import {START_RETRIEVE_PLAN_LIST} from '../../../store/energy/data'
4 | import LinearProgress from '@material-ui/core/LinearProgress'
5 | import Plan from './Plan'
6 |
7 | class EnergyPlanList extends React.Component {
8 | render() {
9 | const { dataSourceIndex } = this.props
10 | let planList = this.props.planList[dataSourceIndex];
11 | planList = !!planList ? planList : {}
12 | const { progress, totalRecords, detailRecords, failedDetailRecords, plans } = planList
13 | const processedRecords = detailRecords + failedDetailRecords
14 |
15 | return (
16 |
17 | {
18 | !!totalRecords && (processedRecords < totalRecords) &&
19 |
20 | }
21 | {
22 | progress === START_RETRIEVE_PLAN_LIST &&
Getting all current plans...
23 | }
24 | {
25 | processedRecords < totalRecords &&
Getting plan details...
26 | }
27 | {
28 | !!plans && processedRecords >= totalRecords && Object.values(plans).map((plan, index) => (
29 |
30 | ))
31 | }
32 |
33 | )
34 | }
35 | }
36 |
37 | const mapStateToProps = state => ({
38 | planList: state.energy
39 | })
40 |
41 | const mapDispatchToProps = {}
42 |
43 | export default connect(mapStateToProps, mapDispatchToProps)(EnergyPlanList)
44 |
--------------------------------------------------------------------------------
/src/components/data/banking/RateTier.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import RateCondition from './RateCondition'
3 | import RateSubTier from './RateSubTier'
4 | import {translateRateApplicationMethod, translateUnitOfMeasure} from '../../../utils/dict'
5 | import {makeStyles} from '@material-ui/core'
6 |
7 | const useStyles = makeStyles(() => ({
8 | sectionTitle: {
9 | fontStyle: 'italic'
10 | },
11 | sectionContent: {
12 | paddingLeft: 20
13 | }
14 | }))
15 |
16 | const RateTier = (props) => {
17 | const classes = useStyles()
18 | const {name, unitOfMeasure, minimumValue, maximumValue, rateApplicationMethod, applicabilityConditions, subTier,
19 | additionalInfo, additionalInfoUri} = props.tier
20 | return (
21 |
22 | {name}
23 | Minimum {minimumValue} {translateUnitOfMeasure(unitOfMeasure)}
24 | {!!maximumValue && Maximum {maximumValue} {translateUnitOfMeasure(unitOfMeasure)}
}
25 | {!!rateApplicationMethod && Applied on {translateRateApplicationMethod(rateApplicationMethod)}
}
26 | {!!applicabilityConditions && }
27 | {
28 | !!subTier &&
29 |
30 |
Sub Tier:
31 |
32 |
33 |
34 |
35 | }
36 | {!!additionalInfo && {additionalInfo}
}
37 | {!!additionalInfoUri && }
38 |
39 | )
40 | }
41 |
42 | export default RateTier
43 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
4 | background-color: lightblue;
5 | -webkit-background-size: cover;
6 | -moz-background-size: cover;
7 | -o-background-size: cover;
8 | background-size: cover;
9 | -webkit-font-smoothing: antialiased;
10 | -moz-osx-font-smoothing: grayscale;
11 | }
12 |
13 | a {
14 | text-decoration: none;
15 | }
16 |
17 | .title {
18 | height: 100px;
19 | display: table-row;
20 | }
21 |
22 | .title span {
23 | display: table-cell;
24 | vertical-align: middle;
25 | }
26 |
27 | .title img {
28 | max-width: 100px;
29 | max-height: 100px;
30 | vertical-align: middle;
31 | padding-right: 5px;
32 | }
33 |
34 | .title h2 {
35 | display: table-cell;
36 | vertical-align: middle;
37 | padding-bottom: 6px;
38 | }
39 |
40 | .tree-element {
41 | margin: 0 0 0 4px;
42 | position: relative;
43 | }
44 |
45 | .tree-element.parent {
46 | display: inline;
47 | }
48 |
49 | .tree-element.child {
50 | margin-left: 16px;
51 | }
52 |
53 | div.tree-element:before {
54 | content: '';
55 | position: absolute;
56 | top: 24px;
57 | left: 1px;
58 | height: calc(100% - 48px);
59 | border-left: 1px solid gray;
60 | }
61 |
62 | p.tree-element {
63 | margin-left: 16px;
64 | }
65 |
66 | .tree-toggler {
67 | position: absolute;
68 | top: 5px;
69 | left: 0px;
70 | width: 0;
71 | height: 0;
72 | border-top: 4px solid transparent;
73 | border-bottom: 4px solid transparent;
74 | border-left: 5px solid gray;
75 | cursor: pointer;
76 | }
77 |
78 | .tree-toggler.open {
79 | transform: rotate(90deg);
80 | }
81 |
82 | .collapsed {
83 | display: none;
84 | }
85 |
--------------------------------------------------------------------------------
/src/store/banking/data/reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | START_RETRIEVE_PRODUCT_LIST,
3 | RETRIEVE_PRODUCT_LIST,
4 | RETRIEVE_PRODUCT_DETAIL,
5 | DELETE_DATA,
6 | CLEAR_DATA
7 | } from './actions'
8 | import {fulfilled} from '../../../utils/async-actions'
9 |
10 | export default function banking(state = [], action) {
11 | switch (action.type) {
12 | case START_RETRIEVE_PRODUCT_LIST: {
13 | const s = [...state]
14 | const {idx} = action.payload
15 | const item = s[idx]
16 | if (item) {
17 | item.progress = action.type
18 | } else {
19 | s[idx] = {
20 | progress: action.type,
21 | detailRecords: 0,
22 | failedDetailRecords: 0,
23 | products: [],
24 | productDetails: []
25 | }
26 | }
27 | return s
28 | }
29 | case fulfilled(RETRIEVE_PRODUCT_LIST): {
30 | const s = [...state]
31 | const {idx, response} = action.payload
32 | const item = s[idx]
33 | item.progress = action.type
34 | item.totalRecords = response.meta.totalRecords
35 | item.products = [...item.products, ...response.data.products]
36 | return s
37 | }
38 | case fulfilled(RETRIEVE_PRODUCT_DETAIL): {
39 | const s = [...state]
40 | const {idx, response} = action.payload
41 | const item = s[idx]
42 | if (response) {
43 | item.productDetails.push(response.data)
44 | item.detailRecords++
45 | } else {
46 | item.failedDetailRecords++
47 | }
48 | return s
49 | }
50 | case DELETE_DATA:
51 | case CLEAR_DATA: {
52 | const s = [...state]
53 | s[action.payload] = null
54 | return s
55 | }
56 | default:
57 | return state
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/data/banking/Eligibility.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {translateEligibilityType} from '../../../utils/dict'
3 |
4 | const Eligibility = (props) => {
5 | const {eligibilityType, additionalValue, additionalInfo, additionalInfoUri} = props.eligibility
6 | return (
7 |
8 |
9 | {translateEligibilityType(eligibilityType)}
10 | {eligibilityType === 'OTHER' &&
- {additionalInfo} }
11 | {
12 | ( eligibilityType === 'MIN_AGE' ||
13 | eligibilityType === 'MAX_AGE' ||
14 | eligibilityType === 'EMPLOYMENT_STATUS' ||
15 | eligibilityType === 'RESIDENCY_STATUS') &&
16 |
- {additionalValue}
17 | }
18 | {
19 | ( eligibilityType === 'BUSINESS' ||
20 | eligibilityType === 'PENSION_RECIPIENT' ||
21 | eligibilityType === 'STAFF' ||
22 | eligibilityType === 'STUDENT' ||
23 | eligibilityType === 'NATURAL_PERSON') && !!additionalValue &&
24 |
- {additionalValue}
25 | }
26 | {
27 | ( eligibilityType === 'BUSINESS' ||
28 | eligibilityType === 'PENSION_RECIPIENT' ||
29 | eligibilityType === 'STAFF' ||
30 | eligibilityType === 'STUDENT' ||
31 | eligibilityType === 'NATURAL_PERSON') && !!additionalInfo &&
32 |
{additionalInfo}
33 | }
34 | {(eligibilityType === 'MIN_INCOME' || eligibilityType === 'MIN_TURNOVER') &&
- ${additionalValue} }
35 |
36 | {!!additionalInfoUri && }
37 |
38 | )
39 | }
40 |
41 | export default Eligibility
42 |
--------------------------------------------------------------------------------
/src/components/data/discovery/StatusOutages.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import DateTime from '../DateTime'
3 | import Duration from '../Duration'
4 | import { connect } from 'react-redux'
5 | import { translateDiscoveryStatus } from '../../../utils/dict'
6 |
7 | class StatusOutages extends React.Component {
8 |
9 | render() {
10 | const { statusDetails, outagesDetails } = this.props
11 | return (
12 | <>
13 | {!!statusDetails &&
14 | <>
15 | Status
16 |
17 |
Status: {translateDiscoveryStatus(statusDetails.status)}{!!statusDetails.explanation && - {statusDetails.explanation} }
18 | {!!statusDetails.detectionTime &&
Detection Time:
}
19 | {!!statusDetails.expectedResolutionTime &&
Expected Resolution Time:
}
20 | {!!statusDetails.updateTime &&
Update Time:
}
21 |
22 | >
23 | }
24 |
25 | {!!outagesDetails && !!outagesDetails.outages && !!outagesDetails.outages.length &&
26 | <>
27 | Scheduled Outages
28 |
29 | {outagesDetails.outages.map((outage, index) =>
30 |
31 | )}
32 |
33 | >
34 | }
35 | >
36 | )
37 | }
38 | }
39 |
40 | const Outage = props => {
41 | const { outage } = props
42 | return (
43 |
44 | Outage Time:
45 | {!!outage.duration && Planned Duration:
}
46 | {!!outage.isPartial && Partial: {outage.isPartial}
}
47 | «{outage.explanation}»
48 |
49 | )
50 | }
51 |
52 | const mapStateToProps = state => ({
53 | })
54 |
55 | const mapDispatchToProps = {
56 | }
57 |
58 | export default connect(mapStateToProps, mapDispatchToProps)(StatusOutages)
--------------------------------------------------------------------------------
/src/store/data-source/reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | ENABLE_DATA_SOURCE,
3 | LOAD_DATA_SOURCE,
4 | ADD_DATA_SOURCE,
5 | SYNC_DATA_SOURCES,
6 | SAVE_DATA_SOURCE,
7 | DELETE_DATA_SOURCE,
8 | MODIFY_DATA_SOURCE_NAME,
9 | MODIFY_DATA_SOURCE_ICON,
10 | MODIFY_DATA_SOURCE_ENERGY_PRD_URL,
11 | MODIFY_DATA_SOURCE_URL
12 | } from './actions'
13 | import {fulfilled} from '../../utils/async-actions'
14 |
15 | export default function dataSources(state=[], action) {
16 | switch (action.type) {
17 | case fulfilled(LOAD_DATA_SOURCE):
18 | return action.payload
19 | case ADD_DATA_SOURCE:
20 | return [...state, {name: '', url: ''}]
21 | case fulfilled(SYNC_DATA_SOURCES):
22 | persistSavedDataSources(action.payload)
23 | return action.payload
24 | case SAVE_DATA_SOURCE: {
25 | const dataSources = state.map((dataSource, index) => (index === action.index ? {...action.payload, unsaved: false} : dataSource))
26 | persistSavedDataSources(dataSources)
27 | return dataSources
28 | }
29 | case DELETE_DATA_SOURCE: {
30 | const dataSources = [...state]
31 | dataSources[action.index].deleted = true
32 | persistSavedDataSources(dataSources)
33 | return dataSources
34 | }
35 | case MODIFY_DATA_SOURCE_NAME:
36 | case MODIFY_DATA_SOURCE_ICON:
37 | case ENABLE_DATA_SOURCE: {
38 | const dataSources = state.map((dataSource, index) => (index === action.index ? {...action.payload} : dataSource))
39 | persistSavedDataSources(dataSources)
40 | return dataSources
41 | }
42 | case MODIFY_DATA_SOURCE_URL:
43 | case MODIFY_DATA_SOURCE_ENERGY_PRD_URL: {
44 | const dataSources = state.map((dataSource, index) => (index === action.index ? {...action.payload, unsaved: true} : dataSource))
45 | persistSavedDataSources(dataSources)
46 | return dataSources
47 | }
48 | default:
49 | return state
50 | }
51 | }
52 |
53 | function persistSavedDataSources(dataSources) {
54 | window.localStorage.setItem("cds-banking-prd-ds",
55 | JSON.stringify(dataSources.filter(dataSource => !dataSource.unsaved && !dataSource.deleted)))
56 | }
57 |
--------------------------------------------------------------------------------
/src/store/aemo_discovery/actions.js:
--------------------------------------------------------------------------------
1 | import {conoutInfo} from '../conout/actions'
2 | import {createConoutError, checkExposedHeaders} from '../../utils/cors'
3 |
4 | const AEMO_URL = 'https://api.aemo.com.au/NEMRetail/cds-au/v1'
5 |
6 | export const RETRIEVE_AEMO_STATUS = 'RETRIEVE_AEMO_STATUS'
7 | export const RETRIEVE_AEMO_OUTAGES = 'RETRIEVE_AEMO_OUTAGES'
8 |
9 | const headers = {
10 | 'Accept': 'application/json',
11 | 'x-v': 1
12 | }
13 |
14 | export const retrieveStatus = () => dispatch => {
15 | const fullUrl = AEMO_URL + '/discovery/status'
16 | const request = new Request(fullUrl, {headers})
17 | dispatch(conoutInfo('Requesting retrieveStatus(): ' + fullUrl))
18 | dispatch({
19 | type: RETRIEVE_AEMO_STATUS,
20 | payload: fetch(request)
21 | .then(response => {
22 | if (response.ok) {
23 | checkExposedHeaders(response, fullUrl, dispatch)
24 | return response.json()
25 | }
26 | throw new Error(`Response not OK. Status: ${response.status} (${response.statusText})`)
27 | })
28 | .then(obj => {
29 | dispatch(conoutInfo(`Received response for ${fullUrl}:`, obj))
30 | return obj
31 | })
32 | .catch(error => {
33 | dispatch(createConoutError(error, fullUrl))
34 | })
35 | })
36 | }
37 |
38 | export const retrieveOutages = () => dispatch => {
39 | const fullUrl = AEMO_URL + '/discovery/outages'
40 | const request = new Request(fullUrl, {headers})
41 | dispatch(conoutInfo('Requesting retrieveOutages(): ' + fullUrl))
42 | dispatch({
43 | type: RETRIEVE_AEMO_OUTAGES,
44 | payload: fetch(request)
45 | .then(response => {
46 | if (response.ok) {
47 | checkExposedHeaders(response, fullUrl, dispatch)
48 | return response.json()
49 | }
50 | throw new Error(`Response not OK. Status: ${response.status} (${response.statusText})`)
51 | })
52 | .then(obj => {
53 | dispatch(conoutInfo(`Received response for ${fullUrl}:`, obj))
54 | return obj
55 | })
56 | .catch(error => {
57 | dispatch(createConoutError(error, fullUrl))
58 | })
59 | })
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/data/banking/AdditionalInfo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {makeStyles} from '@material-ui/core'
3 | import AdditionalInformationUris from './AdditionalInformationUris'
4 |
5 | const useStyles = makeStyles(() => ({
6 | ul: {
7 | marginTop: 0,
8 | marginBottom: 0,
9 | paddingLeft: 20
10 | },
11 | tableCell: {
12 | marginTop: 0,
13 | marginBottom: 0,
14 | padding: 0
15 | }
16 | }))
17 |
18 | const AdditionalInfo = (props) => {
19 | const {tableCell} = props
20 | const {overviewUri, termsUri, eligibilityUri, feesAndPricingUri, bundleUri,
21 | additionalOverviewUris, additionalTermsUris, additionalEligibilityUris, additionalFeesAndPricingUris, additionalBundleUris} = props.additionalInfo
22 | const classes = useStyles()
23 | return (
24 |
25 | {!!overviewUri && Overview }
26 | {!!termsUri && Terms }
27 | {!!eligibilityUri && Eligibility }
28 | {!!feesAndPricingUri && Fee and Pricing }
29 | {!!bundleUri && Bundle }
30 | {!!additionalOverviewUris && }
31 | {!!additionalTermsUris && }
32 | {!!additionalEligibilityUris && }
33 | {!!additionalFeesAndPricingUris && }
34 | {!!additionalBundleUris && }
35 |
36 | )
37 | }
38 |
39 | export default AdditionalInfo
40 |
--------------------------------------------------------------------------------
/src/components/Page.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import DataSourcePanel from './data-source/DataSourcePanel'
3 | import BankingPanel from './data/banking/BankingPanel'
4 | import EnergyPanel from './data/energy/EnergyPanel'
5 | import ConsolePanel from './data/ConsolePanel'
6 | import Header from './header'
7 | import { Container } from '@material-ui/core'
8 | import { makeStyles } from '@material-ui/core/styles'
9 | import BankingComparisonPanel from './comparison/BankingComparisonPanel'
10 | import EnergyComparisonPanel from './comparison/EnergyComparisonPanel'
11 | import AppBar from '@material-ui/core/AppBar'
12 | import Tabs from '@material-ui/core/Tabs'
13 | import Tab from '@material-ui/core/Tab'
14 | import DiscoveryInfo from './data/discovery/DiscoveryInfo'
15 | import AEMODiscoveryInfo from './data/discovery/AEMODiscoveryInfo'
16 |
17 | const useStyles = makeStyles(theme => ({
18 | hidden: {
19 | display: 'none'
20 | }
21 | }))
22 |
23 | function Page() {
24 | const [value, setValue] = React.useState(0)
25 | const classes = useStyles()
26 |
27 | const handleChange = (event, newValue) => {
28 | setValue(newValue);
29 | };
30 |
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
58 |
59 | );
60 | }
61 |
62 | export default Page;
63 |
--------------------------------------------------------------------------------
/src/store/discovery/actions.js:
--------------------------------------------------------------------------------
1 | import {conoutInfo} from '../conout/actions'
2 | import {createConoutError, checkExposedHeaders} from '../../utils/cors'
3 |
4 | export const RETRIEVE_STATUS = 'RETRIEVE_STATUS'
5 | export const RETRIEVE_OUTAGES = 'RETRIEVE_OUTAGES'
6 |
7 | const headers = {
8 | 'Accept': 'application/json'
9 | }
10 |
11 | export const retrieveStatus = (dataSourceIdx, url, xV, xMinV) => dispatch => {
12 | const fullUrl = url + '/discovery/status'
13 | const request = new Request(fullUrl, {
14 | headers: new Headers({...headers, 'x-v': xV, 'x-min-v': xMinV})
15 | })
16 | dispatch(conoutInfo('Requesting retrieveStatus(): ' + fullUrl))
17 | dispatch({
18 | type: RETRIEVE_STATUS,
19 | payload: fetch(request)
20 | .then(response => {
21 | if (response.ok) {
22 | checkExposedHeaders(response, fullUrl, dispatch)
23 | return response.json()
24 | }
25 | throw new Error(`Response not OK. Status: ${response.status} (${response.statusText})`)
26 | })
27 | .then(obj => {
28 | dispatch(conoutInfo(`Received response for ${fullUrl}:`, obj))
29 | return {idx: dataSourceIdx, response: obj}
30 | })
31 | .catch(error => {
32 | dispatch(createConoutError(error, fullUrl))
33 | })
34 | })
35 | }
36 |
37 | export const retrieveOutages = (dataSourceIdx, url, xV, xMinV) => dispatch => {
38 | const fullUrl = url + '/discovery/outages'
39 | const request = new Request(fullUrl, {
40 | headers: new Headers({...headers, 'x-v': xV, 'x-min-v': xMinV})
41 | })
42 | dispatch(conoutInfo('Requesting retrieveOutages(): ' + fullUrl))
43 | dispatch({
44 | type: RETRIEVE_OUTAGES,
45 | payload: fetch(request)
46 | .then(response => {
47 | if (response.ok) {
48 | checkExposedHeaders(response, fullUrl, dispatch)
49 | return response.json()
50 | }
51 | throw new Error(`Response not OK. Status: ${response.status} (${response.statusText})`)
52 | })
53 | .then(obj => {
54 | dispatch(conoutInfo(`Received response for ${fullUrl}:`, obj))
55 | return {idx: dataSourceIdx, response: obj}
56 | })
57 | .catch(error => {
58 | dispatch(createConoutError(error, fullUrl))
59 | })
60 | })
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/data/energy/TimeOfUseRate.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { makeStyles } from '@material-ui/core'
3 | import Rate from './Rate'
4 | import Duration from '../Duration'
5 |
6 | const useStyles = makeStyles(() => ({
7 | sectionTitle: {
8 | fontStyle: 'italic'
9 | },
10 | sectionContent: {
11 | marginTop: 0,
12 | marginBottom: 0,
13 | paddingLeft: 20
14 | }
15 | }))
16 |
17 | const TimeOfUseRate = ({timeOfUseRate}) => {
18 | const classes = useStyles()
19 | const {displayName, description, dailySupplyCharge, rates, period, timeOfUse, type} = timeOfUseRate
20 | return (
21 |
22 | Display Name: {displayName}
23 | {description && (
24 | Description: {description}
25 | )}
26 | {dailySupplyCharge && (
27 | Daily Supply Charge: ${dailySupplyCharge} (exclusive of GST)
28 | )}
29 |
30 |
Rates:
31 |
32 | {rates.map((rate, index) => )}
33 |
34 |
35 | Period:
36 | Time Of Use:
37 |
58 | Type: {type}
59 |
60 | )
61 | }
62 |
63 | export default TimeOfUseRate
64 |
--------------------------------------------------------------------------------
/src/components/data/banking/DepositRate.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import RateTier from './RateTier'
3 | import Duration from '../Duration'
4 | import {translateDepositRateType} from '../../../utils/dict'
5 | import {makeStyles} from '@material-ui/core'
6 | import ecomp from '../../../utils/enum-comp'
7 |
8 | const useStyles = makeStyles(() => ({
9 | sectionTitle: {
10 | fontStyle: 'italic'
11 | },
12 | sectionContent: {
13 | marginTop: 0,
14 | marginBottom: 0,
15 | paddingLeft: 20
16 | }
17 | }))
18 |
19 | const DepositRate = (props) => {
20 | const classes = useStyles()
21 | const {
22 | rate,
23 | depositRateType,
24 | calculationFrequency,
25 | applicationFrequency,
26 | tiers,
27 | additionalValue,
28 | additionalInfo,
29 | additionalInfoUri
30 | } = props.depositRate
31 | return (
32 |
33 | {(rate * 100).toFixed(2)}%
34 |
35 | {translateDepositRateType(depositRateType)}
36 | {
37 | (depositRateType === 'FIXED' || depositRateType === 'INTRODUCTORY') && !!additionalValue &&
38 | -
39 | }
40 | {
41 | ( depositRateType === 'BONUS' ||
42 | depositRateType === 'BUNDLE_BONUS' ||
43 | depositRateType === 'FLOATING' ||
44 | depositRateType === 'MARKET_LINKED') &&
45 | - {additionalValue}
46 | }
47 |
48 | {!!calculationFrequency && Calculated
}
49 | {!!applicationFrequency && Applied
}
50 | {
51 | !!tiers && tiers.length > 0 &&
52 |
53 |
Rate Tiers:
54 |
55 | {tiers.sort((a, b)=>ecomp(a.name, b.name)).map((tier, index) => )}
56 |
57 |
58 | }
59 | {!!additionalInfo && {additionalInfo}
}
60 | {!!additionalInfoUri && }
61 |
62 | )
63 | }
64 |
65 | export default DepositRate
66 |
--------------------------------------------------------------------------------
/src/components/data/banking/Fee.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import FeeDiscount from './FeeDiscount'
3 | import Duration from '../Duration'
4 | import {translateFeeType} from '../../../utils/dict'
5 | import {makeStyles} from '@material-ui/core'
6 | import ecomp from '../../../utils/enum-comp'
7 | import {isDuration} from '../../../utils/datetime'
8 |
9 | const useStyles = makeStyles(() => ({
10 | sectionTitle: {
11 | fontStyle: 'italic'
12 | },
13 | sectionContent: {
14 | marginTop: 0,
15 | marginBottom: 0,
16 | paddingLeft: 20
17 | }
18 | }))
19 |
20 | const Fee = (props) => {
21 | const classes = useStyles()
22 | const {
23 | name,
24 | feeType,
25 | amount,
26 | balanceRate,
27 | transactionRate,
28 | accruedRate,
29 | accrualFrequency,
30 | currency,
31 | additionalValue,
32 | additionalInfo,
33 | additionalInfoUri,
34 | discounts
35 | } = props.fee
36 | return (
37 |
38 |
39 | {name}
40 | {!!amount && - ${amount} }
41 | {!!balanceRate && - {(balanceRate * 100).toFixed(2)}% }
42 | {!!transactionRate && - {(transactionRate * 100).toFixed(2)}% }
43 | {!!accruedRate && - {(accruedRate * 100).toFixed(2)}% }
44 | {!!accrualFrequency && - }
45 |
46 |
47 | Fee Type - {translateFeeType(feeType)}
48 | {feeType === 'PERIODIC' && - }
49 |
50 |
51 | {!!currency && Currency - {currency}
}
52 | {
53 | feeType !== 'PERIODIC' && !!additionalValue &&
54 |
55 | {isDuration(additionalValue) ? <>> : additionalValue}
56 |
}
57 | {!!additionalInfo && {additionalInfo}
}
58 | {!!additionalInfoUri && }
59 | {
60 | !!discounts && discounts.length > 0 &&
61 |
62 |
Discounts:
63 |
64 | {discounts.sort((a, b)=>ecomp(a.discountType, b.discountType)).map(
65 | (discount, index) =>)}
66 |
67 |
68 | }
69 |
70 | )
71 | }
72 |
73 | export default Fee
74 |
--------------------------------------------------------------------------------
/src/components/data/banking/FeeDiscount.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import DiscountEligibility from './DiscountEligibility'
3 | import {makeStyles} from '@material-ui/core'
4 | import {translateDiscountType} from '../../../utils/dict'
5 | import Duration from '../Duration'
6 | import ecomp from '../../../utils/enum-comp'
7 |
8 | const useStyles = makeStyles(() => ({
9 | sectionTitle: {
10 | fontStyle: 'italic'
11 | },
12 | sectionContent: {
13 | marginTop: 0,
14 | marginBottom: 0,
15 | paddingLeft: 20
16 | }
17 | }))
18 |
19 | const FeeDiscount = (props) => {
20 | const classes = useStyles()
21 | const {
22 | description,
23 | discountType,
24 | amount,
25 | balanceRate,
26 | transactionRate,
27 | accruedRate,
28 | feeRate,
29 | additionalValue,
30 | additionalInfo,
31 | additionalInfoUri,
32 | eligibility
33 | } = props.discount
34 | return (
35 |
36 | {!!amount && ${amount}
}
37 | {!!balanceRate && Balance rate: {(balanceRate * 100).toFixed(2)}%
}
38 | {!!transactionRate && Transaction rate: {(transactionRate * 100).toFixed(2)}%
}
39 | {!!accruedRate && Accrued rate: {(accruedRate * 100).toFixed(2)}%
}
40 | {!!feeRate && Fee rate: {(feeRate * 100).toFixed(2)}%
}
41 |
42 | Discount Type - {translateDiscountType(discountType)}
43 | {
44 | (discountType === 'BALANCE' ||
45 | discountType === 'DEPOSITS' ||
46 | discountType === 'PAYMENTS') &&
47 | - ${additionalValue}
48 | }
49 | {
50 | discountType === 'FEE_CAP' &&
51 | -
52 | }
53 |
54 | {description}
55 | {discountType === 'ELIGIBILITY_ONLY' && additionalValue && {additionalValue}
}
56 | {!!additionalInfo && {additionalInfo}
}
57 | {!!additionalInfoUri && }
58 | {
59 | !!eligibility && eligibility.length > 0 &&
60 |
61 |
Discount Eligibilities
62 |
63 | {eligibility.sort((a, b)=>ecomp(a.discountEligibilityType, b.discountEligibilityType)).map(
64 | (discountEligibility, index) => )}
65 |
66 |
67 | }
68 |
69 | )
70 | }
71 |
72 | export default FeeDiscount
73 |
--------------------------------------------------------------------------------
/src/components/data/banking/Feature.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {translateFeatureType} from '../../../utils/dict'
3 | import Duration from '../Duration'
4 |
5 | const Feature = (props) => {
6 | const {featureType, additionalValue, additionalInfo, additionalInfoUri} = props.feature
7 | return (
8 |
9 |
10 | {translateFeatureType(featureType)}
11 | {featureType === 'OTHER' &&
- {additionalInfo} }
12 | {
13 | ( featureType === 'CARD_ACCESS' ||
14 | featureType === 'FREE_TXNS' ||
15 | featureType === 'LOYALTY_PROGRAM' ||
16 | featureType === 'INSURANCE' ||
17 | featureType === 'DIGITAL_WALLET' ||
18 | featureType === 'COMPLEMENTARY_PRODUCT_DISCOUNTS' ||
19 | featureType === 'NOTIFICATIONS' ||
20 | featureType === 'BONUS_REWARDS' ) &&
21 |
- {additionalValue}
22 | }
23 | {
24 | ( featureType === 'ADDITIONAL_CARDS' ||
25 | featureType === 'UNLIMITED_TXNS' ||
26 | featureType === 'OFFSET' ||
27 | featureType === 'OVERDRAFT' ||
28 | featureType === 'REDRAW' ||
29 | featureType === 'BALANCE_TRANSFERS' ||
30 | featureType === 'DIGITAL_BANKING' ||
31 | featureType === 'NPP_PAYID' ||
32 | featureType === 'NPP_ENABLED' ||
33 | featureType === 'DONATE_INTEREST' ||
34 | featureType === 'BILL_PAYMENT') && !!additionalValue &&
35 |
- {additionalValue}
36 | }
37 | {
38 | ( featureType === 'ADDITIONAL_CARDS' ||
39 | featureType === 'UNLIMITED_TXNS' ||
40 | featureType === 'OFFSET' ||
41 | featureType === 'OVERDRAFT' ||
42 | featureType === 'REDRAW' ||
43 | featureType === 'BALANCE_TRANSFERS' ||
44 | featureType === 'DIGITAL_BANKING' ||
45 | featureType === 'NPP_PAYID' ||
46 | featureType === 'NPP_ENABLED' ||
47 | featureType === 'DONATE_INTEREST' ||
48 | featureType === 'BILL_PAYMENT') && !!additionalInfo &&
49 |
{additionalInfo}
50 | }
51 | {
52 | (featureType === 'INTEREST_FREE' || featureType === 'INTEREST_FREE_TRANSFERS') &&
53 |
-
54 | }
55 | {
56 | ( featureType === 'CASHBACK_OFFER' ||
57 | featureType === 'FREE_TXNS_ALLOWANCE') &&
58 |
- ${additionalValue}
59 | }
60 |
61 | {!!additionalInfoUri && }
62 |
63 | )
64 | }
65 |
66 | export default Feature
67 |
--------------------------------------------------------------------------------
/src/components/data/banking/LendingRate.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import RateTier from './RateTier'
3 | import Duration from '../Duration'
4 | import {translateInterestPaymentDue, translateLendingRateType, translateRepaymentType, translateloanPurpose} from '../../../utils/dict'
5 | import ecomp from '../../../utils/enum-comp'
6 | import {makeStyles} from '@material-ui/core'
7 |
8 | const useStyles = makeStyles(() => ({
9 | sectionTitle: {
10 | fontStyle: 'italic'
11 | },
12 | sectionContent: {
13 | marginTop: 0,
14 | marginBottom: 0,
15 | paddingLeft: 20
16 | }
17 | }))
18 |
19 | const LendingRate = (props) => {
20 | const classes = useStyles()
21 | const {
22 | lendingRateType,
23 | rate,
24 | calculationFrequency,
25 | applicationFrequency,
26 | comparisonRate,
27 | interestPaymentDue,
28 | repaymentType,
29 | loanPurpose,
30 | tiers,
31 | additionalValue,
32 | additionalInfo,
33 | additionalInfoUri
34 | } = props.lendingRate
35 | return (
36 |
37 | {(rate * 100).toFixed(2)}%
38 | {!!comparisonRate && Comparison rate: {(comparisonRate * 100).toFixed(2)}%
}
39 |
40 | {translateLendingRateType(lendingRateType)}
41 | {
42 | (lendingRateType === 'FIXED' || lendingRateType === 'INTRODUCTORY') && !!additionalValue &&
43 | -
44 | }
45 | {
46 | ( lendingRateType === 'DISCOUNT' ||
47 | lendingRateType === 'PENALTY' ||
48 | lendingRateType === 'FLOATING' ||
49 | lendingRateType === 'MARKET_LINKED' ||
50 | lendingRateType === 'BUNDLE_DISCOUNT_FIXED' ||
51 | lendingRateType === 'BUNDLE_DISCOUNT_VARIABLE') &&
52 | - {additionalValue}
53 | }
54 | {
55 | ( lendingRateType === 'VARIABLE' ||
56 | lendingRateType === 'PURCHASE' ) && !!additionalValue &&
57 | - {additionalValue}
58 | }
59 |
60 | {!!calculationFrequency && Calculated
}
61 | {!!applicationFrequency && Applied
}
62 | {!!interestPaymentDue && Interest Payment {translateInterestPaymentDue(interestPaymentDue)}
}
63 | {!!repaymentType && Repayment Type {translateRepaymentType(repaymentType)}
}
64 | {!!loanPurpose && Loan Purpose {translateloanPurpose(loanPurpose)}
}
65 | {
66 | !!tiers && tiers.length > 0 &&
67 |
68 |
Rate Tiers:
69 |
70 | {tiers.sort((a, b)=>ecomp(a.name, b.name)).map((tier, index) => )}
71 |
72 |
73 | }
74 | {!!additionalInfo && {additionalInfo}
}
75 | {!!additionalInfoUri && }
76 |
77 | )
78 | }
79 |
80 | export default LendingRate
81 |
--------------------------------------------------------------------------------
/src/components/data/energy/EnergyPlanSolarFeedInTariff.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { makeStyles } from '@material-ui/core'
3 | import Rate from './Rate'
4 | import Duration from '../Duration'
5 |
6 | const useStyles = makeStyles(() => ({
7 | sectionTitle: {
8 | fontStyle: 'italic'
9 | },
10 | sectionContent: {
11 | marginTop: 0,
12 | marginBottom: 0,
13 | paddingLeft: 20
14 | }
15 | }))
16 |
17 | const EnergyPlanSolarFeedInTariff = ({solarFeedInTariff}) => {
18 | const classes = useStyles()
19 | const {displayName, description, startTime, endTime, scheme, payerType, tariffUType, singleTariff, timeVaryingTariffs} = solarFeedInTariff
20 | return (
21 |
22 | Display Name: {displayName}
23 | {description && (
24 | Description: {description}
25 | )}
26 | {startTime && (
27 | Start Time: {startTime}
28 | )}
29 | {endTime && (
30 | End Time: {endTime}
31 | )}
32 | Scheme: {scheme}
33 | Payer Type: {payerType}
34 | Tariff Type: {tariffUType}
35 | {singleTariff && (
36 |
37 |
Single Tariff:
38 |
Rates:
39 |
40 | {singleTariff.rates.map((rate, index) => )}
41 |
42 |
Period:
43 |
44 | )}
45 | {timeVaryingTariffs && (
46 | <>
47 | Time Varying Tariffs:
48 |
83 | >
84 | )}
85 |
86 | )
87 | }
88 |
89 | export default EnergyPlanSolarFeedInTariff
90 |
--------------------------------------------------------------------------------
/src/components/data/banking/BankingProductList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {connect} from 'react-redux'
3 | import {START_RETRIEVE_PRODUCT_LIST, startRetrieveProductList, retrieveProductList} from '../../../store/banking/data'
4 | import LinearProgress from '@material-ui/core/LinearProgress'
5 | import ProductCategory from './ProductCategory'
6 | import {normalise} from '../../../utils/url'
7 |
8 | class BankingProductList extends React.Component {
9 |
10 | componentDidMount() {
11 | const { dataSourceIndex, dataSource, versionInfo } = this.props
12 | const { url } = dataSource
13 | const normalisedUrl = normalise(url)
14 | const productListUrl = normalisedUrl + '/banking/products'
15 | this.props.startRetrieveProductList(dataSourceIndex)
16 | this.props.retrieveProductList(dataSourceIndex, normalisedUrl, productListUrl, versionInfo.xV, versionInfo.xMinV)
17 | }
18 |
19 | render() {
20 | const { dataSourceIndex } = this.props
21 | let productList = this.props.productList[dataSourceIndex];
22 | productList = !!productList ? productList : {}
23 | const { progress, totalRecords, detailRecords, failedDetailRecords, products, productDetails } = productList
24 | const productsByCategory = {}
25 | const processedRecords = detailRecords + failedDetailRecords
26 | if (!!totalRecords && totalRecords <= processedRecords) {
27 | const productsMap = {}
28 | if (failedDetailRecords > 0) {
29 | products.forEach(product => {productsMap[product.productId] = product})
30 | }
31 | !!productDetails && productDetails.forEach(productDetail => {
32 | if (productDetail) {
33 | let productCategory = productDetail.productCategory;
34 | if (!productsByCategory[productCategory]) {
35 | productsByCategory[productCategory] = []
36 | }
37 | productsByCategory[productCategory].push(productDetail)
38 | delete productsMap[productDetail.productId]
39 | }
40 | })
41 | Object.keys(productsMap).forEach(productId => {
42 | const product = productsMap[productId]
43 | let productCategory = product.productCategory;
44 | if (!productsByCategory[productCategory]) {
45 | productsByCategory[productCategory] = []
46 | }
47 | productsByCategory[productCategory].push(product)
48 | })
49 | }
50 |
51 | return (
52 |
53 | {
54 | !!totalRecords && (processedRecords < totalRecords) &&
55 |
56 | }
57 | {
58 | progress === START_RETRIEVE_PRODUCT_LIST &&
Getting all current products...
59 | }
60 | {
61 | processedRecords < totalRecords &&
Getting product details...
62 | }
63 | {
64 | !!products && processedRecords >= totalRecords &&
65 | Object.keys(productsByCategory).sort().map((category, index) => (
66 |
67 | ))
68 | }
69 |
70 | )
71 | }
72 | }
73 |
74 | const mapStateToProps = state => ({
75 | productList: state.banking,
76 | versionInfo: state.versionInfo.vHeaders
77 | })
78 |
79 | const mapDispatchToProps = {startRetrieveProductList, retrieveProductList}
80 |
81 | export default connect(mapStateToProps, mapDispatchToProps)(BankingProductList)
82 |
--------------------------------------------------------------------------------
/src/components/data/discovery/AEMODiscoveryInfo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Accordion from '@material-ui/core/Accordion'
3 | import AccordionSummary from '@material-ui/core/AccordionSummary'
4 | import AccordionActions from '@material-ui/core/AccordionActions'
5 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
6 | import SubjectIcon from '@material-ui/icons/Subject'
7 | import Typography from '@material-ui/core/Typography'
8 | import Divider from '@material-ui/core/Divider'
9 | import RefreshIcon from '@material-ui/icons/Refresh'
10 | import Fab from '@material-ui/core/Fab'
11 | import Tooltip from '@material-ui/core/Tooltip'
12 | import { makeStyles } from '@material-ui/core/styles'
13 | import { fade } from '@material-ui/core/styles/colorManipulator'
14 | import StatusOutages from './StatusOutages'
15 | import { connect } from 'react-redux'
16 | import { retrieveStatus, retrieveOutages } from '../../../store/aemo_discovery'
17 |
18 | const useStyles = makeStyles(theme => ({
19 | container: {
20 | marginLeft: theme.typography.pxToRem(20),
21 | marginRight: theme.typography.pxToRem(20)
22 | },
23 | panel: {
24 | backgroundColor: fade('#fff', 0.9)
25 | },
26 | heading: {
27 | display: 'flex',
28 | alignItems: 'center',
29 | justifyContent: 'space-between',
30 | fontSize: theme.typography.pxToRem(20),
31 | },
32 | details: {
33 | maxWidth:'95%',
34 | marginLeft: 'auto',
35 | marginRight: 'auto',
36 | marginBottom: 20
37 | }
38 | }))
39 |
40 | const AEMODiscoveryInfo = (props) => {
41 |
42 | const classes = useStyles()
43 | const [expanded, setExpanded] = React.useState(true)
44 | const {statusDetails, outagesDetails} = props.data
45 |
46 | const toggleExpansion = (event, newExpanded) => {
47 | setExpanded(newExpanded)
48 | }
49 |
50 | const refreshStatusOutages = () => {
51 | props.retrieveStatus()
52 | props.retrieveOutages()
53 | }
54 |
55 | React.useEffect(() => {
56 | refreshStatusOutages()
57 | // eslint-disable-next-line
58 | }, [])
59 |
60 | return (
61 |
62 | }
64 | aria-controls='panel1c-content'
65 | >
66 |
67 | Status & Outages
68 |
69 |
70 |
71 |
Secondary Data Holders provide data to Data Holders via CDR requests, who in turn provide the data to ADRs. Such data is called Shared Responsibility Data (SR data).
72 |
Currently, only the energy sector has a designated secondary data holder: AEMO. This page lists the status and outages for AEMO.
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | )
89 | }
90 |
91 | const mapStateToProps = state => ({
92 | data: state.aemoDiscovery
93 | })
94 |
95 | const mapDispatchToProps = {
96 | retrieveStatus,
97 | retrieveOutages
98 | }
99 |
100 | export default connect(mapStateToProps, mapDispatchToProps)(AEMODiscoveryInfo)
--------------------------------------------------------------------------------
/src/components/data/energy/EnergyPlanTariffPeriod.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { makeStyles } from '@material-ui/core'
3 | import Rate from './Rate'
4 | import SingleRate from './SingleRate'
5 | import TimeOfUseRate from './TimeOfUseRate'
6 |
7 | const useStyles = makeStyles(() => ({
8 | sectionTitle: {
9 | fontStyle: 'italic'
10 | },
11 | sectionContent: {
12 | marginTop: 0,
13 | marginBottom: 0,
14 | paddingLeft: 20
15 | }
16 | }))
17 |
18 | const EnergyPlanTariffPeriod = ({tariffPeriod}) => {
19 | const classes = useStyles()
20 | const {type, displayName, startDate, endDate, dailySupplyChargeType, dailySupplyCharge, bandedDailySupplyCharges, timeZone, rateBlockUType, singleRate, timeOfUseRates, demandCharges} = tariffPeriod
21 | return (
22 |
23 | {type && (
24 | Type: {type}
25 | )}
26 | Display Name: {displayName}
27 | Start Date: {startDate}
28 | End Date: {endDate}
29 | {dailySupplyChargeType && (
30 | Daily Supply Charge Type: {dailySupplyChargeType}
31 | )}
32 | {dailySupplyCharge && (
33 | Daily Supply Charge: ${dailySupplyCharge}
34 | )}
35 | {bandedDailySupplyCharges && (
36 | <>
37 | Banded Daily Supply Charges:
38 |
39 | {bandedDailySupplyCharges.map((dailySupplyCharge, index) => )}
40 |
41 | >
42 | )}
43 | {timeZone && (
44 | Time Zone: {timeZone}
45 | )}
46 | Rate Block Type: {rateBlockUType}
47 | {singleRate && (
48 | <>
49 | Single Rate:
50 |
51 | >
52 | )}
53 | {timeOfUseRates && (
54 | <>
55 | Time Of Use Rates:
56 |
57 | {timeOfUseRates.map((timeOfUseRate, index) => )}
58 |
59 | >
60 | )}
61 | {demandCharges && (
62 | <>
63 | Demand Charges:
64 |
65 | {demandCharges.map(({displayName, description, amount, measureUnit, startTime, endTime, days, minDemand, maxDemand, measurementPeriod, chargePeriod}, index) => (
66 |
67 | Display Name: {displayName}
68 | {description && (
69 | Description: {description}
70 | )}
71 | Amount: ${amount} (exclusive of GST)
72 | {measureUnit && (
73 | Measure Unit: {measureUnit}
74 | )}
75 | {startTime && (
76 | Start Time: {startTime}
77 | )}
78 | {endTime && (
79 | End Time: {endTime}
80 | )}
81 | {days && (
82 | Days: {days.join(', ')}
83 | )}
84 | Min Demand: {minDemand || '0.00'}kW
85 | {maxDemand && (
86 | Max Demand: {maxDemand}kW
87 | )}
88 | Measurement Period: {measurementPeriod}
89 | Charge Period: {chargePeriod}
90 |
91 | ))}
92 |
93 | >
94 | )}
95 |
96 | )
97 | }
98 |
99 | export default EnergyPlanTariffPeriod
100 |
--------------------------------------------------------------------------------
/src/store/energy/data/actions.js:
--------------------------------------------------------------------------------
1 | import {conoutInfo, conoutError} from '../../conout/actions'
2 | import {createConoutError, checkExposedHeaders} from '../../../utils/cors'
3 | import {encodeRFC3986URIComponent} from '../../../utils/url'
4 |
5 | export const START_RETRIEVE_PLAN_LIST = 'START_RETRIEVE_PLAN_LIST'
6 | export const RETRIEVE_PLAN_LIST = 'RETRIEVE_PLAN_LIST'
7 | export const RETRIEVE_PLAN_DETAIL = 'RETRIEVE_PLAN_DETAIL'
8 | export const RETRIEVE_ALL_PLAN_DETAILS = 'RETRIEVE_ALL_PLAN_DETAILS'
9 | export const CLEAR_DATA = 'CLEAR_ENERGY_DATA'
10 |
11 | export const startRetrievePlanList = (dataSourceIdx) => ({
12 | type: START_RETRIEVE_PLAN_LIST,
13 | payload: {idx: dataSourceIdx}
14 | })
15 |
16 | const headers = {
17 | 'Accept': 'application/json'
18 | }
19 |
20 | export const retrievePlanList = (dataSourceIdx, baseUrl, planListUrl, xV, xMinV, effective, fuelType) =>
21 | (dispatch) => {
22 | const request = new Request(planListUrl, {headers: new Headers({...headers, 'x-v': xV, 'x-min-v': xMinV})})
23 | dispatch(conoutInfo(`Requesting retrievePlanList() for ${planListUrl}`))
24 | const response = dispatch({
25 | type: RETRIEVE_PLAN_LIST,
26 | payload: fetch(request)
27 | .then(response => {
28 | if (response.ok) {
29 | checkExposedHeaders(response, planListUrl, dispatch)
30 | return response.json()
31 | }
32 | throw new Error(`Response not OK. Status: ${response.status} (${response.statusText})`)
33 | })
34 | .then(obj => {
35 | dispatch(conoutInfo(`Received retrievePlanList() response for ${planListUrl}:`, obj))
36 | return obj
37 | })
38 | .catch(error => {
39 | dispatch(createConoutError(error, planListUrl))
40 | return {
41 | meta: {totalRecords: 0},
42 | data: {plans: []},
43 | links: {}
44 | }
45 | })
46 | .then(json => ({idx: dataSourceIdx, response: json}))
47 | })
48 | response.then(({value})=> {
49 | const {plans} = value.response.data
50 | const actions = plans.map(plan => retrievePlanDetail(dataSourceIdx, baseUrl, plan.planId, xV, xMinV))
51 | const {next} = value.response.links
52 | if (!!next) {
53 | const proxyRegExp = RegExp('^(https?://.*)(https?://.*)', 'gi') // RegExp objects are not thread safe
54 | const proxyInfo = proxyRegExp.exec(planListUrl)
55 | if ((proxyInfo && (next === proxyInfo[2])) || (!proxyInfo && (next === planListUrl))) {
56 | dispatch(conoutError(`The link next should not be the same as the current page URL (${planListUrl}):`, value.response.links))
57 | } else {
58 | actions.push(retrievePlanList(dataSourceIdx, baseUrl, proxyInfo ? proxyInfo[1] + next : next, xV, xMinV, effective, fuelType))
59 | }
60 | }
61 | dispatch(retrieveAllPlanDetails(actions))
62 | })
63 | }
64 |
65 | export const retrievePlanDetail = (dataSourceIdx, url, planId, xV, xMinV) => (dispatch, getState) => {
66 | const fullUrl = url + '/energy/plans/' + encodeRFC3986URIComponent(planId)
67 | const request = new Request(fullUrl, {
68 | headers: new Headers({...headers, 'x-v': xV, 'x-min-v': xMinV})
69 | })
70 | dispatch(conoutInfo('Requesting retrievePlanDetail() for plan ' + planId))
71 | dispatch({
72 | type: RETRIEVE_PLAN_DETAIL,
73 | payload: fetch(request)
74 | .then(response => {
75 | if (response.ok) {
76 | return response.json()
77 | }
78 | throw new Error(`Response not OK. Status: ${response.status} (${response.statusText})`)
79 | })
80 | .then(obj => {
81 | dispatch(conoutInfo(`Received response for ${fullUrl}:`, obj))
82 | return {idx: dataSourceIdx, response: obj}
83 | })
84 | .catch(error => {
85 | dispatch(createConoutError(error, fullUrl))
86 | return {idx: dataSourceIdx, response: null}
87 | })
88 | })
89 | }
90 |
91 | export const retrieveAllPlanDetails = (actions) => dispatch => dispatch({
92 | type: RETRIEVE_ALL_PLAN_DETAILS,
93 | payload: Promise.all(actions.map(action => dispatch(action)))
94 | })
95 |
96 | export const clearData = (dataSourceIdx) => ({
97 | type: CLEAR_DATA,
98 | payload: dataSourceIdx
99 | })
100 |
--------------------------------------------------------------------------------
/src/store/banking/data/actions.js:
--------------------------------------------------------------------------------
1 | import {conoutInfo, conoutError, conoutWarn} from '../../conout/actions'
2 | import {createConoutError, checkExposedHeaders} from '../../../utils/cors'
3 | import {encodeRFC3986URIComponent} from '../../../utils/url'
4 |
5 | export const START_RETRIEVE_PRODUCT_LIST = 'START_RETRIEVE_PRODUCT_LIST'
6 | export const RETRIEVE_PRODUCT_LIST = 'RETRIEVE_PRODUCT_LIST'
7 | export const RETRIEVE_PRODUCT_DETAIL = 'RETRIEVE_PRODUCT_DETAIL'
8 | export const RETRIEVE_ALL_PRODUCT_DETAILS = 'RETRIEVE_ALL_PRODUCT_DETAILS'
9 | export const DELETE_DATA = 'DELETE_DATA'
10 | export const CLEAR_DATA = 'CLEAR_DATA'
11 |
12 | export const startRetrieveProductList = (dataSourceIdx) => ({
13 | type: START_RETRIEVE_PRODUCT_LIST,
14 | payload: {idx: dataSourceIdx}
15 | })
16 |
17 | const headers = {
18 | 'Accept': 'application/json'
19 | }
20 |
21 | export const retrieveProductList = (dataSourceIdx, baseUrl, productListUrl, xV, xMinV) =>
22 | (dispatch) => {
23 | const request = new Request(productListUrl, {headers: new Headers({...headers, 'x-v': xV, 'x-min-v': xMinV})})
24 | dispatch(conoutInfo(`Requesting retrieveProductList() for ${productListUrl}`))
25 | const response = dispatch({
26 | type: RETRIEVE_PRODUCT_LIST,
27 | payload: fetch(request)
28 | .then(response => {
29 | if (response.ok) {
30 | checkExposedHeaders(response, productListUrl, dispatch)
31 | return response.json()
32 | }
33 | throw new Error(`Response not OK. Status: ${response.status} (${response.statusText})`)
34 | })
35 | .then(obj => {
36 | dispatch(conoutInfo(`Received retrieveProductList() response for ${productListUrl}:`, obj))
37 | return obj
38 | })
39 | .catch(error => {
40 | dispatch(createConoutError(error, productListUrl))
41 | return {
42 | meta: {totalRecords: 0},
43 | data: {products: []},
44 | links: {}
45 | }
46 | })
47 | .then(json => ({idx: dataSourceIdx, response: json}))
48 | })
49 | response.then(({value})=> {
50 | const {products} = value.response.data
51 | const actions = products.map(product => retrieveProductDetail(dataSourceIdx, baseUrl, product.productId, xV, xMinV))
52 | const {next} = value.response.links
53 | if (!!next) {
54 | if (next === productListUrl) {
55 | dispatch(conoutError(`The link next should not be the same as the current page URL (${productListUrl}):`, value.response.links))
56 | } else {
57 | actions.push(retrieveProductList(dataSourceIdx, baseUrl, next, xV, xMinV))
58 | }
59 | }
60 | dispatch(retrieveAllProductDetails(actions))
61 | })
62 | }
63 |
64 | export const retrieveProductDetail = (dataSourceIdx, url, productId, xV, xMinV) => (dispatch, getState) => {
65 | const fullUrl = url + '/banking/products/' + encodeRFC3986URIComponent(productId)
66 | const request = new Request(fullUrl, {
67 | headers: new Headers({...headers, 'x-v': xV, 'x-min-v': xMinV})
68 | })
69 | dispatch(conoutInfo('Requesting retrieveProductDetail() for product ' + productId))
70 | dispatch({
71 | type: RETRIEVE_PRODUCT_DETAIL,
72 | payload: fetch(request)
73 | .then(response => {
74 | if (response.ok) {
75 | return response.json()
76 | }
77 | throw new Error(`Response not OK. Status: ${response.status} (${response.statusText})`)
78 | })
79 | .then(obj => {
80 | dispatch(conoutInfo(`Received response for ${fullUrl}:`, obj))
81 | return obj
82 | })
83 | .then(json => {
84 | const { productDetails } = getState().banking[dataSourceIdx]
85 | const { data } = json
86 | if (productDetails.some(prod => prod.productId === data.productId
87 | && prod.productCategory === data.productCategory)) {
88 | dispatch(conoutWarn(`Product with id ${data.productId} already exists in ${data.productCategory}`))
89 | return {idx: dataSourceIdx, response: null}
90 | }
91 | return {idx: dataSourceIdx, response: json}
92 | })
93 | .catch(error => {
94 | dispatch(createConoutError(error, fullUrl))
95 | return {idx: dataSourceIdx, response: null}
96 | })
97 | })
98 | }
99 |
100 | export const retrieveAllProductDetails = (actions) => dispatch => dispatch({
101 | type: RETRIEVE_ALL_PRODUCT_DETAILS,
102 | payload: Promise.all(actions.map(action => dispatch(action)))
103 | })
104 |
105 | export const deleteData = (dataSourceIdx) => ({
106 | type: DELETE_DATA,
107 | payload: dataSourceIdx
108 | })
109 |
110 | export const clearData = (dataSourceIdx) => ({
111 | type: CLEAR_DATA,
112 | payload: dataSourceIdx
113 | })
114 |
--------------------------------------------------------------------------------
/src/components/data/banking/BankingPanel.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {connect} from 'react-redux'
3 | import {makeStyles} from '@material-ui/core/styles'
4 | import Accordion from '@material-ui/core/Accordion'
5 | import AccordionSummary from '@material-ui/core/AccordionSummary'
6 | import AccordionActions from '@material-ui/core/AccordionActions'
7 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
8 | import SubjectIcon from '@material-ui/icons/Subject'
9 | import Typography from '@material-ui/core/Typography'
10 | import Divider from '@material-ui/core/Divider'
11 | import CompareIcon from '@material-ui/icons/Compare'
12 | import { fade } from '@material-ui/core/styles/colorManipulator'
13 | import Fab from '@material-ui/core/Fab'
14 | import Grid from '@material-ui/core/Grid'
15 | import BankingProductList from './BankingProductList'
16 | import { compareProducts } from '../../../store/banking/comparison'
17 |
18 | const useStyles = makeStyles(theme => ({
19 | container: {
20 | marginLeft: theme.typography.pxToRem(20),
21 | marginRight: theme.typography.pxToRem(20)
22 | },
23 | panel: {
24 | backgroundColor: fade('#fff', 0.9)
25 | },
26 | heading: {
27 | display: 'flex',
28 | alignItems: 'center',
29 | justifyContent: 'space-between',
30 | fontSize: theme.typography.pxToRem(20),
31 | },
32 | details: {
33 | maxWidth:'95%',
34 | marginLeft: 'auto',
35 | marginRight: 'auto',
36 | marginBottom: 20
37 | },
38 | button: {
39 | margin: theme.spacing(1)
40 | },
41 | leftIcon: {
42 | marginRight: theme.spacing(1),
43 | }
44 | }))
45 |
46 | const BankingPanel = (props) => {
47 | const {dataSources, savedDataSourcesCount} = props
48 | const classes = useStyles()
49 | const [expanded, setExpanded] = React.useState(true)
50 | const compare = () => {
51 | if( /Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) {
52 | alert('The screen size is too small! Please use a bigger screen to compare.')
53 | return
54 | }
55 | props.compareProducts(props.selectedProducts)
56 | setExpanded(false)
57 | }
58 | const toggleExpansion = (event, newExpanded) => {
59 | setExpanded(newExpanded)
60 | }
61 |
62 | const getWidth = (dataSourceCount, min) => {
63 | return Math.max(12 / dataSourceCount, min)
64 | }
65 |
66 | return (
67 |
68 | }
70 | aria-controls='panel1c-content'
71 | >
72 |
73 | Products
74 |
75 |
76 |
77 | {
78 | savedDataSourcesCount > 0 &&
79 |
80 | {dataSources.map((dataSource, index) => (
81 | isBankingDataSource(dataSource) &&
82 |
89 | {!!dataSource.icon &&
}
{dataSource.name}
90 |
91 |
92 | ))}
93 |
94 | }
95 |
96 |
97 |
98 | 4}
100 | className={classes.button} onClick={compare}>
101 |
102 | Compare
103 |
104 |
105 |
106 | )
107 | }
108 |
109 | function isBankingDataSource(dataSource) {
110 | return !dataSource.unsaved && !dataSource.deleted && dataSource.enabled && (!dataSource.sectors || dataSource.sectors.includes("banking"))
111 | }
112 |
113 | const mapStateToProps = state=>({
114 | dataSources : state.dataSources,
115 | savedDataSourcesCount: state.dataSources.filter(dataSource => isBankingDataSource(dataSource)).length,
116 | selectedProducts: state.bankingSelection
117 | })
118 |
119 | const mapDispatchToProps = { compareProducts }
120 |
121 | export default connect(mapStateToProps, mapDispatchToProps)(BankingPanel)
122 |
--------------------------------------------------------------------------------
/src/store/data-source/actions.js:
--------------------------------------------------------------------------------
1 | export const LOAD_DATA_SOURCE = 'LOAD_DATA_SOURCE'
2 | export const ADD_DATA_SOURCE = 'ADD_DATA_SOURCE'
3 | export const SYNC_DATA_SOURCES = 'SYNC_DATA_SOURCES'
4 | export const SAVE_DATA_SOURCE = 'SAVE_DATA_SOURCE'
5 | export const DELETE_DATA_SOURCE = 'DELETE_DATA_SOURCE'
6 | export const MODIFY_DATA_SOURCE_NAME = 'MODIFY_DATA_SOURCE_NAME'
7 | export const MODIFY_DATA_SOURCE_URL = 'MODIFY_DATA_SOURCE_URL'
8 | export const MODIFY_DATA_SOURCE_ENERGY_PRD_URL = 'MODIFY_DATA_SOURCE_ENERGY_PRD_URL'
9 | export const MODIFY_DATA_SOURCE_ICON = 'MODIFY_DATA_SOURCE_ICON'
10 | export const ENABLE_DATA_SOURCE = 'ENABLE_DATA_SOURCE'
11 |
12 | const MAJOR_NAMES = {'ANZ': [], 'CommBank': ['CBA', 'Commonwealth Bank'], 'NATIONAL AUSTRALIA BANK': ['NAB', 'National'], 'Westpac': []}
13 | const MAJORS = Object.keys(MAJOR_NAMES)
14 |
15 | function mergeDatasources(into, from) {
16 | const result = {};
17 | into.forEach(ds => result[ds.name] = ds)
18 | from.forEach(ds => {
19 | const {name} = ds
20 | if (MAJORS.includes(name)) {
21 | // Consolidate the aliases of the Big Four
22 | MAJOR_NAMES[name].forEach(alias => {
23 | result[name] = {...result[alias], ...result[name]}
24 | delete result[alias]
25 | })
26 | }
27 | result[name] = {...result[name], ...ds}
28 | })
29 | return Object.values(result)
30 | }
31 |
32 | function fetchDatasources() {
33 | const dssPromise = fetch('https://api.cdr.gov.au/cdr-register/v1/all/data-holders/brands/summary', {headers: {"x-v": 1}})
34 | .then(response => response.json())
35 | .then(({data}) => data.map(({brandName: name, publicBaseUri: url, logoUri: icon, industries: sectors}) => ({name, url, icon, sectors})))
36 | const ovsPromise = fetch(process.env.PUBLIC_URL + '/override.json')
37 | .then(response => response.json())
38 | return Promise.all([dssPromise, ovsPromise]).then(([datasources, overrides]) =>
39 | mergeDatasources(datasources, overrides)
40 | )
41 | }
42 |
43 | function loadLocalDatasources() {
44 | const ds = window.localStorage.getItem("cds-banking-prd-ds")
45 | return ds ? JSON.parse(ds) : false
46 | }
47 |
48 | function sortMajorsFirst(datasources) {
49 |
50 | const nameSort = (a, b) => a.name < b.name ? -1 : 1
51 |
52 | const majorDatasources = [], minorDatasources = []
53 | datasources.forEach(ds => {
54 | if (majorDatasources.length < MAJORS.length && MAJORS.includes(ds.name)) {
55 | majorDatasources.push(ds)
56 | } else {
57 | minorDatasources.push(ds)
58 | }
59 | })
60 | return [...majorDatasources.sort(nameSort), ...minorDatasources.sort(nameSort)]
61 | }
62 |
63 | export function loadDataSource() {
64 | const ds = loadLocalDatasources()
65 | return {
66 | type: LOAD_DATA_SOURCE,
67 | payload: ds ? Promise.resolve(ds) : fetchDatasources()
68 | .then(datasources => {
69 | datasources = sortMajorsFirst(datasources)
70 | for (let i = 0; i < 4 && i < datasources.length; i++) {
71 | datasources[i].enabled = true
72 | }
73 | return datasources
74 | })
75 | }
76 | }
77 |
78 | export function addDataSource() {
79 | return {
80 | type: ADD_DATA_SOURCE
81 | }
82 | }
83 |
84 | export function syncDataSources() {
85 | return {
86 | type: SYNC_DATA_SOURCES,
87 | payload: fetchDatasources().then(datasources => {
88 | const localDatasources = loadLocalDatasources()
89 | return sortMajorsFirst(localDatasources ? mergeDatasources(localDatasources, datasources) : datasources)
90 | })
91 | }
92 | }
93 |
94 | export function saveDataSource(index, payload) {
95 | return {
96 | type: SAVE_DATA_SOURCE,
97 | index: index,
98 | payload: payload
99 | }
100 | }
101 |
102 | export function deleteDataSource(index) {
103 | return {
104 | type: DELETE_DATA_SOURCE,
105 | index: index
106 | }
107 | }
108 |
109 | export function modifyDataSourceName(index, payload) {
110 | return {
111 | type: MODIFY_DATA_SOURCE_NAME,
112 | index: index,
113 | payload: payload
114 | }
115 | }
116 |
117 | export function modifyDataSourceUrl(index, payload) {
118 | return {
119 | type: MODIFY_DATA_SOURCE_URL,
120 | index: index,
121 | payload: payload
122 | }
123 | }
124 |
125 | export function modifyDataSourceEnergyPrdUrl(index, payload) {
126 | return {
127 | type: MODIFY_DATA_SOURCE_ENERGY_PRD_URL,
128 | index: index,
129 | payload: payload
130 | }
131 | }
132 |
133 | export function modifyDataSourceIcon(index, payload) {
134 | return {
135 | type: MODIFY_DATA_SOURCE_ICON,
136 | index: index,
137 | payload: payload
138 | }
139 | }
140 |
141 | export function enableDataSource(index, payload) {
142 | return {
143 | type: ENABLE_DATA_SOURCE,
144 | index: index,
145 | payload: payload
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/components/data/discovery/DiscoveryInfo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Accordion from '@material-ui/core/Accordion'
3 | import AccordionSummary from '@material-ui/core/AccordionSummary'
4 | import AccordionActions from '@material-ui/core/AccordionActions'
5 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
6 | import SubjectIcon from '@material-ui/icons/Subject'
7 | import Typography from '@material-ui/core/Typography'
8 | import Grid from '@material-ui/core/Grid'
9 | import Divider from '@material-ui/core/Divider'
10 | import RefreshIcon from '@material-ui/icons/Refresh'
11 | import Fab from '@material-ui/core/Fab'
12 | import Tooltip from '@material-ui/core/Tooltip'
13 | import StatusOutages from './StatusOutages'
14 | import { makeStyles } from '@material-ui/core/styles'
15 | import { fade } from '@material-ui/core/styles/colorManipulator'
16 | import { connect } from 'react-redux'
17 | import { normalise } from '../../../utils/url'
18 | import { retrieveStatus, retrieveOutages } from '../../../store/discovery'
19 |
20 | const useStyles = makeStyles(theme => ({
21 | container: {
22 | marginLeft: theme.typography.pxToRem(20),
23 | marginRight: theme.typography.pxToRem(20)
24 | },
25 | panel: {
26 | backgroundColor: fade('#fff', 0.9)
27 | },
28 | heading: {
29 | display: 'flex',
30 | alignItems: 'center',
31 | justifyContent: 'space-between',
32 | fontSize: theme.typography.pxToRem(20),
33 | },
34 | details: {
35 | maxWidth:'95%',
36 | marginLeft: 'auto',
37 | marginRight: 'auto',
38 | marginBottom: 20
39 | }
40 | }))
41 |
42 | const DiscoveryInfo = (props) => {
43 |
44 | const {dataSources, savedDataSourcesCount} = props
45 | const classes = useStyles()
46 | const [expanded, setExpanded] = React.useState(true)
47 |
48 | const toggleExpansion = (event, newExpanded) => {
49 | setExpanded(newExpanded)
50 | }
51 |
52 | React.useEffect(() => {
53 | refreshStatusOutages()
54 | // eslint-disable-next-line
55 | }, [props.dataSources])
56 |
57 | const getWidth = (dataSourceCount, min) => {
58 | return Math.max(12 / dataSourceCount, min)
59 | }
60 |
61 | const refreshStatusOutages = () => {
62 | const { versionInfo } = props
63 | props.dataSources.forEach((dataSource, index) => {
64 | const url = normalise(dataSource.url)
65 | if (!dataSource.unsaved && dataSource.enabled && !dataSource.deleted) {
66 | props.retrieveStatus(index, url, versionInfo.xV, versionInfo.xMinV)
67 | props.retrieveOutages(index, url, versionInfo.xV, versionInfo.xMinV)
68 | }
69 | })
70 | }
71 |
72 | return (
73 |
74 | }
76 | aria-controls='panel1c-content'
77 | >
78 |
79 | Status & Outages
80 |
81 |
82 |
83 | {
84 | savedDataSourcesCount > 0 &&
85 |
86 | {dataSources.map((dataSource, index) => {
87 | const data = props.data[index]
88 | if (!data) {
89 | return false
90 | }
91 | const {statusDetails, outagesDetails} = data
92 | return (!dataSource.unsaved && dataSource.enabled && !dataSource.deleted &&
93 |
100 | {!!dataSource.icon &&
}
{dataSource.name}
101 |
102 |
103 | )
104 | })}
105 |
106 | }
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | )
120 | }
121 |
122 | const mapStateToProps = state => ({
123 | dataSources : state.dataSources,
124 | savedDataSourcesCount: state.dataSources.filter(dataSource => !dataSource.unsaved && !dataSource.deleted && dataSource.enabled).length,
125 | versionInfo: state.versionInfo.vHeaders,
126 | data: state.discovery
127 | })
128 |
129 | const mapDispatchToProps = {
130 | retrieveStatus,
131 | retrieveOutages
132 | }
133 |
134 | export default connect(mapStateToProps, mapDispatchToProps)(DiscoveryInfo)
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/components/data/energy/PlanContract.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { makeStyles } from '@material-ui/core'
3 | import IntrinsicGreenPower from './IntrinsicGreenPower'
4 | import ControlledLoad from './ControlledLoad'
5 | import EnergyPlanIncentive from './EnergyPlanIncentive'
6 | import EnergyPlanDiscount from './EnergyPlanDiscount'
7 | import EnergyPlanGreenPowerCharge from './EnergyPlanGreenPowerCharge'
8 | import EnergyPlanEligibility from './EnergyPlanEligibility'
9 | import EnergyPlanFee from './EnergyPlanFee'
10 | import EnergyPlanSolarFeedInTariff from './EnergyPlanSolarFeedInTariff'
11 | import EnergyPlanTariffPeriod from './EnergyPlanTariffPeriod'
12 | import Duration from '../Duration'
13 |
14 | const useStyles = makeStyles(() => ({
15 | sectionTitle: {
16 | fontStyle: 'italic'
17 | },
18 | sectionContent: {
19 | marginTop: 0,
20 | marginBottom: 0,
21 | paddingLeft: 20
22 | }
23 | }))
24 |
25 | const PlanContract = ({contract}) => {
26 | const classes = useStyles()
27 | const {additionalFeeInformation, pricingModel, timeZone, isFixed, variation, onExpiryDescription, paymentOption,
28 | intrinsicGreenPower, controlledLoad, incentives, discounts, greenPowerCharges, eligibility, fees, solarFeedInTariff,
29 | tariffPeriod, termType, benefitPeriod, terms, meterTypes, coolingOffDays, billFrequency} = contract
30 | return (
31 | <>
32 | {additionalFeeInformation && (
33 | Additional Fee Information: {additionalFeeInformation}
34 | )}
35 | Pricing Model: {pricingModel}
36 | Time Zone: {timeZone || 'AEST'}
37 | Is Fixed: {isFixed + ''}
38 | {variation && (
39 | Variation: {variation}
40 | )}
41 | {onExpiryDescription && (
42 | On Expiry Description: {onExpiryDescription}
43 | )}
44 | Payment Option: {paymentOption.join(', ')}
45 | {intrinsicGreenPower && (
46 | <>
47 | Intrinsic Green Power:
48 |
49 |
50 |
51 | >
52 | )}
53 | {controlledLoad && (
54 | <>
55 | Controlled Load:
56 |
57 | {controlledLoad.map((cl, index) => )}
58 |
59 | >
60 | )}
61 | {incentives && (
62 | <>
63 | Incentives:
64 |
65 | {incentives.map((incentive, index) => )}
66 |
67 | >
68 | )}
69 | {discounts && (
70 | <>
71 | Discounts:
72 |
73 | {discounts.map((discount, index) => )}
74 |
75 | >
76 | )}
77 | {greenPowerCharges && (
78 | <>
79 | Green Power Charges:
80 |
81 | {greenPowerCharges.map((greenPowerCharge, index) => )}
82 |
83 | >
84 | )}
85 | {eligibility && (
86 | <>
87 | Eligibility:
88 |
89 | {eligibility.map((el, index) => )}
90 |
91 | >
92 | )}
93 | {fees && (
94 | <>
95 | Fees:
96 |
97 | {fees.map((fee, index) => )}
98 |
99 | >
100 | )}
101 | {solarFeedInTariff && (
102 | <>
103 | Solar Feed-in Tariff:
104 |
105 | {solarFeedInTariff.map((tariff, index) => )}
106 |
107 | >
108 | )}
109 | <>
110 | Tariff Period:
111 |
112 | {tariffPeriod.map((period, index) => )}
113 |
114 | >
115 | {termType && (
116 | Term Type: {termType}
117 | )}
118 | {benefitPeriod && (
119 | Benefit Period: {benefitPeriod}
120 | )}
121 | {terms && (
122 | Terms: {terms}
123 | )}
124 | {meterTypes && (
125 | Meter Types: {meterTypes.join(', ')}
126 | )}
127 | {coolingOffDays && (
128 | Cooling Off: {coolingOffDays} days
129 | )}
130 |
131 | Bill Frequency:
132 |
133 | {billFrequency.map((billingSchedule, index) => (<>{!!index && <>, >} >))}
134 |
135 |
136 | >
137 | )
138 | }
139 |
140 | export default PlanContract
141 |
--------------------------------------------------------------------------------
/src/components/data/energy/Plan.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {connect} from 'react-redux'
3 | import {makeStyles, withStyles} from '@material-ui/core'
4 | import MuiAccordion from '@material-ui/core/Accordion'
5 | import MuiAccordionSummary from '@material-ui/core/AccordionSummary'
6 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
7 | import Typography from '@material-ui/core/Typography'
8 | import Checkbox from '@material-ui/core/Checkbox'
9 | import DateTime from '../DateTime'
10 | import AdditionalInfo from './AdditionalInfo'
11 | import {deselectPlan, selectPlan} from '../../../store/energy/selection'
12 | import Type from './Type'
13 | import FuelType from './FuelType'
14 | import CustomerType from './CustomerType'
15 | import ExternalLink from './ExternalLink'
16 | import Geography from './Geography'
17 | import PlanContract from './PlanContract'
18 | import MeteringCharge from './MeteringCharge'
19 |
20 | const useStyles = makeStyles(() => ({
21 | root: {
22 | display: 'flex',
23 | alignItems: 'flex-start'
24 | },
25 | sectionTitle: {
26 | fontStyle: 'italic'
27 | },
28 | sectionContent: {
29 | marginTop: 0,
30 | marginBottom: 0,
31 | paddingLeft: 20
32 | }
33 | }))
34 |
35 | const Accordion = withStyles({
36 | root: {
37 | width: '100%',
38 | backgroundColor: 'transparent',
39 | boxShadow: 'none',
40 | '&:not(:last-child)': {
41 | borderBottom: 0,
42 | },
43 | '&:before': {
44 | display: 'none',
45 | },
46 | '&$expanded': {
47 | margin: 'auto',
48 | },
49 | },
50 | expanded: {},
51 | })(MuiAccordion)
52 |
53 | const AccordionSummary = withStyles({
54 | root: {
55 | paddingLeft: 0,
56 | paddingRight: 24,
57 | alignItems: 'flex-start',
58 | backgroundColor: 'transparent',
59 | marginBottom: -1,
60 | maxHeight: 36,
61 | minHeight: 24,
62 | '&$expanded': {
63 | maxHeight: 36,
64 | minHeight: 24,
65 | }
66 | },
67 | content: {
68 | margin: '8px 0',
69 | '&$expanded': {
70 | margin: '8px 0',
71 | },
72 | },
73 | expandIcon: {
74 | paddingTop: 8,
75 | '&$expanded': {
76 | paddingTop: 8
77 | }
78 | },
79 | expanded: {}
80 | })(MuiAccordionSummary)
81 |
82 | const Plan = (props) => {
83 | const {plan, dataSourceIndex, selectedPlans} = props
84 | const classes = useStyles()
85 | const selected = selectedPlans.some(selection => (selection.dataSourceIdx === dataSourceIndex && selection.plan.planId === plan.planId))
86 | const handleChange = event => event.target.checked ? props.selectPlan(dataSourceIndex, plan) : props.deselectPlan(dataSourceIndex, plan)
87 | const blob = new Blob([JSON.stringify(plan, null, 2)], {type : 'application/json'})
88 |
89 | return (
90 |
91 |
96 |
97 | }
99 | aria-controls='panel1c-content'
100 | >
101 | {plan.displayName}
102 |
103 |
104 |
{plan.description}
105 |
Brand: {plan.brand} {!!plan.brandName && ({plan.brandName}) }
106 |
Type:
107 |
Fuel Type:
108 |
Plan ID: {plan.planId}
109 |
Last updated at JSON
110 | {!!plan.effectiveFrom &&
Effective from
}
111 | {!!plan.effectiveTo &&
Effective to
}
112 | {!!plan.applicationUri &&
Apply here }
113 | {
114 | !!plan.additionalInformation &&
115 |
116 |
Additional Information:
117 |
118 |
119 | }
120 | {!!plan.customerType &&
Customer Type:
}
121 | {
122 | !!plan.geography &&
123 |
124 |
Geography:
125 |
126 |
127 |
128 |
129 | }
130 | {
131 | !!plan.meteringCharges &&
132 |
133 |
Metering Charges:
134 |
135 | {plan.meteringCharges.map((meteringCharge, index) => )}
136 |
137 |
138 | }
139 | {
140 | !!plan.gasContract &&
141 |
142 |
Gas Contract:
143 |
146 |
147 | }
148 | {
149 | !!plan.electricityContract &&
150 |
151 |
Electricity Contract:
152 |
155 |
156 | }
157 |
158 |
159 |
160 | )
161 | }
162 |
163 | const mapStateToProps = state => ({
164 | selectedPlans: state.energySelection
165 | })
166 |
167 | const mapDispatchToProps = { selectPlan, deselectPlan }
168 |
169 | export default connect(mapStateToProps, mapDispatchToProps)(Plan)
170 |
--------------------------------------------------------------------------------
/src/components/data/ConsolePanel.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Accordion from '@material-ui/core/Accordion'
3 | import AccordionActions from '@material-ui/core/AccordionActions'
4 | import AccordionSummary from '@material-ui/core/AccordionSummary'
5 | import ComputerIcon from '@material-ui/icons/Computer'
6 | import Divider from '@material-ui/core/Divider'
7 | import Grid from '@material-ui/core/Grid'
8 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
9 | import Typography from '@material-ui/core/Typography'
10 | import Fab from '@material-ui/core/Fab'
11 | import Tooltip from '@material-ui/core/Tooltip'
12 | import RefreshIcon from '@material-ui/icons/Refresh'
13 | import DeleteIcon from '@material-ui/icons/Delete'
14 | import { connect } from 'react-redux'
15 | import { fade } from '@material-ui/core/styles/colorManipulator'
16 | import { makeStyles } from '@material-ui/core/styles'
17 | import moment from 'moment'
18 | import { refreshConout, cleanConout } from '../../store/conout/actions'
19 | import _ from 'lodash';
20 |
21 | const useStyles = makeStyles(theme => ({
22 | container: {
23 | marginLeft: theme.typography.pxToRem(20),
24 | marginRight: theme.typography.pxToRem(20)
25 | },
26 | panel: {
27 | backgroundColor: fade('#fff', 0.9)
28 | },
29 | heading: {
30 | display: 'flex',
31 | alignItems: 'center',
32 | justifyContent: 'space-between',
33 | fontSize: theme.typography.pxToRem(20),
34 | },
35 | details: {
36 | maxWidth:'95%',
37 | marginLeft: 'auto',
38 | marginRight: 'auto',
39 | maxHeight: 300,
40 | overflow: 'auto',
41 | marginBottom: 20
42 | },
43 | timestamp: {
44 | color: 'grey',
45 | paddingRight: 10,
46 | fontSize: 'smaller'
47 | }
48 | }))
49 |
50 | const ConsolePanel = (props) => {
51 | const classes = useStyles()
52 | const [expanded, setExpanded] = React.useState(false)
53 | const toggleExpansion = (event, newExpanded) => {
54 | setExpanded(newExpanded)
55 | }
56 |
57 | return (
58 |
59 | }
61 | aria-controls='panel1c-content'
62 | >
63 |
64 | Console Output
65 |
66 |
67 |
68 | {props.conout.actions.map((msg, i) =>
69 |
70 | {moment(msg.timestamp).format('L HH:mm:ss.SSS')}
71 | {msg.payload.html
72 | ?
73 | : {msg.payload.txt}
74 | }
75 | {msg.payload.obj && }
76 |
77 | )}
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | )
100 | }
101 |
102 | const TreeView = ({
103 | data,
104 | toggled = false,
105 | name = null,
106 | isLast = true,
107 | isChildElement = false,
108 | isParentToggled = true
109 | }) => {
110 | const [isToggled, setIsToggled] = React.useState(toggled)
111 | const isDataArray = data && Array.isArray(data)
112 | const plainText = !data || (!isDataArray && (data instanceof Error || typeof data !== 'object'))
113 |
114 | return (
115 |
120 | {!_.isEmpty(data) && <>
121 |
setIsToggled(!isToggled)}/>
124 | <> >
125 | >}
126 | {name && {name}: }
127 | {plainText ? (data ? data + '' : (data === null ? 'null' : data)) :
128 | <>
129 | {isDataArray ? '[' : '{'}
130 | {!isToggled && !_.isEmpty(data) && '...'}
131 | {Object.keys(data).map((v, i, a) => {
132 | return typeof data[v] === 'object' ? (
133 |
141 | ) : (
142 |
146 | {isDataArray ? '' : {v}: }
147 |
148 | {i === a.length - 1 ? '' : ','}
149 |
150 | )
151 | })}
152 | {isDataArray ? ']' : '}'}
153 | >
154 | }
155 | {!isLast ? ',' : ''}
156 |
157 | )
158 | }
159 |
160 | const Print = ({ val }) => {
161 | const framing = typeof val === 'string' ? '"' : ''
162 | return framing + val + framing
163 | }
164 |
165 | const mapStateToProps = state => ({
166 | conout: state.conout
167 | })
168 |
169 | const mapDispatchToProps = {refreshConout, cleanConout}
170 |
171 | export default connect(mapStateToProps, mapDispatchToProps)(ConsolePanel)
172 |
--------------------------------------------------------------------------------
/src/components/comparison/EnergyComparisonPanel.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {connect} from 'react-redux'
3 | import {makeStyles} from '@material-ui/core/styles'
4 | import Accordion from '@material-ui/core/Accordion'
5 | import AccordionSummary from '@material-ui/core/AccordionSummary'
6 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
7 | import CompareArrowsIcon from '@material-ui/icons/CompareArrows'
8 | import Typography from '@material-ui/core/Typography'
9 | import {fade} from '@material-ui/core/styles/colorManipulator'
10 | import Table from '@material-ui/core/Table'
11 | import TableBody from '@material-ui/core/TableBody'
12 | import TableCell from '@material-ui/core/TableCell'
13 | import TableHead from '@material-ui/core/TableHead'
14 | import TableRow from '@material-ui/core/TableRow'
15 | import {format} from '../../utils/datetime'
16 | import AdditionalInfo from '../data/energy/AdditionalInfo'
17 | import Type from '../data/energy/Type'
18 | import FuelType from '../data/energy/FuelType'
19 | import ExternalLink from '../data/energy/ExternalLink'
20 | import Geography from '../data/energy/Geography'
21 | import CustomerType from '../data/energy/CustomerType'
22 | import MeteringCharge from '../data/energy/MeteringCharge'
23 | import PlanContract from '../data/energy/PlanContract'
24 |
25 | const useStyles = makeStyles(theme => ({
26 | panel: {
27 | backgroundColor: fade('#fff', 0.9)
28 | },
29 | heading: {
30 | display: 'flex',
31 | alignItems: 'center',
32 | justifyContent: 'space-between',
33 | fontSize: theme.typography.pxToRem(20),
34 | },
35 | table: {
36 | width:'95%',
37 | marginLeft: 'auto',
38 | marginRight: 'auto'
39 | },
40 | headerContainer: {
41 | width: '100%',
42 | display: 'table'
43 | },
44 | dataContainer: {
45 | height: 600,
46 | width: '100%',
47 | overflow: 'auto',
48 | display: 'block'
49 | },
50 | headCell: {
51 | color: fade('#000', 0.9),
52 | fontWeight: 700,
53 | fontSize: '0.8rem'
54 | },
55 | dataCell: {
56 | verticalAlign: 'top'
57 | },
58 | ul: {
59 | margin: 0,
60 | padding:0
61 | }
62 | }))
63 |
64 | const EnergyComparisonPanel = (props) => {
65 | const {dataSources, planSelections} = props
66 | const classes = useStyles()
67 | const planDataKeys = [
68 | {key: 'displayName', label: 'Display Name'},
69 | {key: 'description', label: 'Description'},
70 | {key: 'type', label: 'Type'},
71 | {key: 'fuelType', label: 'Fuel Type'},
72 | {key: 'brand', label: 'Brand'},
73 | {key: 'brandName', label: 'Brand Name'},
74 | {key: 'lastUpdated', label: 'Last Updated'},
75 | {key: 'effectiveFrom', label: 'Effective From'},
76 | {key: 'effectiveTo', label: 'Effective To'},
77 | {key: 'applicationUri', label: 'Application Link'},
78 | {key: 'additionalInformation', label: 'Additional Information'},
79 | {key: 'customerType', label: 'Customer Type'},
80 | {key: 'geography', label: 'Geography'},
81 | {key: 'meteringCharges', label: 'Metering Charges'},
82 | {key: 'gasContract', label: 'Gas Contract'},
83 | {key: 'electricityContract', label: 'Electricity Contract'},
84 | ]
85 |
86 | const render = (plan, key) => {
87 | switch (key) {
88 | case 'displayName':
89 | case 'description':
90 | case 'brand':
91 | case 'brandName':
92 | return plan[key]
93 | case 'lastUpdated':
94 | case 'effectiveFrom':
95 | case 'effectiveTo':
96 | return !!plan[key] ? format(plan[key]) : ''
97 | case 'type':
98 | return
99 | case 'fuelType':
100 | return
101 | case 'applicationUri':
102 | return !!plan[key] && Apply here
103 | case 'additionalInformation':
104 | return !!plan[key] &&
105 | case 'customerType':
106 | return !!plan[key] &&
107 | case 'geography':
108 | return !!plan[key] &&
109 | case 'meteringCharges':
110 | return !!plan[key] &&
111 |
112 | {plan[key].map((meteringCharge, index) => )}
113 |
114 | case 'gasContract':
115 | case 'electricityContract':
116 | return !!plan[key] &&
117 | default:
118 | return ''
119 | }
120 | }
121 |
122 | return (
123 | !!planSelections && planSelections.length > 0 &&
124 |
125 | }
127 | aria-controls='panel1c-content'
128 | >
129 |
130 | Energy Comparison
131 |
132 |
133 |
134 |
135 |
136 |
137 | {planSelections.map((selection, index) =>
138 |
139 | {dataSources[selection.dataSourceIdx].name} - {selection.plan.displayName}
140 | )}
141 |
142 |
143 |
144 |
145 |
146 | {planDataKeys.map(dataKey => (
147 |
148 |
149 | {dataKey.label}
150 |
151 | {planSelections.map((selection, index) =>
152 |
153 | {render(selection.plan, dataKey.key)}
154 |
155 | )}
156 |
157 | ))}
158 |
159 |
160 |
161 | )
162 | }
163 |
164 | const mapStateToProps = state => ({
165 | dataSources: state.dataSources,
166 | planSelections: state.energyComparison
167 | })
168 |
169 | export default connect(mapStateToProps)(EnergyComparisonPanel)
170 |
--------------------------------------------------------------------------------
/src/utils/dict.js:
--------------------------------------------------------------------------------
1 | export const productCategoryDict = {
2 | RESIDENTIAL_MORTGAGES: 'Residential Mortgages',
3 | CRED_AND_CHRG_CARDS: 'Credit and Charge Cards',
4 | PERS_LOANS: 'Personal Loans',
5 | MARGIN_LOANS: 'Margin Loans',
6 | LEASES: 'Leases',
7 | TRADE_FINANCE: 'Trade Finance',
8 | OVERDRAFTS: 'Overdrafts',
9 | BUSINESS_LOANS: 'Business Loans',
10 | TRANS_AND_SAVINGS_ACCOUNTS: 'Transaction and Savings Accounts',
11 | TERM_DEPOSITS: 'Term Deposits',
12 | TRAVEL_CARDS: 'Travel Cards',
13 | REGULATED_TRUST_ACCOUNTS: 'Regulated Trust Accounts'
14 | }
15 |
16 | export const constraintTypeDict = {
17 | MIN_BALANCE: 'Minimum Balance',
18 | MAX_BALANCE: 'Maximum Balance',
19 | OPENING_BALANCE: 'Opening Balance',
20 | MIN_LVR: 'Minimum Loan to Value Ratio',
21 | MAX_LVR: 'Maximum Loan to Value Ratio',
22 | MAX_LIMIT: 'Maximum Credit Limit',
23 | MIN_LIMIT: 'Minimum Credit Limit'
24 | }
25 |
26 | export const depositRateTypeDict = {
27 | FIXED: 'Fixed',
28 | BONUS: 'Bonus',
29 | BUNDLE_BONUS: 'Bundle Bonus',
30 | VARIABLE: 'Variable',
31 | INTRODUCTORY: 'Introductory',
32 | FLOATING: 'Floating',
33 | MARKET_LINKED: 'Market Linked'
34 | }
35 |
36 | export const lendingRateTypeDict = {
37 | FIXED: 'Fixed',
38 | VARIABLE: 'Variable',
39 | INTRODUCTORY: 'Introductory',
40 | FLOATING: 'Floating',
41 | MARKET_LINKED: 'Market Linked',
42 | DISCOUNT: 'Discount',
43 | PENALTY: 'Penalty',
44 | CASH_ADVANCE: 'Cash Advance',
45 | PURCHASE: 'Purchase',
46 | BUNDLE_DISCOUNT_FIXED: 'Bundle Discount Fixed',
47 | BUNDLE_DISCOUNT_VARIABLE: 'Bundle Discount Variable'
48 | }
49 |
50 | export const interestPaymentDueDict = {
51 | IN_ARREARS: 'in Arrears',
52 | IN_ADVANCE: 'in Advance'
53 | }
54 |
55 | export const eligibilityTypeDict = {
56 | BUSINESS: 'Business',
57 | PENSION_RECIPIENT: 'Pension Recipient',
58 | MIN_AGE: 'Minimum Age',
59 | MAX_AGE: 'Maximum Age',
60 | MIN_INCOME: 'Minimum Income',
61 | MIN_TURNOVER: 'Minimum Turnover',
62 | STAFF: 'Staff',
63 | STUDENT: 'Student',
64 | EMPLOYMENT_STATUS: 'Employment Status',
65 | RESIDENCY_STATUS: 'Residency Status',
66 | NATURAL_PERSON: 'Natural Person',
67 | OTHER: 'Other'
68 | }
69 |
70 | export const featureTypeDict = {
71 | CARD_ACCESS: 'Card Access',
72 | CASHBACK_OFFER: 'Cashback Offer',
73 | ADDITIONAL_CARDS: 'Additional Cards',
74 | UNLIMITED_TXNS: 'Unlimited Transactions',
75 | EXTRA_REPAYMENTS: 'Extra Repayments',
76 | FRAUD_PROTECTION: 'Fraud Protection',
77 | FREE_TXNS: 'Free Transactions',
78 | FREE_TXNS_ALLOWANCE: 'Free Transaction Allowance',
79 | GUARANTOR: 'Guarantor',
80 | LOYALTY_PROGRAM: 'Loyalty Program',
81 | OFFSET: 'Offset',
82 | OVERDRAFT: 'Overdraft',
83 | REDRAW: 'Redraw',
84 | INSURANCE: 'Insurance',
85 | BALANCE_TRANSFERS: 'Balance Transfer',
86 | INSTALMENT_PLAN: 'Instalment Plan',
87 | INTEREST_FREE: 'Interest Free',
88 | INTEREST_FREE_TRANSFERS: 'Interest Free Transfers',
89 | DIGITAL_WALLET: 'Digital Wallet',
90 | DIGITAL_BANKING: 'Digital Banking',
91 | NPP_PAYID: 'NPP PayID',
92 | NPP_ENABLED: 'NPP Enabled',
93 | DONATE_INTEREST: 'Donate Interest',
94 | BILL_PAYMENT: 'Bill Payment',
95 | COMPLEMENTARY_PRODUCT_DISCOUNTS: 'Complementary Product Discounts',
96 | BONUS_REWARDS: 'Bonus Rewards',
97 | NOTIFICATIONS: 'Notifications',
98 | RELATIONSHIP_MANAGEMENT: 'Relationship Management',
99 | OTHER: 'Other'
100 | }
101 |
102 | export const feeTypeDict = {
103 | PERIODIC: 'Periodic',
104 | TRANSACTION: 'Transaction',
105 | WITHDRAWAL: 'Withdrawal',
106 | DEPOSIT: 'Deposit',
107 | PAYMENT: 'Payment',
108 | PURCHASE: 'Purchase',
109 | EVENT: 'Event',
110 | UPFRONT: 'Upfront',
111 | VARIABLE: 'Variable',
112 | EXIT: 'Exit'
113 | }
114 |
115 | export const discountTypeDict = {
116 | BALANCE: 'Balance',
117 | DEPOSITS: 'Deposits',
118 | PAYMENTS: 'Payments',
119 | FEE_CAP: 'Fee Cap',
120 | ELIGIBILITY_ONLY: 'Eligibility Only'
121 | }
122 |
123 | export const unitOfMeasureDict = {
124 | DOLLAR: 'Dollars',
125 | PERCENT: 'Percent',
126 | MONTH: 'Month(s)',
127 | DAY: 'Day(s)'
128 | }
129 |
130 | export const rateApplicationMethodDict = {
131 | WHOLE_BALANCE: 'Whole Balance',
132 | PER_TIER: 'Per Tier'
133 | }
134 |
135 | export const repaymentTypeDict = {
136 | INTEREST_ONLY: 'Interest Only',
137 | PRINCIPAL_AND_INTEREST: 'Principal and Interest'
138 | }
139 |
140 | export const loanPurposeDict = {
141 | INVESTMENT: 'Investment',
142 | OWNER_OCCUPIED: 'Owner Occupied'
143 | }
144 |
145 | export const discoveryStatusDict = {
146 | OK: 'OK',
147 | PARTIAL_FAILURE: 'Partial failure',
148 | SCHEDULED_OUTAGE: 'Scheduled outage',
149 | UNAVAILABLE: 'Unavailable'
150 | }
151 |
152 | export const productDataKeys = [
153 | {key: 'description', label: 'Description'},
154 | {key: 'brand', label: 'Brand'},
155 | {key: 'brandName', label: 'Brand Name'},
156 | {key: 'lastUpdated', label: 'Last Updated'},
157 | {key: 'isTailored', label: 'Tailored?'},
158 | {key: 'effectiveFrom', label: 'Effective From'},
159 | {key: 'effectiveTo', label: 'Effective To'},
160 | {key: 'applicationUri', label: 'Application Link'},
161 | {key: 'additionalInformation', label: 'Additional Information'},
162 | {key: 'bundles', label: 'Bundles'},
163 | {key: 'constraints', label: 'Constraints'},
164 | {key: 'depositRates', label: 'Deposit Rates'},
165 | {key: 'lendingRates', label: 'Lending Rates'},
166 | {key: 'eligibility', label: 'Eligibility'},
167 | {key: 'features', label: 'Features'},
168 | {key: 'fees', label: 'Fees'}
169 | ]
170 |
171 | export const translateProductCategory = (category) => productCategoryDict[category]
172 |
173 | export const translateConstraintType = (constraintType) => constraintTypeDict[constraintType]
174 |
175 | export const translateDepositRateType = (depositRateType) => depositRateTypeDict[depositRateType]
176 |
177 | export const translateLendingRateType = (lendingRateType) => lendingRateTypeDict[lendingRateType]
178 |
179 | export const translateInterestPaymentDue = (interestPaymentDue) => interestPaymentDueDict[interestPaymentDue]
180 |
181 | export const translateRepaymentType = (repaymentType) => repaymentTypeDict[repaymentType]
182 |
183 | export const translateloanPurpose = (loanPurpose) => loanPurposeDict[loanPurpose]
184 |
185 | export const translateEligibilityType = (eligibilityType) => eligibilityTypeDict[eligibilityType]
186 |
187 | export const translateFeatureType = (featureType) => featureTypeDict[featureType]
188 |
189 | export const translateFeeType = (feeType) => feeTypeDict[feeType]
190 |
191 | export const translateDiscountType = (discountType) => discountTypeDict[discountType]
192 |
193 | export const translateUnitOfMeasure = (unitOfMeasure) => unitOfMeasureDict[unitOfMeasure]
194 |
195 | export const translateRateApplicationMethod = (rateApplicationMethod) => rateApplicationMethodDict[rateApplicationMethod]
196 |
197 | export const translateDiscoveryStatus = (discoveryStatus) => discoveryStatusDict[discoveryStatus]
198 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Product Comparator (Demo)
2 |
3 | ## Overview
4 |
5 | The Consumer Data Right (CDR) Product Comparator is an [open-source](https://opensource.com/resources/what-open-source), proof of concept (PoC) project, developed in React, for the CDR community. This tool allows users to compare financial products by accessing data directly from unauthenticated APIs of registered Data Holders.
6 |
7 | ## Using the Product Comparator
8 |
9 | The Data Standards Body offers a [live demo](https://consumerdatastandardsaustralia.github.io/product-comparator-demo/) of the Product Comparator, accessible online for immediate use without any setup. This instance enables users to:
10 |
11 | - View a comprehensive list of registered Data Holder Brands and their public details, automatically sourced from the CDR Register and updated within the tool.
12 | - Temporarily add Data Holder brands and their server URLs using the 'Add' button.
13 | - Fetch product data from the unauthenticated CDR product APIs (PRD), health check status data from the Status APIs and scheduled outage data from the Outages APIs for testing and verification.
14 | - Compare products openly offered to the market by the Data Holders, displayed on a user-friendly, responsive webpage.
15 | - Access a detailed log of API calls and responses for debugging Data Holder implementations.
16 |
17 | Additionally, you can set up a local instance of the Product Comparator demo for customised and extended use cases. For more information, refer to the **Local Setup and Customisation** section below.
18 |
19 | ## Local Setup and Customisation
20 |
21 | ### Prerequisites
22 |
23 | Before you begin, ensure you have the following installed:
24 |
25 | - Git, for cloning the repository.
26 | - [Node.js](https://nodejs.org/en/) (v10 or higher).
27 | - npm (Node Package Manager) - **included with Node.js installation**.
28 | - [Yarn](https://yarnpkg.com/) (Optional but preferred) - Javascript package manager
29 |
30 | ### Installation
31 |
32 | 1. Create a fork of this repository. To do this, click the **Fork** button in the top right corner of the GitHub [repository home page](https://consumerdatastandardsaustralia.github.io/product-comparator-demo/).
33 |
34 | 2. After forking the repository, clone it to your local machine. You can do this by running the following command in your terminal or command prompt:
35 |
36 | ```shell
37 | git clone https://github.com/your-username/project-name.git
38 | ```
39 |
40 | Replace **`your-username`** with your GitHub username and **`project-name`** with the name of your repository.
41 |
42 | 3. Once the repository is cloned, navigate to the project directory by running:
43 |
44 | ```
45 | cd project-name
46 | ```
47 |
48 | Replace **`project-name`** with the name of the repository.
49 |
50 | 4. Finally, install all necessary dependencies by running the following command in the project directory:
51 |
52 | ```shell
53 | npm install
54 | ```
55 |
56 | Or, if you prefer using Yarn:
57 |
58 | ```shell
59 | yarn install
60 | ```
61 |
62 |
63 | ### Run
64 |
65 | 1. Start the development server locally by running the following command in the project directory:
66 |
67 | ```shell
68 | npm run start
69 | ```
70 |
71 | Or, if you are using Yarn:
72 |
73 | ```shell
74 | yarn start
75 | ```
76 |
77 | 2. Open your web browser and navigate to http://localhost:3000 to access the CDR Product Comparator (Demo) application.
78 |
79 | ### Build
80 |
81 | 1. Customise the project as needed for your specific use case.
82 | 2. Run `npm run build` OR `yarn build` to build production release
83 |
84 | ## Contribution Process
85 |
86 | We welcome contributions from the community! If you'd like to contribute to this project, please follow these simple steps:
87 |
88 | 1. Create a new branch for your work from the `master` branch:
89 |
90 | ```
91 | git checkout -b feature/your-feature-name
92 | ```
93 |
94 | 2. Begin making your changes or contributions.
95 | 3. Follow the instructions in the project repository to run and test your changes locally.
96 | 4. Commit your changes with clear and concise commit messages.
97 | 5. Push your changes to your forked repository.
98 | 6. Open a pull request (PR) using the _master_ branch in the [original repository](https://github.com/ConsumerDataStandardsAustralia/product-comparator-demo) as the destination branch. Include a detailed description of your changes and the problem you are addressing.
99 | 7. Engage in the discussion on your PR and make any necessary adjustments based on feedback from maintainers and other contributors.
100 | 8. Once your PR is approved and all tests pass, it will be merged into the project.
101 |
102 | ### Note:
103 |
104 | Please ensure your contributions align with our project's objectives and [guidelines](https://d61cds.notion.site/Contribution-Guidelines-8b99d030fea946668fbc75444197e68b?pvs=4).
105 |
106 | ## Reporting Issues
107 |
108 | Encountered an issue? We're here to help. Please visit our [issue reporting guidelines](https://d61cds.notion.site/Issue-Reporting-Guidelines-71a329a0658c4b69a232eab95822509b?pvs=4) for submitting an issue.
109 |
110 | ## Stay Updated
111 |
112 | Join our newsletter to receive the latest updates, release notes, and alerts. [Subscribe here](https://consumerdatastandards.us18.list-manage.com/subscribe?u=fb3bcb1ec5662d9767ab3c414&id=a4414b3906).
113 |
114 | ## License
115 |
116 | The artefact is released under the [MIT License](https://github.com/ConsumerDataRight/mock-register/blob/main/LICENSE), which allows the community to use and modify it freely.
117 |
118 | ## Disclaimer
119 |
120 | The artefacts in this repository are offered without warranty or liability, in accordance with the [MIT licence.](https://github.com/ConsumerDataStandardsAustralia/java-artefacts/blob/master/LICENSE)
121 |
122 | [The Data Standards Body](https://www.csiro.au/en/News/News-releases/2018/Data61-appointed-to-Data-Standards-Body-role) (DSB) develops these artefacts in the course of its work, in order to perform quality assurance on the Australian Consumer Data Right Standards (Data Standards).
123 |
124 | The DSB makes this repository, and its artefacts, public [on a non-commercial basis](https://github.com/ConsumerDataStandardsAustralia/java-artefacts/blob/master/LICENSE) in the interest of supporting the participants in the CDR ecosystem.
125 |
126 | The resources of the DSB are primarily directed towards assisting the [Data Standards Chair](https://consumerdatastandards.gov.au/about/) for [developing the Data Standards](https://github.com/ConsumerDataStandardsAustralia/standards).
127 |
128 | Consequently, the development work provided on the artefacts in this repository is on a best-effort basis, and the DSB acknowledges the use of these tools alone is not sufficient for, nor should they be relied upon with respect to [accreditation](https://www.accc.gov.au/focus-areas/consumer-data-right-cdr-0/cdr-draft-accreditation-guidelines), conformance, or compliance purposes.
--------------------------------------------------------------------------------
/src/components/comparison/BankingComparisonPanel.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {connect} from 'react-redux'
3 | import {makeStyles} from '@material-ui/core/styles'
4 | import Accordion from '@material-ui/core/Accordion'
5 | import AccordionSummary from '@material-ui/core/AccordionSummary'
6 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
7 | import CompareArrowsIcon from '@material-ui/icons/CompareArrows'
8 | import Typography from '@material-ui/core/Typography'
9 | import {fade} from '@material-ui/core/styles/colorManipulator'
10 | import Table from '@material-ui/core/Table'
11 | import TableBody from '@material-ui/core/TableBody'
12 | import TableCell from '@material-ui/core/TableCell'
13 | import TableHead from '@material-ui/core/TableHead'
14 | import TableRow from '@material-ui/core/TableRow'
15 | import {productDataKeys} from '../../utils/dict'
16 | import {format} from '../../utils/datetime'
17 | import AdditionalInfo from '../data/banking/AdditionalInfo'
18 | import ecomp from '../../utils/enum-comp'
19 | import Bundle from '../data/banking/Bundle'
20 | import Constraint from '../data/banking/Constraint'
21 | import DepositRate from '../data/banking/DepositRate'
22 | import LendingRate from '../data/banking/LendingRate'
23 | import Eligibility from '../data/banking/Eligibility'
24 | import Feature from '../data/banking/Feature'
25 | import Fee from '../data/banking/Fee'
26 | import CardArt from '../data/banking/CardArt'
27 |
28 | const useStyles = makeStyles(theme => ({
29 | panel: {
30 | backgroundColor: fade('#fff', 0.9)
31 | },
32 | heading: {
33 | display: 'flex',
34 | alignItems: 'center',
35 | justifyContent: 'space-between',
36 | fontSize: theme.typography.pxToRem(20),
37 | },
38 | table: {
39 | width:'95%',
40 | marginLeft: 'auto',
41 | marginRight: 'auto'
42 | },
43 | headerContainer: {
44 | width: '100%',
45 | display: 'table'
46 | },
47 | dataContainer: {
48 | height: 600,
49 | width: '100%',
50 | overflow: 'auto',
51 | display: 'block'
52 | },
53 | headCell: {
54 | color: fade('#000', 0.9),
55 | fontWeight: 700,
56 | fontSize: '0.8rem'
57 | },
58 | dataCell: {
59 | verticalAlign: 'top'
60 | }
61 | }))
62 |
63 | const render = (product, key) => {
64 | switch (key) {
65 | case 'description':
66 | case 'brand':
67 | case 'brandName':
68 | return product[key]
69 | case 'lastUpdated':
70 | case 'effectiveFrom':
71 | case 'effectiveTo':
72 | return !!product[key] ? format(product[key]) : ''
73 | case 'isTailored':
74 | return product[key] ? 'Yes' : 'No'
75 | case 'applicationUri':
76 | return !!product[key] && Apply here
77 | case 'additionalInformation':
78 | return !!product[key] &&
79 | case 'bundles':
80 | return !!product[key] && product[key].length > 0 &&
81 |
82 | {product[key].sort((a, b)=>ecomp(a.name, b.name)).map((bundle, index) => )}
83 |
84 | case 'constraints':
85 | return !!product[key] && product[key].length > 0 &&
86 |
87 | {product[key].sort((a, b)=>ecomp(a.name, b.name)).map((constraint, index) => )}
88 |
89 | case 'depositRates':
90 | return !!product[key] && product[key].length > 0 &&
91 |
92 | {product[key].sort((a, b)=>ecomp(a.name, b.name)).map((depositRate, index) => )}
93 |
94 | case 'lendingRates':
95 | return !!product[key] && product[key].length > 0 &&
96 |
97 | {product[key].sort((a, b)=>ecomp(a.name, b.name)).map((lendingRate, index) => )}
98 |
99 | case 'eligibility':
100 | return !!product[key] && product[key].length > 0 &&
101 |
102 | {product[key].sort((a, b)=>ecomp(a.name, b.name)).map((eligibility, index) =>)}
103 |
104 | case 'features':
105 | return !!product[key] && product[key].length > 0 &&
106 |
107 | {product[key].sort((a, b)=>ecomp(a.name, b.name)).map((feature, index) => )}
108 |
109 | case 'fees':
110 | return !!product[key] && product[key].length > 0 &&
111 |
112 | {product[key].filter(fee => fee).sort((a, b)=>ecomp(a.name, b.name)).map((fee, index) => )}
113 |
114 | case 'cardArt':
115 | return !!product[key] && product[key].length > 0 &&
116 |
117 | {product[key].map((cardArt, index) => )}
118 |
119 | default:
120 | return ''
121 | }
122 | }
123 |
124 | const ComparisonPanel = (props) => {
125 | const {dataSources, products} = props
126 | const classes = useStyles()
127 | return (
128 | !!products && products.length > 0 &&
129 |
130 | }
132 | aria-controls='panel1c-content'
133 | >
134 |
135 | Product Comparison
136 |
137 |
138 |
139 |
140 |
141 |
142 | {products.map((productData, index) =>
143 |
144 | {dataSources[productData.dataSourceIdx].name} - {productData.product.name}
145 | )}
146 |
147 |
148 |
149 |
150 |
151 | {productDataKeys.map(dataKey => (
152 |
153 |
154 | {dataKey.label}
155 |
156 | {products.map((productData, index) =>
157 |
158 | {render(productData.product, dataKey.key)}
159 |
160 | )}
161 |
162 | ))}
163 |
164 |
165 |
166 | )
167 | }
168 |
169 | const mapStateToProps = state => ({
170 | dataSources: state.dataSources,
171 | products: state.bankingComparison
172 | })
173 |
174 | export default connect(mapStateToProps)(ComparisonPanel)
175 |
--------------------------------------------------------------------------------
/src/components/data/energy/EnergyPanel.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {connect} from 'react-redux'
3 | import {makeStyles} from '@material-ui/core/styles'
4 | import Accordion from '@material-ui/core/Accordion'
5 | import AccordionSummary from '@material-ui/core/AccordionSummary'
6 | import AccordionActions from '@material-ui/core/AccordionActions'
7 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
8 | import SubjectIcon from '@material-ui/icons/Subject'
9 | import Typography from '@material-ui/core/Typography'
10 | import Divider from '@material-ui/core/Divider'
11 | import CompareIcon from '@material-ui/icons/Compare'
12 | import { fade } from '@material-ui/core/styles/colorManipulator'
13 | import Fab from '@material-ui/core/Fab'
14 | import Grid from '@material-ui/core/Grid'
15 | import Radio from '@material-ui/core/Radio'
16 | import RadioGroup from '@material-ui/core/RadioGroup'
17 | import FormControlLabel from '@material-ui/core/FormControlLabel'
18 | import FormLabel from '@material-ui/core/FormLabel'
19 | import EnergyPlanList from './EnergyPlanList'
20 | import {startRetrievePlanList, retrievePlanList, clearData} from '../../../store/energy/data'
21 | import {normalise} from '../../../utils/url'
22 | import {comparePlans} from '../../../store/energy/comparison'
23 |
24 | const useStyles = makeStyles(theme => ({
25 | container: {
26 | marginLeft: theme.typography.pxToRem(20),
27 | marginRight: theme.typography.pxToRem(20)
28 | },
29 | panel: {
30 | backgroundColor: fade('#fff', 0.9)
31 | },
32 | heading: {
33 | display: 'flex',
34 | alignItems: 'center',
35 | justifyContent: 'space-between',
36 | fontSize: theme.typography.pxToRem(20),
37 | },
38 | details: {
39 | maxWidth:'95%',
40 | marginLeft: 'auto',
41 | marginRight: 'auto',
42 | marginBottom: 20
43 | },
44 | button: {
45 | margin: theme.spacing(1)
46 | },
47 | leftIcon: {
48 | marginRight: theme.spacing(1),
49 | }
50 | }))
51 |
52 | const EnergyPanel = (props) => {
53 | const {dataSources, savedDataSourcesCount, versionInfo} = props
54 | const classes = useStyles()
55 | const [expanded, setExpanded] = React.useState(true)
56 | const [effective, setEffective] = React.useState('CURRENT')
57 | const [fuelType, setFuelType] = React.useState('GAS')
58 | const compare = () => {
59 | if( /Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) {
60 | alert('The screen size is too small! Please use a bigger screen to compare.')
61 | return
62 | }
63 | props.comparePlans(props.selectedPlans)
64 | setExpanded(false)
65 | }
66 |
67 | React.useEffect(() => {
68 | dataSources.forEach((dataSource, dataSourceIndex) => {
69 | if (isEnergyDataSource(dataSource)) {
70 | props.startRetrievePlanList(dataSourceIndex)
71 | const normalisedUrl = normalise(dataSource.energyPrd ? dataSource.energyPrd : dataSource.url)
72 | const planListUrl = normalisedUrl + '/energy/plans?effective=' + effective + '&fuelType=' + fuelType
73 | props.retrievePlanList(dataSourceIndex, normalisedUrl, planListUrl, versionInfo.xV, versionInfo.xMinV, effective, fuelType)
74 | }
75 | })
76 | return function() {
77 | dataSources.forEach((dataSource, dataSourceIndex) => {
78 | if (isEnergyDataSource(dataSource)) {
79 | props.clearData(dataSourceIndex)
80 | }
81 | })
82 | }
83 | // eslint-disable-next-line react-hooks/exhaustive-deps
84 | }, [effective, fuelType, versionInfo.xV, versionInfo.xMinV, savedDataSourcesCount])
85 |
86 | const getWidth = (dataSourceCount, min) => {
87 | return Math.max(12 / dataSourceCount, min)
88 | }
89 |
90 | return (
91 | setExpanded(newExpanded)}>
92 | }
94 | aria-controls='panel1c-content'
95 | >
96 |
97 | Plans
98 |
99 |
100 |
101 |
102 | Effective
103 | setEffective(e.target.value)}>
104 | } label="Current" />
105 | } label="Future" />
106 | } label="All" />
107 |
108 |
109 |
110 | Fuel type
111 | setFuelType(e.target.value)}>
112 | } label="Electricity" />
113 | } label="Gas" />
114 | } label="Dual" />
115 | } label="All" />
116 |
117 |
118 |
119 |
120 | {
121 | savedDataSourcesCount > 0 &&
122 |
123 | {dataSources.map((dataSource, index) => (isEnergyDataSource(dataSource) &&
124 |
131 | {!!dataSource.icon &&
}
{dataSource.name}
132 |
133 |
134 | ))}
135 |
136 | }
137 |
138 |
139 |
140 | 4}
142 | className={classes.button} onClick={compare}>
143 |
144 | Compare
145 |
146 |
147 |
148 | )
149 | }
150 |
151 | function isEnergyDataSource(dataSource) {
152 | return !dataSource.unsaved && !dataSource.deleted && dataSource.enabled && (!dataSource.sectors || dataSource.sectors.includes("energy"))
153 | }
154 |
155 | const mapStateToProps = state=>({
156 | dataSources : state.dataSources,
157 | savedDataSourcesCount: state.dataSources.filter(dataSource => isEnergyDataSource(dataSource)).length,
158 | selectedPlans: state.energySelection,
159 | versionInfo: state.versionInfo.vHeaders
160 | })
161 |
162 | const mapDispatchToProps = {startRetrievePlanList, retrievePlanList, clearData, comparePlans}
163 |
164 | export default connect(mapStateToProps, mapDispatchToProps)(EnergyPanel)
165 |
--------------------------------------------------------------------------------
/src/components/data-source/DataSourcePanel.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {connect} from 'react-redux'
3 | import {withStyles} from '@material-ui/core/styles'
4 | import Accordion from '@material-ui/core/Accordion'
5 | import AccordionSummary from '@material-ui/core/AccordionSummary'
6 | import AccordionActions from '@material-ui/core/AccordionActions'
7 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
8 | import SyncIcon from '@material-ui/icons/Sync'
9 | import EditIcon from '@material-ui/icons/Edit'
10 | import DoneOutlineIcon from '@material-ui/icons/DoneOutline'
11 | import IconButton from '@material-ui/core/IconButton'
12 | import AccountBalanceIcon from '@material-ui/icons/AccountBalance'
13 | import Typography from '@material-ui/core/Typography'
14 | import Divider from '@material-ui/core/Divider'
15 | import PlayListAddIcon from '@material-ui/icons/PlaylistAdd'
16 | import Grid from '@material-ui/core/Grid'
17 | import DataSource from './DataSource'
18 | import TextField from '@material-ui/core/TextField'
19 | import Autocomplete from '@material-ui/lab/Autocomplete'
20 | import { fade } from '@material-ui/core/styles/colorManipulator'
21 | import { loadDataSource, addDataSource, syncDataSources } from '../../store/data-source'
22 | import { loadVersionInfo, saveVersionInfo, setVersionsEditable, setVersionsReadOnly } from '../../store/version-info'
23 | import { startRetrieveProductList, retrieveProductList, clearData } from '../../store/banking/data'
24 | import { clearSelection} from '../../store/banking/selection'
25 | import { normalise } from '../../utils/url'
26 | import Fab from '@material-ui/core/Fab'
27 | import Tooltip from '@material-ui/core/Tooltip'
28 |
29 | const styles = theme => ({
30 | panel: {
31 | backgroundColor: fade('#fff', 0.9)
32 | },
33 | heading: {
34 | display: 'flex',
35 | alignItems: 'center',
36 | justifyContent: 'space-between',
37 | fontSize: theme.typography.pxToRem(20),
38 | },
39 | details: {
40 | maxWidth: '80%',
41 | marginLeft: 'auto',
42 | marginRight: 'auto',
43 | marginBottom: 20
44 | },
45 | version: {
46 | textAlign: 'center',
47 | '& .MuiTextField-root': {
48 | margin: theme.spacing(2),
49 | width: '20ch'
50 | },
51 | },
52 | leftIcon: {
53 | marginRight: theme.spacing(1),
54 | }
55 | })
56 |
57 | class DataSourcePanel extends React.Component {
58 |
59 | componentDidMount() {
60 | this.props.loadDataSource()
61 | this.props.loadVersionInfo()
62 | }
63 |
64 | render() {
65 | const {classes, dataSources, addDataSource, syncDataSources, vHeaders} = this.props
66 | const versions = ['1', '2', '3', '4', '5']
67 | let {xV, xMinV} = vHeaders
68 |
69 | const updateVersions = () => {
70 | if (xV && xMinV && (xV !== vHeaders.xV || xMinV !== vHeaders.xMinV)) {
71 | this.props.saveVersionInfo({xV, xMinV})
72 | dataSources.forEach((dataSource, dataSourceIndex) => {
73 | if (!dataSource.unsaved && !dataSource.deleted && dataSource.enabled) {
74 | this.props.clearSelection(dataSourceIndex)
75 | this.props.clearData(dataSourceIndex)
76 | this.props.startRetrieveProductList(dataSourceIndex)
77 | const normalisedUrl = normalise(dataSource.url)
78 | this.props.retrieveProductList(dataSourceIndex, normalisedUrl, normalisedUrl + '/banking/products', xV, xMinV)
79 | }
80 | })
81 | } else {
82 | this.props.setVersionsReadOnly()
83 | }
84 | }
85 |
86 | return (
87 |
88 | }
90 | aria-controls='panel1c-content'
91 | >
92 |
95 |
96 |
97 | { dataSources.length > 0 &&
98 |
99 |
100 | Enabled
101 | Name
102 | API base url
103 | Icon url
104 |
105 | {dataSources.map((dataSource, index) =>
106 | !dataSource.deleted && )}
107 |
108 | }
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 | {this.props.readOnly ?
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 | :
132 |
133 |
(
139 |
140 | )}
141 | onInputChange={(ev, value) => xV = value}
142 | style={{display: 'inline'}}
143 | />
144 | (
150 |
151 | )}
152 | onInputChange={(ev, value) => xMinV = value}
153 | style={{display: 'inline'}}
154 | />
155 |
156 |
157 |
158 |
159 |
160 |
161 | }
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 | )
174 | }
175 | }
176 |
177 | const mapStateToProps = state => ({
178 | dataSources: state.dataSources,
179 | vHeaders: state.versionInfo.vHeaders,
180 | readOnly: !state.versionInfo.editable
181 | })
182 |
183 | const mapDispatchToProps = {
184 | loadDataSource,
185 | addDataSource,
186 | syncDataSources,
187 | loadVersionInfo,
188 | saveVersionInfo,
189 | setVersionsEditable,
190 | setVersionsReadOnly,
191 | startRetrieveProductList,
192 | retrieveProductList,
193 | clearSelection,
194 | clearData
195 | }
196 |
197 | export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(DataSourcePanel))
198 |
--------------------------------------------------------------------------------
/src/components/data/banking/Product.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {connect} from 'react-redux'
3 | import MuiAccordion from '@material-ui/core/Accordion'
4 | import MuiAccordionSummary from '@material-ui/core/AccordionSummary'
5 | import {makeStyles, withStyles} from '@material-ui/core'
6 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
7 | import Typography from '@material-ui/core/Typography'
8 | import Bundle from './Bundle'
9 | import Constraint from './Constraint'
10 | import DepositRate from './DepositRate'
11 | import LendingRate from './LendingRate'
12 | import Eligibility from './Eligibility'
13 | import Feature from './Feature'
14 | import Fee from './Fee'
15 | import CardArt from './CardArt'
16 | import Checkbox from '@material-ui/core/Checkbox'
17 | import {deselectProduct, selectProduct} from '../../../store/banking/selection'
18 | import DateTime from '../DateTime'
19 | import AdditionalInfo from './AdditionalInfo'
20 | import ecomp from '../../../utils/enum-comp'
21 |
22 | const useStyles = makeStyles(() => ({
23 | root: {
24 | display: 'flex',
25 | alignItems: 'flex-start'
26 | },
27 | details: {
28 | display: 'block',
29 | lineHeight: '1.8rem',
30 | paddingRight: 40
31 | },
32 | datetime: {
33 | textDecoration: 'underline'
34 | },
35 | sectionTitle: {
36 | fontStyle: 'italic'
37 | },
38 | sectionContent: {
39 | marginTop: 0,
40 | marginBottom: 0,
41 | paddingLeft: 20
42 | }
43 | }))
44 |
45 | const Accordion = withStyles({
46 | root: {
47 | width: '100%',
48 | backgroundColor: 'transparent',
49 | boxShadow: 'none',
50 | '&:not(:last-child)': {
51 | borderBottom: 0,
52 | },
53 | '&:before': {
54 | display: 'none',
55 | },
56 | '&$expanded': {
57 | margin: 'auto',
58 | },
59 | },
60 | expanded: {},
61 | })(MuiAccordion)
62 |
63 | const AccordionSummary = withStyles({
64 | root: {
65 | paddingLeft: 0,
66 | paddingRight: 24,
67 | alignItems: 'flex-start',
68 | backgroundColor: 'transparent',
69 | marginBottom: -1,
70 | maxHeight: 36,
71 | minHeight: 24,
72 | '&$expanded': {
73 | maxHeight: 36,
74 | minHeight: 24,
75 | }
76 | },
77 | content: {
78 | margin: '8px 0',
79 | '&$expanded': {
80 | margin: '8px 0',
81 | },
82 | },
83 | expandIcon: {
84 | paddingTop: 8,
85 | '&$expanded': {
86 | paddingTop: 8
87 | }
88 | },
89 | expanded: {},
90 | })(MuiAccordionSummary)
91 |
92 | const Product = (props) => {
93 | const classes = useStyles()
94 | const {product, dataSourceIndex, selectedProducts} = props
95 | const selected = selectedProducts.filter(
96 | prd=>(prd.dataSourceIdx === dataSourceIndex && prd.product.productId === product.productId)).length > 0
97 | const handleChange = event => {
98 | event.target.checked ? props.selectProduct(dataSourceIndex, product) : props.deselectProduct(dataSourceIndex, product)
99 | }
100 | const blob = new Blob([JSON.stringify(product, null, 2)], {type : 'application/json'})
101 | return (
102 |
103 |
108 |
109 | }
111 | aria-controls='panel1c-content'
112 | >
113 | {product.name}
114 |
115 |
116 |
{product.description}
117 |
Brand: {product.brand} {!!product.bandName && ({product.bandName}) }
118 |
119 |
{product.isTailored ? 'Tailored' : 'Not Tailored'}
120 | {!!product.effectiveFrom &&
Effective from
}
121 | {!!product.effectiveTo &&
Effective to
}
122 | {!!product.applicationUri &&
}
123 | {
124 | !!product.additionalInformation &&
125 |
126 |
Additional Information:
127 |
128 |
129 | }
130 | {
131 | !!product.bundles && product.bundles.length > 0 &&
132 |
133 |
Bundles:
134 |
135 | {product.bundles.sort((a, b)=>ecomp(a.name, b.name)).map(
136 | (bundle, index) => )}
137 |
138 |
139 | }
140 | {
141 | !!product.constraints && product.constraints.length > 0 &&
142 |
143 |
Constraints:
144 |
145 | {product.constraints.sort((a, b)=>ecomp(a.constraintType, b.constraintType)).map(
146 | (constraint, index) => )}
147 |
148 |
149 | }
150 | {
151 | !!product.depositRates && product.depositRates.length > 0 &&
152 |
153 |
Deposit Rates:
154 |
155 | {product.depositRates.sort((a, b)=>ecomp(a.depositRateType, b.depositRateType)).map(
156 | (depositRate, index) => )}
157 |
158 |
159 | }
160 | {
161 | !!product.lendingRates && product.lendingRates.length > 0 &&
162 |
163 |
Lending Rates:
164 |
165 | {product.lendingRates.sort((a, b)=>ecomp(a.lendingRateType, b.lendingRateType)).map(
166 | (lendingRate, index) => )}
167 |
168 |
169 | }
170 | {
171 | !!product.eligibility && product.eligibility.length > 0 &&
172 |
173 |
Eligibilities:
174 |
175 | {product.eligibility.sort((a, b)=>ecomp(a.eligibilityType, b.eligibilityType)).map(
176 | (eligibility, index) => )}
177 |
178 |
179 | }
180 | {
181 | !!product.features && product.features.length > 0 &&
182 |
183 |
Features:
184 |
185 | {product.features.sort((a, b)=>ecomp(a.featureType, b.featureType)).map(
186 | (feature, index) => )}
187 |
188 |
189 | }
190 | {
191 | !!product.fees && product.fees.length > 0 &&
192 |
193 |
Fees:
194 |
195 | {product.fees.filter(fee => fee).sort((a, b)=>ecomp(a.feeType, b.feeType)).map(
196 | (fee, index) => )}
197 |
198 |
199 | }
200 | {
201 | !!product.cardArt && product.cardArt.length > 0 &&
202 |
203 |
Card Art:
204 |
205 | {product.cardArt.map((cardArt, index) =>
206 |
207 | )}
208 |
209 |
210 | }
211 |
212 |
213 |
214 | )
215 | }
216 |
217 | const mapStateToProps = state => ({
218 | selectedProducts: state.bankingSelection
219 | })
220 |
221 | const mapDispatchToProps = { selectProduct, deselectProduct }
222 |
223 | export default connect(mapStateToProps, mapDispatchToProps)(Product)
224 |
--------------------------------------------------------------------------------
/src/components/data-source/DataSource.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { makeStyles, withStyles } from '@material-ui/core/styles'
3 | import Grid from '@material-ui/core/Grid'
4 | import Checkbox from '@material-ui/core/Checkbox'
5 | import TextField from '@material-ui/core/TextField'
6 | import IconButton from '@material-ui/core/IconButton'
7 | import DoneOutlineIcon from '@material-ui/icons/DoneOutline'
8 | import DeleteIcon from '@material-ui/icons/Delete'
9 | import Tooltip from '@material-ui/core/Tooltip'
10 | import {
11 | saveDataSource,
12 | deleteDataSource,
13 | enableDataSource,
14 | modifyDataSourceName,
15 | modifyDataSourceIcon,
16 | modifyDataSourceEnergyPrdUrl,
17 | modifyDataSourceUrl
18 | } from '../../store/data-source'
19 | import { clearSelection} from '../../store/banking/selection'
20 | import { deleteData, clearData } from '../../store/banking/data'
21 | import {connect} from 'react-redux'
22 | import isUrl from '../../utils/url'
23 | import Snackbar from '@material-ui/core/Snackbar'
24 | import CloseIcon from '@material-ui/icons/Close'
25 | import ErrorIcon from '@material-ui/icons/Error'
26 | import {SnackbarContent} from '@material-ui/core'
27 | import MuiAccordion from '@material-ui/core/Accordion'
28 | import MuiAccordionSummary from '@material-ui/core/AccordionSummary'
29 | import MuiAccordionDetails from '@material-ui/core/AccordionDetails'
30 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
31 | import FormControlLabel from '@material-ui/core/FormControlLabel'
32 |
33 | const Accordion = withStyles({
34 | root: {
35 | '&:before': {
36 | display: 'none'
37 | },
38 | boxShadow: 'none',
39 | '&.Mui-expanded': {
40 | boxShadow: '0 2px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 1px 3px 0px rgb(0 0 0 / 12%)'
41 | }
42 | }
43 | })(MuiAccordion)
44 |
45 | const AccordionSummary = withStyles({
46 | content: {
47 | margin: 0
48 | }
49 | })(MuiAccordionSummary)
50 |
51 | const AccordionDetails = withStyles({
52 | root: {
53 | padding: 0
54 | }
55 | })(MuiAccordionDetails)
56 |
57 | const useStyles = makeStyles((theme) => ({
58 | buttonContainer: {
59 | display: 'flex',
60 | alignItems: 'flex-end'
61 | },
62 | snackbar: {
63 | backgroundColor: theme.palette.error.dark
64 | },
65 | message: {
66 | display: 'flex',
67 | alignItems: 'center'
68 | },
69 | fieldLabel: {
70 | width: '95%'
71 | },
72 | icon: {
73 | marginRight: theme.spacing(2)
74 | }
75 | }))
76 |
77 | const DataSource = (props) => {
78 |
79 | const classes = useStyles()
80 |
81 | const { dataSource, index } = props
82 |
83 | const [errorState, setErrorState] = React.useState({open: false, errorMessage: ''})
84 |
85 | const handleChange = name => event => {
86 | if (name === 'name') {
87 | props.modifyDataSourceName(index, {...dataSource, [name]: event.target.value})
88 | } else if (name === 'url') {
89 | props.modifyDataSourceUrl(index, {...dataSource, [name]: event.target.value})
90 | if (!dataSource.unsaved) {
91 | props.clearSelection(index)
92 | props.clearData(index)
93 | }
94 | } else if (name === 'energyPrd') {
95 | props.modifyDataSourceEnergyPrdUrl(index, {...dataSource, [name]: event.target.value})
96 | if (!dataSource.unsaved) {
97 | props.clearSelection(index)
98 | props.clearData(index)
99 | }
100 | } else if (name === 'icon') {
101 | props.modifyDataSourceIcon(index, {...dataSource, [name]: event.target.value})
102 | } else if (name === 'enabled') {
103 | props.enableDataSource(index, {...dataSource, [name]: event.target.checked})
104 | if (dataSource.enabled) {
105 | props.clearSelection(index)
106 | props.clearData(index)
107 | }
108 | }
109 | }
110 |
111 | const save = ev => {
112 | if (!isDataSourceValid()) {
113 | let message = ''
114 | if (dataSource.name.trim().length === 0) {
115 | message = 'Bank name is required. '
116 | }
117 | if (!isUrl(dataSource.url)) {
118 | message += 'URL is invalid.'
119 | }
120 | setErrorState({
121 | open: true,
122 | errorMessage: message
123 | })
124 | } else {
125 | props.saveDataSource(index, {...dataSource})
126 | }
127 | ev.stopPropagation()
128 | ev.preventDefault()
129 | }
130 |
131 | const closeErrorMessage = () => {
132 | setErrorState({...errorState, open: false})
133 | }
134 |
135 | const del = () => {
136 | props.deleteDataSource(index)
137 | props.deleteData(index)
138 | props.clearSelection(index)
139 | }
140 |
141 | const isDataSourceValid = () => {
142 | return dataSource.name.trim().length > 0 && isUrl(dataSource.url)
143 | }
144 |
145 | const ignore = ev => ev.stopPropagation()
146 |
147 | return (
148 |
149 | }
151 | aria-controls='panel1c-content'
152 | >
153 |
154 |
155 | }
163 | />
164 |
165 |
166 | }
179 | />
180 |
181 |
182 | }
195 | />
196 |
208 | {errorState.errorMessage}}
211 | action={[
212 |
213 |
214 |
215 | ]}
216 | />
217 | }
218 | />
219 |
220 |
221 | }
233 | />
234 |
235 |
236 | { dataSource.unsaved ?
237 |
242 |
243 |
244 |
245 | }
246 | /> :
247 |
252 |
253 |
254 |
255 | }
256 | />
257 | }
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
275 |
276 |
277 |
278 | )
279 | }
280 |
281 | const mapDispatchToProps = {
282 | saveDataSource,
283 | deleteDataSource,
284 | enableDataSource,
285 | modifyDataSourceName,
286 | modifyDataSourceUrl,
287 | modifyDataSourceEnergyPrdUrl,
288 | modifyDataSourceIcon,
289 | clearSelection,
290 | deleteData,
291 | clearData
292 | }
293 |
294 | export default connect(null, mapDispatchToProps)(DataSource)
295 |
--------------------------------------------------------------------------------