We start by looking at the .zul ZUML component definitions, then following we will see the detail grails composer code found in "BillAuditToolComposer.groovy".
Note: disregard the extra closing tags displayed in the ZUML. Blogger automatically, adds those.
The main page starts with a standard .zul header and "window" component tab.
Note: much of the following detail could have been defined programatically inside the grails composer for this zul page. However, this was the first page I started with and was still learning the ins and outs of the ZKGrails builder code. I use more of the programatic logic in the Int'l and Dom 'breakout' pages, which detail the data by month.
The composer contains two major methods for processing and displaying the Domestic and International calling data pulled from the DB. It also, provides several methods for building and displaying the related charts shown in the 2 chart tabs.
Start by injecting a few components into the composer. "Col_month" correspond to ZK components in the corresponding .zul page.
Most of the backend of the app is defined in the obligatory afterCompose method.
First, get the data we need and build the mail data structure. The app tries to pull a year of data if it exits and uses it to dynamically build the needed ZK components. Since, each row of data on the main page is a monthly summary of information, and because there's "Least Cost Router" (LCR) reported billing information and VoIP vendor reported information, this step is pretty complex.
We now process and display Int'l calling data into ZK rows using ZKGrails "rows" & "row" components:
Next, we process the "Domestic" data, building the row components to be displayed under the domestic tab in the main page.
Here we define the logic for displaying the data labels using the check box component under the Int'l Chart. This features was added because we found the data labels sometimes overlapped, and removing the labels sometimes made it easier to read the chart.
Note: disregard the extra closing tags displayed in the ZUML. Blogger automatically, adds those.
The main page starts with a standard .zul header and "window" component tab.
To wrap all the various kinds of data and charts into tabs, we first define a "tabbox" component, which creates just the visual "tabs" and the label displayed on each. Once those are defined a "tabpanel" is defined, which is rendered under each tab respectively.![]()
We define our first "tabpanels" container component for each of the following "tabpanel" components.
Note: much of the following detail could have been defined programatically inside the grails composer for this zul page. However, this was the first page I started with and was still learning the ins and outs of the ZKGrails builder code. I use more of the programatic logic in the Int'l and Dom 'breakout' pages, which detail the data by month.
The definition of our next tabpanel. It is just a chart of the previous data, and uses the ZK img tag and composer methods show far below. See the ZKGrails builder jQuery-like call $('#intlChart01').
The details of this tabpanel are rendered via an "include" component, which calls an entirely different .zul page; a .zul page that has its own grails composer and methods.![]()
This begins the definition of our summarized domestic calling data tabpanel.
We define our domestic data chart tabpanel.
Again, rather than coding all the complexity of our monthly breakouts, we are just including an entirely separate .zul page that has its own grails composer backing it.![]()
We include an entirely separate .zul page, which wraps our Grails scaffolding created CRUD data entry and editing pages.
We now close our tabpanels contain component.
Composer Details
The composer contains two major methods for processing and displaying the Domestic and International calling data pulled from the DB. It also, provides several methods for building and displaying the related charts shown in the 2 chart tabs.
Start by injecting a few components into the composer. "Col_month" correspond to ZK components in the corresponding .zul page.
class BillAuditToolComposer extends GrailsComposer { def chartService def col_month def col_month2 def addDataLabel01 = false def addDataLabel02 = false
Most of the backend of the app is defined in the obligatory afterCompose method.
First, get the data we need and build the mail data structure. The app tries to pull a year of data if it exits and uses it to dynamically build the needed ZK components. Since, each row of data on the main page is a monthly summary of information, and because there's "Least Cost Router" (LCR) reported billing information and VoIP vendor reported information, this step is pretty complex.
def afterCompose = { window -> /** * Prepare date range for 1 year of data * */ Calendar cal = Calendar.getInstance() cal.add(Calendar.HOUR, +8) Calendar cal2 = Calendar.getInstance() cal2.add(Calendar.YEAR, -1) Date today = cal.getTime() Date lastYear = cal2.getTime() /** * Build Query and get results * */ def query = VendorStats.where{ period in (lastYear .. today) } def results = query.list()The data is divided into 4 main structures.
/** * Gather data into the models * * I = Int'l value * D = Domestic value * L = LCR value * A = Actual bill value */ def IL = [:] def IA = [:] def DL = [:] def DA = [:]Since the main tabs summarize the International and Domestic calling data, logic is needed to sum the data for each of the the given months.
results.each{ vs -> def month = new SimpleDateFormat("yyyy-MM").format(vs.period) def month1 = new SimpleDateFormat("MMM").format(vs.period) def IL_label = "Int'l LCR" def IA_label = "Int'l Actual" def DL_label = "Domestic LCR" def DA_label = "Domestic Actual" if (! IL[month]){ IL[month] = ["label":IL_label, "month":month1, "minutes":0, "cost":0]} if (! IA[month]){ IA[month] = ["label":IA_label, "month":month1, "minutes":0, "cost":0]} if (! DL[month]){ DL[month] = ["label":DL_label, "month":month1, "minutes":0, "cost":0]} if (! DA[month]){ DA[month] = ["label":DA_label, "month":month1, "minutes":0, "cost":0]} if (vs.vendor.intl){ //Intl if (vs.lcr){ //LCR def m = vs.minutes def c = vs.cost IL[month]["minutes"] = IL[month]["minutes"] + m IL[month]["cost"] = IL[month]["cost"] + c } else { //Actual def m = vs.minutes def c = vs.cost IA[month]["minutes"] = IA[month]["minutes"] + m IA[month]["cost"] = IA[month]["cost"] + c } } else { //Domestic if (vs.lcr){ //LCR def m = vs.minutes def c = vs.cost DL[month]["minutes"] = DL[month]["minutes"] + m DL[month]["cost"] = DL[month]["cost"] + c } else { //Actual def m = vs.minutes def c = vs.cost DA[month]["minutes"] = DA[month]["minutes"] + m DA[month]["cost"] = DA[month]["cost"] + c } } }
Display the International Call Data
We now process and display Int'l calling data into ZK rows using ZKGrails "rows" & "row" components:
/** * Build the Int'l data tab * */ $('#rows01').append{ IL.each{ m, v -> def red = "color:red" def I_min_diff = 1 def I_cost_diff = 1 try { if ( IA[m]["minutes"] ){ I_min_diff = IL[m]["minutes"] / ( IA[m]["minutes"] ) } if ( IA[m]["cost"] ){ I_cost_diff = IL[m]["cost"] / ( IA[m]["cost"] ) } String moneyPat = "\$##,###.##" String minutesPat = "##,###" String percentPat = "##.##%" def IL_minutes = numFormer( IL[m]["minutes"], minutesPat ) def IL_cost = numFormer( IL[m]["cost"], moneyPat ) def IA_minutes = numFormer( IA[m]["minutes"], minutesPat ) def IA_cost = numFormer( IA[m]["cost"], moneyPat ) def string_I_min_diff = numFormer( I_min_diff, percentPat ) def string_I_cost_diff = numFormer( I_cost_diff, percentPat )The data has just been formatted and now the first row component is built. Note: there is a conditional to color the firgures red if it exceeds a 100%.
row { label(value:m) label(value:IL_minutes) label(value:IL_cost) label(value:IA_minutes) label(value:IA_cost) if( I_min_diff > 1 ){ label(value:string_I_min_diff, style:red) } else { label(value:string_I_min_diff) } if( I_cost_diff > 1 ){ label(value:string_I_cost_diff, style:red) } else { label(value:string_I_cost_diff) } } } catch( NullPointerException e ){ println "caught: " + e } } col_month.sort(false) }
Display the Domestic Call Data
Next, we process the "Domestic" data, building the row components to be displayed under the domestic tab in the main page.
/** * Build the Domestic data tab * */ $('#rows02').append{ DL.each{ m, v -> def red = "color:red" def D_min_diff = 1 def D_cost_diff = 1 try { if ( DA[m]["minutes"] ){ D_min_diff = DL[m]["minutes"] / ( DA[m]["minutes"] ) } if ( DA[m]["cost"] ){ D_cost_diff = DL[m]["cost"] / ( DA[m]["cost"] ) } def moneyPat = "\$##,###.##" def minutesPat = "##,###" def percentPat = "##.##%" def DL_minutes = numFormer( DL[m]["minutes"], minutesPat ) def DL_cost = numFormer( DL[m]["cost"], moneyPat ) def DA_minutes = numFormer( DA[m]["minutes"], minutesPat ) def DA_cost = numFormer( DA[m]["cost"], moneyPat ) def string_D_min_diff = numFormer( D_min_diff, percentPat ) def string_D_cost_diff = numFormer( D_cost_diff, percentPat ) row { label(value:m) label(value:DL_minutes) label(value:DL_cost) label(value:DA_minutes) label(value:DA_cost) if( D_min_diff > 1 ){ label(value:string_D_min_diff, style:red) } else { label(value:string_D_min_diff) } if( D_cost_diff > 1 ){ label(value:string_D_cost_diff, style:red) } else { label(value:string_D_cost_diff) } } } catch( NullPointerException e ){ println "caught: " + e } } col_month2.sort(false) }
Chart related processes
Here we define the logic for displaying the data labels using the check box component under the Int'l Chart. This features was added because we found the data labels sometimes overlapped, and removing the labels sometimes made it easier to read the chart.
$('#intlDataLabel01').onCheck{ def id = "#intlChart01" if ( it.isChecked() ){ def params = [title:"Int'l: LCR vs Actual Cost", dataLabel:true] getChart( IL, IA, id, params ) } else { def params = [title:"Int'l: LCR vs Actual Cost", dataLabel:false] getChart( IL, IA, id, params ) } } $('#intlChart01').append{ def params = [title:"Int'l: LCR vs Actual Cost", dataLabel:false] def chart = chartService.getChart( IL, IA, params ) def bufferImg = chart.createBufferedImage( 700, 400 ) def chartId = $('#intlChart01') chartId.setContent( bufferImg ) }Now, we compose the charts for the domestic data.
$('#domDataLabel01').onCheck{ def id = "#domChart01" if ( it.isChecked() ){ def params = [title:"Domestic: LCR vs Actual Cost", dataLabel:true] getChart( DL, DA, id, params ) } else { def params = [title:"Domestic: LCR vs Actual Cost", dataLabel:false] getChart( DL, DA, id, params ) } } $('#domChart01').append{ def params = [title:"Domestic: LCR vs Actual Cost", dataLabel:false] def chart = chartService.getChart( DL, DA, params ) def bufferImg = chart.createBufferedImage( 700, 400 ) def chartId = $('#domChart01') chartId.setContent( bufferImg ) } } //afterCompose()We have finished our main "afterCompose()" method, and define a couple of utility methods used above.
def getChart( L, A, id, params ){ def chart = chartService.getChart( L, A, params ) def bufferImg = chart.createBufferedImage( 700, 400 ) def chartId = $( id ) chartId.setContent( bufferImg ) }
def numFormer( value, pattern ){ DecimalFormat form = new DecimalFormat(pattern) String output = form.format(value) return output } }
0 comments:
Post a Comment