49、旅行费用报告系统开发全解析

旅行费用报告系统开发

旅行费用报告系统开发全解析

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|解决表单加载时的计算问题|

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值