Sr. Web Developer
mediabistro.com
US-NY-New York

Justtechjobs.com Post A Job | Post A Resume

PHP and Adobe Air: Building a Time-tracking and Billing Application - Part II
Welcome back. In part 1 of this series, you created some PHP remote services and the Clocked! widget application. Part 2 covers PHP administration and completion of the timer widget. Let's jump right into Adobe Flex Builder and add more widget features.
Fill in the placeholder functions
You've already created placeholder functions in the <mx:Script> block of the clockedWidget.mxml file. Now it's time to fill them in, beginning with the getClientsResult() function. This function is called when the application gets the list of clients from PHP. When that happens, the application needs to respond by populating a drop-down list, or combo box, with the names of the clients. The code looks like this:

private function getClientsResult():void {
&nspbr;var clients:Array = clockedService.getClients.lastResult;
&nspbr;currentState='loggedIn'; &nspbr;clientCB.dataProvider = clients; &nspbr;clientCB.labelField = 'company_name'; }
First, the function places the result of the remote object call into the client variable, an array. Next, the dataProvider property of the ComboBox with the ID of clientCB is set to the clients array. The property that contains each company name is company_name, so the ComboBox's labelField is set to that value. Finally, the application's state is changed to loggedIn. But wait: Didn't you already do that in the logInResult function? Yes, so you need to delete or comment out the line that reads currentState = 'loggedIn' inside the logInResult function. Instead of changing the state on a successful login, you instead get the list of clients. Put this line in place of the now-removed state change:

clockedService.getClients();

With this change in place, the ComboBox will already be populated when the user is presented with the loggedIn view state. The next step is to get the list of projects belonging to the client from which users can select. First, add a change event listener to the clientCB ComboBox. Doing so allows you to call a function whenever a client is selected from the list. Inside the clientCB's <mx:ComboBox> tag, add this code:

change="clockedService.getProjects(clientCB.selectedItem.id)"

Now, whenever a user chooses a client from the list, that client's ID is sent to the remote PHP method getProjects. And you've probably guessed that you now need to complete the getProjectsResult function to handle the response. The completed function appears below:

private function getProjectsResult():void {
	var projects:Array = clockedService.getProjects.lastResult;
	projectCB.dataProvider = projects;
	projectCB.labelField = 'name';
}

The getProjectsResult function is similar to getClientsResult except that it populates the projectCB combo box and specifies the name property as the label field. Now, when a user selects a client, that client's projects will be retrieved and listed for the user.

Add support for the Ticket object
The next addition to the timer widget is support for the custom Ticket object. Recall that you already created a Ticket class in PHP with Ticket.php. For the class mapping in Zend_Amf to work, you need an Adobe ActionScript version of Ticket.php. First, right-click the src folder in the left pane of Flex Builder, then click New > Folder. When you create custom classes in ActionScript (as in many other languages), the protocol is to use dot-syntax to avoid confusion. It is also accepted practice to use domain names, as they are a handy way of ensuring uniqueness. Because I own the domain name flexandair.com, I create the folder structure com/flexandair/ and place my custom classes in the flexandair folder. For convenience, you'll want to do the same for this exercise. You can then access the classes in ActionScript by importing them from com.flexandair.MyClass. To set this up in your project, you can enter the full path in the New Folder dialog box: /com/flexandair. Click Finish, and both directories will be created.
Next, right-click the flexandair folder, then click New > ActionScript Class. For the class name, type TicketVO. Click Finish, and the basic class file is generated. The first things you need to add to this file are the [Bindable] tag and an alias definition for class mapping. Add these two lines right below the package declaration and opening bracket, on lines 3 and 4:

[Bindable]
[RemoteClass(alias="TicketVO")]

The [Bindable] tag simply allows data binding to this class's properties. The next line is to indicate that this class is mapped remotely as TicketVO. Next, add the same properties here in ActionScript that you added before to the Ticket.php file. Add these lines right below the class declaration and opening bracket, before the TicketVO constructor function:

