旅行费用报告系统开发全解析
1. 日期计算函数的优化
在开发过程中,测试通过后,通过 “grep to - do” 发现一个潜在问题。代码如下:
/**
* todo: will this fail on daylight savings time?
*/
public function addDays($start, $days) {
return date("Y-m-d", strtotime($start)+$days*86400);
}
经过分析,发现该函数在夏令时可能存在问题。在美国大部分地区,夏令时在十月的最后一个周日结束,当天会将时钟调回,这一天的秒数超过 86400 秒。为了验证这个问题,编写了测试代码:
function testDaylightSavingTime() {
$tvw = new TravelExpenseWeek (array());
$this->assertEquals("2004-10-30", $tvw->addDays("2004-10-29", 1));
$this->assertEquals("2004-10-31", $tvw->addDays("2004-10-29", 2));
$this->assertEquals("2004-11-01", $tvw->addDays("2004-10-29", 3), "no DST");
}
测试结果表明确实存在问题。通过查阅函数文档,发现
strtotime()
可以通过接受日期偏移量来进行日期计算。于是重新编写了
addDays()
函数:
public function addDays($start, $days) {
return date("Y-m-d", strtotime($start." +".$days." days"));
}
由于修改
getExpenseAmount
函数属于优化操作,可稍后进行,保持开发的连贯性很重要。
2. 完成旅行费用报告的初步版本
经过几个小时的工作,得到了旅行费用报告的初步版本。以下是
travel - expenses.php
文件的代码,它是用户从主菜单进入的着陆点:
<?php
require_once ("lib/common.php");
require_once ("lib/expense.phpm");
// is the user logged in?
if (!$session->isLoggedIn()) {
redirect ("index.php");
}
$user = $session->getUserObject();
$week = new TravelExpenseWeek (
array ('emp_id' => $user->id,
'week_start' => getCurrentStartWeek(),
'territory_worked' => $_REQUEST["territory_worked"],
'comments' => $_REQUEST["comments"],
'cash_advance' => $_REQUEST["cash_advance"],
'mileage_rate' => $GLOBALS["expense-mileage-travelrate"]),
$session->getDatabaseHandle());
// display
if ($_REQUEST["action"] != "persist_expense") {
$week->readWeek();
$smarty->assign_by_ref ("user", $user);
$smarty->assign_by_ref ("week", $week);
$smarty->assign('start_weeks', getStartWeeks());
$smarty->assign('current_start_week', getCurrentStartWeek());
$smarty->assign_by_ref ('expenses', $week->getExpensesMetaArray());
$smarty->assign('travelrate', $GLOBALS["expense-mileage-travelrate"]);
$smarty->display('travel-expenses.tpl');
exit();
}
// gather and persist week
$week->parse($_REQUEST);
$week->persist();
print "saved, thanks";
?>
这个脚本有两种操作模式:一种是从数据库读取旅行费用信息并显示(
readWeek
),另一种是根据
action
是否设置为
persist_expense
来保存表单数据(
persist
)。
getExpensesMetaArray()
函数是屏幕显示的核心,所有重量级对象都通过引用传递给 Smarty,避免自动创建副本。
3. 旅行费用页面的 Smarty 模板
以下是
templates/travel - expenses.tpl
文件的代码,用于显示旅行费用页面:
{include file="header.tpl" title="Widget World - Travel Expenses"}
{literal}
< SCRIPT TYPE="text/javascript" >
< !-
function reloadCalc () {
window.document.forms[0].week_start.value =
window.document.forms[1].week_start.value // hidden form
window.document.forms[0].submit(); // hidden form }
// - >
< /SCRIPT >
{/literal}
< h3 > Travel Expense Report < /h3 >
< form method="post" >
< input type="hidden" name="action" value="reload_expense" >
< input type="hidden" name="week_start" value="" >
< /form >
< form id="calc" name="calc" action="travel-expenses.php" method="post" >
< table border="0" width="100%" >
< tr >
< td > < b > Employee Name: < /b > < /td >
< td > {$user->first_name} {$user->last_name} < /td > < td > < b > Department: < /b > < /td >
< td > {$user->department} < /td >
< /tr >
< tr >
< td > < b > Number: < /b > < /td >
< td > {$user->id} < /td > < td > < b > Start Week: < /b > < /td >
< td > < SELECT NAME="week_start" onchange="reloadCalc()" > {html_options
values=$start_weeks output=$start_weeks selected=$current_start_week}
< /SELECT > < /td >
< /tr >
< tr >
< td > < b > Territory Worked: < /b > < /td >
< td colspan=3 > < input name="territory_worked" size=20 maxsize=60 value=
"{$week->territory_worked}" > < /td >
< /tr >
< /table >
< br > < br >
页面上的第一个表单类似于客户联系报告的表单,使用了一个特殊的 “隐藏” 表单,通过 JavaScript 填充起始周的值,当选择新的周时自动提交表单。第二个表单包含一些基本信息,如员工姓名、部门、起始周和工作区域。
以下是页面显示流程的 mermaid 流程图:
graph TD;
A[用户进入 travel - expenses.php] --> B{用户是否登录};
B -- 未登录 --> C[重定向到 index.php];
B -- 已登录 --> D[创建 TravelExpenseWeek 对象];
D --> E{action 是否为 persist_expense};
E -- 否 --> F[读取周信息并显示模板];
E -- 是 --> G[解析并保存表单数据];
4. 旅行费用页面的表格和特殊处理
接下来是页面的表格部分,用于显示费用信息:
< table border="0" >
< tr > < td > < /td > < td > Sun < /td > < td > Mon < /td > < td > Tues < /td > < td > Wed < /td > < td > Thur < /td >
< td > Fri < /td > < td > Sat < /td > < td > Total < /td > < /tr >
{section name=idx loop=$expenses}{strip}
< tr > < td > < b > {$expenses[idx].name} < /b > < /td > < td > < /td > < td > < /td > < td > < /td > < td > < /td >
< td > < /td > < td > < /td > < td > < /td > < td > < /td > < /tr >
{section name=idx2 loop=$expenses[idx].data}{strip}
{assign var="p" value=$expenses[idx].persist[idx2]}
< tr bgcolor="{cycle values="#eeeee, #dddddd"}" >
< td > {$expenses[idx].data[idx2]} < /td >
< td > < input name="{$expenses[idx].code}_sun_{$smarty.section.idx2.index}"
type="text" size="7" maxsize="17" value="{$week->getExpenseAmount(0, $p)}" > < /td >
这里有两个主要的循环,用于遍历费用数组。为了方便,动态创建了 Smarty 变量
$p
。对于交通费用,有特殊的处理:
{if $expenses[idx].code == 'trans'}
< tr > < td > {$expenses[idx].name} Subtotal < /td >
< td > < input readonly name="{$expenses[idx].code}_sun_sub"
type="text" size="7" maxsize="17" > < /td >
< tr > < td > Nbr of miles traveled < /td >
< td > < input name="mitr_sun" type="text" size="7" maxsize="17"
value="{$week->getExpenseAmount(0,
'trans_miles_traveled')}" > < /td >
{else}
< tr > < td > {$expenses[idx].name} Total < /td > < td > < input readonly
name="{$expenses[idx].code}_sun_sub" type="text" size="7"
maxsize="17" > < /td >
{/if}
这种处理方式避免了在交通费用底部为里程添加额外的行。
以下是表格数据显示的流程表格:
|步骤|操作|
|----|----|
|1|遍历费用数组|
|2|为每行费用创建表格行|
|3|为每个费用项创建表格单元格|
|4|如果是交通费用,进行特殊处理|
|5|显示小计、应付款项和周评论|
页面的其余部分显示了小计、应付款项和周评论:
< tr > < td > < /td > < td > < /td > < td > < /td > < td > < /td > < td > < /td >
< td colspan="3" > Subtotal < /td >
< td >
< input readonly name="subtotal" type="text" size="7" maxsize="17" >
< /td >
< /tr >
< tr > < td > < /td > < td > < /td > < td > < /td > < td > < /td > < td > < /td >
< td colspan="3" > Less Cash Advance < /td >
< td > < input name="cash_advance" type="text" size="7"
maxsize="17" value="{$week->cash_advance}" > < /td >
< /tr >
< tr > < td > < /td > < td > < /td > < td > < /td > < td > < /td > < td > < /td >
< td colspan="3" > Due Employee < /td >
< td > < input readonly name="totaldueemployee" type="text" size="7"
maxsize="17" > < /td > < /tr >
< tr > < td > < /td > < td > < /td > < td > < /td > < td > < /td > < td > < /td >
< td colspan="3" > Due Company < /td >
< td > < input readonly name="totalduecompany" type="text" size="7"
maxsize="17" > < /td > < /tr >
< /table >
< br > < br >
Comments: < br >
< TEXTAREA NAME="comments" COLS=80 ROWS=6 > {$week->comments} < /TEXTAREA >
< br > < br > < center >
< input type="submit" name="submit" value=" Submit Report " >
< /center >
< input type="hidden" name="action" value="persist_expense" >
< /form >
{include file="footer.tpl"}
虽然这个页面缺乏基本的错误检查和电子表格功能,但它已经可以在生产环境中使用,并且可以将其交付给客户以获取反馈。这个版本完成了故事 2、3 和 10。
旅行费用报告系统开发全解析
5. 实现旅行费用报告的电子表格功能
客户对旅行费用报告很满意,并希望其具备电子表格功能。为了实现这一需求,使用 JavaScript 的
onkeyup
事件,在输入新信息时实时重新计算。以下是新的
templates/travel - expenses.tpl
文件代码:
{include file="header.tpl" title="Widget World - Travel Expenses"}
{literal}
< SCRIPT TYPE="text/javascript" >
< !- function subtotal(thisForm, totalcell, cellArray) {
var subtot = 0;
for (var i=0; i < cellArray.length; i++) {
if(isNaN(thisForm[cellArray[i]].value))
thisForm[totalcell].value = 0;
else
subtot = Math.round(subtot*100 +
thisForm[cellArray[i]].value*100)/100;
}
thisForm[totalcell].value = subtot;
return subtot;
}
function subday (thisForm, totalcell, prefix, maxindex) {
var cellArray = new Array (maxindex);
for (var i=0; i < maxindex; i++) {
cellArray[i] = thisForm[prefix+i].name;
}
return subtotal(thisForm, totalcell, cellArray);
}
function subweek (thisForm, totalcell, prefix, postfix) {
return subtotal (thisForm, totalcell,
new Array (prefix+'sun'+postfix, prefix+'mon'+postfix,
prefix+'tue'+postfix, prefix+'wed'+postfix,
prefix+'thr'+postfix, prefix+'fri'+postfix,
prefix+'sat'+postfix));
}
function daycalc (thisForm, day, code, thisindex, maxindex) {
subday (thisForm, code+"_"+day+"_sub", code+"_"+day+"_", maxindex);
subweek(thisForm, code+"_week_sub_"+thisindex, code+"_", '_'+thisindex);
subday (thisForm, code+"_week_sub", code+"_week_sub_", maxindex);
totalcalc (thisForm);
return true;
}
function micalc (thisForm, day, travelrate) {
var totalcell = 'mitot_'+day;
var sourcecell = 'mitr_'+day;
// mileage input and mileage total
thisForm[totalcell].value = Math.round(
thisForm[sourcecell].value * 100 * travelrate)/100;
subweek (thisForm, 'mitr_tot', 'mitr_', ");
subweek (thisForm, 'mitot_tot', 'mitot_', ");
// trans total by day
thisForm["trans_"+day+"_sub2"].value = Math.round(
(thisForm[totalcell].value * 100) +
(thisForm["trans_"+day+"_sub"].value * 100))/100;
// grand total of week
subweek (thisForm, "trans_week_sub2", "trans_", "_sub2");
totalcalc (thisForm);
}
function checkTransInput (thisForm, day) {
if (thisForm["trans_"+day+"_sub"].value > 0 &&
thisForm["mitr_"+day].value == "" ) {
alert( "Please enter your mileage for "+day);
return false;
} return true;
}
function checkInputs(thisForm) {
if ( checkTransInput (thisForm, "sun") == false) { return false; }
if ( checkTransInput (thisForm, "mon") == false) { return false; }
if ( checkTransInput (thisForm, "tue") == false) { return false; }
if ( checkTransInput (thisForm, "wed") == false) { return false; }
if ( checkTransInput (thisForm, "thr") == false) { return false; }
if ( checkTransInput (thisForm, "fri") == false) { return false; }
if ( checkTransInput (thisForm, "sat") == false) { return false; }
if ( thisForm["subtotal"].value == 0) { alert ("Please enter data.");
return false; } if (thisForm["territory_worked"].value == "") {
alert ("Please enter your territory worked.");
return false; }
return true;
}
function totalcalc (thisForm) {
var sectionArray = new Array ('lodging', 'meals', 'trans', 'misc');
var subtotal = 0;
for (var i=0; i < sectionArray.length; i++) {
subtotal = subtotal +
Math.round(thisForm[sectionArray[i]+"_week_sub"].value*100)/100;
}
subtotal = subtotal + Math.round(thisForm["mitot_tot"].value*100)/100;
thisForm["subtotal"].value = subtotal;
var total = subtotal - Math.round(thisForm["cash_advance"].value*100)/100;
total = Math.round(total*100)/100;
if (total >= 0) {
thisForm["totaldueemployee"].value = total;
thisForm["totalduecompany"].value = "";
} else {
thisForm["totaldueemployee"].value = "";
thisForm["totalduecompany"].value = Math.round(total * 100)/100 *(-1);
}
}
function recalculate (thisForm, mileage) {
daycalc(thisForm, 'sun', 'lodging', '0', 3);
daycalc(thisForm, 'sat', 'lodging', '0', 3);
daycalc(thisForm, 'sat', 'lodging', '1', 3);
daycalc(thisForm, 'sat', 'lodging', '2', 3);
daycalc(thisForm, 'sun', 'meals', '0', 5);
daycalc(thisForm, 'sat', 'meals', '0', 5);
daycalc(thisForm, 'sat', 'meals', '1', 5);
daycalc(thisForm, 'sat', 'meals', '2', 5);
daycalc(thisForm, 'sat', 'meals', '3', 5);
daycalc(thisForm, 'sat', 'meals', '4', 5);
daycalc(thisForm, 'sun', 'trans', '0', 5);
daycalc(thisForm, 'sat', 'trans', '0', 5);
daycalc(thisForm, 'sat', 'trans', '1', 5);
daycalc(thisForm, 'sat', 'trans', '2', 5);
daycalc(thisForm, 'sat', 'trans', '3', 5);
daycalc(thisForm, 'sat', 'trans', '4', 5);
daycalc(thisForm, 'sun', 'misc', '0', 5);
daycalc(thisForm, 'sat', 'misc', '0', 5);
daycalc(thisForm, 'sat', 'misc', '1', 5);
daycalc(thisForm, 'sat', 'misc', '2', 5);
daycalc(thisForm, 'sat', 'misc', '3', 5);
daycalc(thisForm, 'sat', 'misc', '4', 5);
micalc(thisForm, 'sun', mileage);
micalc(thisForm, 'sat', mileage);
return (totalcalc(thisForm));
}
function reloadCalc () {
window.document.forms[0].week_start.value =
window.document.forms[1].week_start.value // hidden form
window.document.forms[0].submit(); // hidden form }
// - >
< /SCRIPT >
{/literal}
-
subtotal()函数:用于计算一组输入框的小计。 -
subday()和subweek()函数:分别用于计算每天和每周的小计。 -
daycalc()函数:在每个接受用户输入的单元格的onkeyup事件中调用,更新当天和本周的小计,并调用totalcalc()函数更新总小计。 -
micalc()函数:用于计算里程相关的小计和总计。 -
checkTransInput()和checkInputs()函数:用于基本的输入错误检查。 -
totalcalc()函数:计算总小计和应付款项。 -
recalculate()函数:用于重新计算整个表单,在出现错误时可通过点击 “Recalculate” 按钮调用。
以下是计算流程的 mermaid 流程图:
graph TD;
A[用户输入数据] --> B{是否为里程输入};
B -- 是 --> C[micalc 计算里程相关小计和总计];
B -- 否 --> D[daycalc 计算当天和本周小计];
C --> E[totalcalc 计算总小计和应付款项];
D --> E;
F[点击 Recalculate 按钮] --> G[recalculate 重新计算整个表单];
G --> E;
6. 修改模板以实现电子表格功能
为了让每个单元格响应
onkeyup
事件,修改
templates/travel - expenses.tpl
文件中的七个每日输入框:
< input name= " {$expenses[idx].code}_sun_{$smarty.section.idx2.index} "
onkeyup="return daycalc(this.form, 'sun', '{$expenses[idx].code}',
'{$smarty.section.idx2.index}', {$smarty.section.idx2.total})"
type="text" size="7" maxsize="17" value="{$week->getExpenseAmount(0, $p)}" >
同时,为里程输入框添加
onkeyup
函数:
< input name="mitr_sun" onkeyup="return micalc(this.form, 'sun',
{$travel_rate})" ...
添加一个 “Recalculate” 按钮,用于在出现问题时重新计算表单:
< br > < br >
< center >
< input type="button" value=" Recalculate " onclick="return recalculate
(this.form, {$travelrate})" >
< /center >
主提交按钮现在会在点击时运行
checkInputs()
函数进行基本的输入错误检查:
< input type="submit" name="submit" value=" Submit Report " onclick=
"return checkInputs(this.form);" >
7. 解决表单加载时的问题
当表单加载时,需要确保运行
recalculate()
函数。在
travel - expenses.php
文件中修改显示部分:
// display
if ($_REQUEST["action"] != "persist_expense") {
$week->readWeek();
$smarty->assign_by_ref ("user", $user);
$smarty->assign_by_ref ("week", $week);
$smarty->assign('start_weeks', getStartWeeks());
$smarty->assign('current_start_week', getCurrentStartWeek());
$smarty->assign_by_ref ('expenses', $week->getExpensesMetaArray());
$smarty->assign('travelrate', $GLOBALS["expense-mileage-travelrate"]);
$smarty->assign('formfunc',
"recalculate(window.document.forms[1],".
$GLOBALS["expense-mileage-travelrate"].")");
$smarty->display('travel-expenses.tpl');
exit();
}
将一个函数赋值给变量
formfunc
,然后修改
header.tpl
文件:
< !DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" >
< html >
< head >
< meta HTTP-EQUIV="content-type" CONTENT="text/html; charset=ISO-8859-1" >
< title > {$title|default:"no title"} < /title >
< /head >
< body onload="{$formfunc|default:""}" >
< h1 > Widget World < /h1 >
< hr > < p >
这样,表单在加载时会自动运行
recalculate()
函数。
总结
通过以上步骤,完成了旅行费用报告系统的开发,从初步版本到实现电子表格功能,逐步完善系统。虽然还有一些小问题需要解决,但系统已经具备了基本的功能,可以交付给客户使用并获取反馈。以下是开发步骤总结表格:
|步骤|操作|
|----|----|
|1|优化日期计算函数,解决夏令时问题|
|2|完成旅行费用报告的初步版本,实现数据读取和保存|
|3|创建 Smarty 模板显示旅行费用页面|
|4|处理表格数据显示和交通费用特殊情况|
|5|实现电子表格功能,包括实时计算和错误检查|
|6|修改模板以响应
onkeyup
事件和添加 Recalculate 按钮|
|7|解决表单加载时的计算问题|
旅行费用报告系统开发
超级会员免费看

被折叠的 条评论
为什么被折叠?



