├── 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 |
5 |
{title}
6 | 9 |
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 &&
More info
} 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 &&
    More info
    } 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 &&
    More info
    } 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 | 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 | CDS logo 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 |
    28 | 29 | 30 | {cardArt.imageUri} 31 | 32 |
    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 | 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 | 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 | 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 &&
    More info
    } 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 | 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 &&
    More info
    } 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 &&
    More info
    } 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 | 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 | 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 |
    56 | 57 |
    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 |
      38 | {timeOfUse.map(({days, startTime, endTime, additionalInfo, additionalInfoUri}, index) => ( 39 |
    • 40 | {days && ( 41 |
      Days: {days.join(', ')}
      42 | )} 43 | {startTime && ( 44 |
      Start Time: {startTime}
      45 | )} 46 | {endTime && ( 47 |
      End Time: {endTime}
      48 | )} 49 | {additionalInfo && ( 50 |
      Additional Info: {additionalInfo}
      51 | )} 52 | {additionalInfoUri && ( 53 | 54 | )} 55 |
    • 56 | ))} 57 |
    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 |
      49 | {timeVaryingTariffs.map(({type, displayName, rates, period, timeVariations}, index) => ( 50 |
    • 51 | {type && ( 52 |
      Type: {type}
      53 | )} 54 |
      Display Name: {displayName}
      55 | {rates && ( 56 | <> 57 |
      Rates:
      58 |
        59 | {rates.map((rate, index) => )} 60 |
      61 | 62 | )} 63 |
      Period:
      64 | <> 65 |
      Time Variations:
      66 |
        67 | {timeVariations.map(({days, startTime, endTime}, index) => ( 68 |
      • 69 |
        Days: {days.join(', ')}
        70 | {startTime && ( 71 |
        Start Time: {startTime}
        72 | )} 73 | {endTime && ( 74 |
        End Time: {endTime}
        75 | )} 76 |
      • 77 | ))} 78 |
      79 | 80 |
    • 81 | ))} 82 |
    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 |
    AEMO
    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 |
    144 | 145 |
    146 |
    147 | } 148 | { 149 | !!plan.electricityContract && 150 |
    151 |
    Electricity Contract:
    152 |
    153 | 154 |
    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 |
    93 | Data sources 94 |
    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 |
    Last updated at JSON
    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 | --------------------------------------------------------------------------------