public var duration:Number = 0;
public var category:String = '';
public var details:String = '';
public var client_id:int = 0;
public var project_id:int = 0;
public var account_id:int = 0;
public var start_time:Number = 0;
public var end_time:Number = 0;

As you can see, this file is similar to its PHP counterpart, and that's all there is to it.

Develop the timer

Before you can send tickets to PHP, you need to add the inner-workings of the timer. ActionScript contains a built- in Timer class, but despite its name, it isn't well suited for use as a timer in the literal sense. Because it relies on the application's frame rate, time calculations can vary significantly between computers. For this application, you use a custom StopWatch class. You can download the class file, StopWatch.as. I don't cover this class in line-by-line detail, but when you open it, you'll see that it is copiously commented and pretty straightforward. After you've downloaded the file, place it in the src/com/flexandair folder of your project alongside Ticket.as. Back inside the script block of clockedWidget.mxml, add another import statement below the Alert import to bring in these new classes:

import com.flexandair.*;

You also need to keep track of whether the clock is running. To do that, add a Boolean variable called isRunning. You also need variables for the active ticket, the user's hourly rate, the time increment used for billing purposes, and the billable amount accrued. Add these lines below the import statements:

[Bindable] private var stopWatch:StopWatch = new StopWatch();
private var isRunning:Boolean = false;
private var rate:int = 35;
private var increments:int = 900; //15 minutes in seconds
private var money:Number = 0;
public var ticket:TicketVO = new TicketVO();
To actually start the timer, use a function called startTimer():
private function startTimer():void { swLabel.addEventListener(Event.ENTER_FRAME, updateTime); stopWatch.start(); isRunning = true; }
The first line tells the clock-like label to run the updateTime function you'll create in a moment on each new frame. Then, the stopWatch's start method is called. Finally, the isRunning variable is set to True. Next, add the updateTime function referenced above:

private function updateTime(e:Event):void {
	swLabel.text = stopWatch.getTimeStampAsString();				
	money = Math.ceil((stopWatch.getTimeStamp()/100) / increments) * (.25 * rate);
	moneyAccruedLabel.text = myCF.format(money);
}

The updateTime function sets the clock label to the string that the custom StopWatch class returns. Then, the accrued value of the time elapsed is calculated, and that value is displayed in a label control. You'll see an ID you haven't created yet: myCF. Use an MXML component called a CurrencyFormatter to create a string from the raw number. To create the myCF CurrencyFormatter, place this MXML code above your script block:

<mx:CurrencyFormatter currencySymbol="$" decimalSeparatorTo="." precision="2" id="myCF"/>

The above CurrencyFormatter's format() method produces a string with a dollar sign ($), a decimal, and two decimal places.
Allowing users to control the timer With functions to start the timer and display pertinent information to users, you next need a function to start and stop everything at a user's instruction. It makes more sense to have just one button that both starts and stops the clock. The clock should operate like a switch, with the vast majority of user actions being either stop or start requests. To make the single button work this way, simply check the isRunning Boolean value whenever the button is clicked:

private function buttonClick():void {
	if (isRunning == false) {
		startTimer();
		clockBtn.label = "Stop";
	}
	else {
clockLabel.removeEventListener(Event.ENTER_FRAME, updateTime);
		stopWatch.pause();
		isRunning = false;
		clockBtn.label = "Start";
	}
}

As you can see, whenever the button is clicked, the state of the stopwatch is reversed and so is the label on the button. Note that Stop only means pause; in other words, the counter is not reset, and no values are cleared. To actually reset the timer, you need to use another function:

private function reset():void {
	stopWatch.pause();
	isRunning = false;
	clockBtn.label = 'Start';
	stopWatch = new StopWatch();
	swLabel.text = '00:00:00';
}

Like the pause code in the buttonClick() function, the reset function stops the clock and sets the button's label to Start. It also sets the stopWatch variable to a completely new instance of the StopWatch class, destroying any saved values from the previous instance. Finally, the clock label is updated to reflect the reset. Before doing anything else, add the click event listener to the clockBtn button control by adding click="buttonClick()" to its MXML tag. You should now be able to run the application, log in, then choose a client and project from the combo boxes. Figure 1 shows the application in its loggedIn state.
The running application
Figure 1. The running application

Submitting tickets To begin the process of submitting a new ticket, you need to add a Save button. In the Design view, select the loggedIn view state, then drag a button onto the application. Type Save for the label, and assign beginSubmit() as its on-click function.
One more piece of information you need from the user is the note for the active ticket. The widget application is meant to take up as little desktop real estate as possible. To that end, you won't insert a big text area control right on the widget for note entry. Instead, you'll use ActionScript to create a new native window containing a text area and a prompt for the user to add a note. Inside the new window, place a simple custom MXML component to accept user input and submit the ticket.
To start, right-click the src/com/flexandair folder, then click New > MXML Component. Name the component SaveForm, and set its width to 240 pixels and its height to 130 pixels. Click Finish, and the new component will be created and open in the main pane. Add the following lines of MXML to create a simple form:

<mx:Label x="9" y="3" text="Please enter notes/details:" fontFamily="Arial" color="#000000" fontSize="12"/>
<mx:TextArea id="notesTA" x="9" y="20" width="223" height="54" color="#030303"/>
<mx:Button x="107" y="105" label="Cancel" fontWeight="normal" click="closeWindow()"/>
<mx:Button x="173" y="105" label="Submit" fontWeight="normal" click="sendTicket()"/>
<mx:ComboBox x="70" y="75" width="140" id="catList" dataProvider="[Onsite, Development]"></mx:ComboBox>
<mx:Label x="8" y="77" text="Category:"/> 

Then, create a new <mx:Script> block. At the top of the script block, import your custom TicketVO class along with the mx.core.Window and mx.controls.Alert classes, then declare an activeTicket variable:

import com.flexandair.TicketVO;
import mx.core.Window
import mx.controls.Alert;			
public var activeTicket:TicketVO = new TicketVO;

You probably already know that you need a way to close the new window, so add the closeWindow function:

private function closeWindow():void {
	var window:Window = this.parent as Window;
	window.close();
}

Because this is the SaveForm component and its direct parent is the pop-up window, you can access the parent using this.parent. Then, the window's built-in close() method is called. Back in the main MXML file's script block, add the code to open and display the window. First, you need to add an import statement here for the mx.core.Window class like you did in the SaveForm. Then, add this function:

private function beginSubmit():void {
assembleTicket();
	var newWindow:Window = new Window(); 
	newWindow.systemChrome = NativeWindowSystemChrome.STANDARD; 
	newWindow.transparent = false; 
	newWindow.showTitleBar = true;
	newWindow.showStatusBar = false;
	newWindow.horizontalScrollPolicy = 'off';
	newWindow.verticalScrollPolicy = 'off';
	newWindow.title = "Add Notes"; 
	newWindow.width = 240; 
	newWindow.height = 140; 
	newWindow.open(true);
	var sf:SaveForm = new SaveForm();
	sf.activeTicket = ticket;
	newWindow.addChild(sf);	
}

Basically, this function creates a new window, sets some of its properties, and adds the custom SaveForm. Then, another function—assembleTicket()—is called to bring together your TicketVO object. Here's that function:


private function assembleTicket():void {
	clockLabel.removeEventListener(Event.ENTER_FRAME, updateTime);
	stopWatch.pause();
	isRunning = false;
	clockBtn.label = "Start";
	ticket = new TicketVO();
	ticket.client_id = clientCB.selectedItem.id;
	ticket.account_id = activeUser;
	ticket.project_id = projectCB.selectedItem.id;
	ticket.duration = stopWatch.getTimeStampAggregate();
	ticket.start_time = Math.round(stopWatch.initialStart / 1000);
	ticket.end_time = Math.round(new Date().getTime() / 1000);
}

This function first stops the clock. Then, several of the ticket's key properties are set—in fact, all the properties you know so far. The user adds the category and notes/description in the new window. The time stamps and duration are divided by 1000 so that you're working with seconds rather than milliseconds.
Back in the SaveForm.mxml component, add a new <mx:RemoteObject> like the one in clockedWidget.mxml. Add this outside the script block:


<mx:RemoteObject id="ticketService" endpoint="http://localhost/gateway.php" source="ClockedService" destination="Zend_Amf">
		<mx:method name="saveTicket" result="saveResult()"/>
</mx:RemoteObject>

Now, go back to the script block, and add the saveResult function mentioned above. It looks like this:


private function saveResult():void {
	Alert.show('Ticket saved','Success');
	closeWindow();
}

To actually send the completed ticket to PHP, construct a new function called sendTicket():

private function sendTicket():void {
	activeTicket.category = catList.selectedLabel;
	activeTicket.details = notesTA.text;
	ticketService.saveTicket(activeTicket);
}

This function adds the category and description to the ticket, then sends it using the ticketService remote object. Then, the saveResult function is called, the pop-up window is closed, and a success message is displayed.
With the widget's basic features complete, you're ready to move on to the PHP management section.

The PHP Interface
Because this article assumes that you're familiar with PHP website development, I won't cover the details of the PHP interface. You can find all the code you need to make it work in this article's source archive. Instead, I walk you through a sample use case for the clocked! PHP application. Please keep in mind that code distributed with these articles should not be deployed in a production environment; use them only on your local machine for testing.

Basic workflow To begin testing and using the Clocked! application, you first need to place the PHP files on your web server. Download the website.zip file included with this article, and extract its contents to a folder beneath your web root. Be sure to not overwrite any of your existing files. Then, locate the DBprops.php file. Change the variable values in this file to reflect your MySQL host, user, and password. You also need to set this information in the DBConnection.php file. The example website uses Zend_Db for database access, so be sure you give the DBConnection.php file the same access to the Zend Framework as you did in part 1 for the Zend_Amf files, like gateway.php.
First, create a new client. To do this, navigate to the folder in which you placed the example site in your web browser. This article assumes that you're using the web root folder (http://localhost/). You'll see some choices along the top, as shown in Figure 2.


Choices
Click here for larger image

Figure 2. Application choices

Click Clients to see a table listing any existing or sample clients. Click New Client to create a new one. When that's complete, click Add Project in the right-most column of the client list to add a new project for this client. When you have completed that, you're ready to run the Clocked! widget and add a timed ticket to your new client and project.
Run the widget for as long as you like. As is customary in timed billing, after the first second elapses, an increment is added to the billable total. The default increment is 15 minutes, or 0.25 hours. So, a billable time of 1 second will be invoiced as 0.25 hours.

After you stop the widget and save the new ticket, you can return to the PHP web interface and view it in the Tickets section. To test generating an invoice, go back to the Clients page. Next to your test client, click Add Invoice in the left-most column. All the available tickets for the selected client are displayed, and you can choose which to include in the invoice. Then, enter an invoice date and a due date, and click Submit. Your invoice will be generated and presented to you in Hypertext Markup Language (HTML) form in the browser window. For your convenience, a "Generate PDF" function is also included, which generates a Portable Document Format (PDF) file using the open source, stand- alone dompdf library and prompts you for a download.

What's next?
Now that you have a basic version of the Clocked! application, feel free to extend it further. You could add PHP authentication, sendmail integration, and further database features. You can also generate some really amazing PDF invoices with dompdf. See the official website for complete information and examples.
The widget Adobe AIR application has virtually limitless options for extending and customizing your users' experience. You might want to use a custom window chrome, or graphical skin, or add "minimize to tray" functionality. You can even add drag-and-drop file uploads to associate documents or other files with a particular client or project. To begin exploring all the exciting possibilities, visit the Adobe AIR Developer Center and the AIR for Flex Developers website.